Passed
Pull Request — master (#6637)
by
unknown
08:33
created

ExerciseLib::getUserQuestionScoreGlobal()   D

Complexity

Conditions 26

Size

Total Lines 77
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 26
eloc 48
nop 7
dl 0
loc 77
rs 4.1666
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\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 = \Chamilo\CoreBundle\Framework\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('Weighting').'</span>'
361
                                    : ''
362
                            )
363
                            .get_lang('CorrectAnswer').'</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('Select your answers')
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 Display::img(
1740
                        $questionRepo->getHotSpotImageUrl($questionEntity),
1741
                        $objQuestionTmp->selectTitle(),
1742
                        ['width' => '600px']
1743
                    );
1744
1745
                    return 0;
1746
                }
1747
            }
1748
1749
            if (!$only_questions) {
1750
                if ($show_title) {
1751
                    if ($exercise->display_category_name) {
1752
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->id);
1753
                    }
1754
                    echo $objQuestionTmp->getTitleToDisplay($exercise, $current_item);
1755
                }
1756
1757
                echo '
1758
                    <input type="hidden" name="hidden_hotspot_id" value="'.$questionId.'" />
1759
                    <div class="exercise_questions">
1760
                        '.$objQuestionTmp->selectDescription().'
1761
                        <div class="row">
1762
                            <div class="col-sm-8 col-md-9">
1763
                                <div id="annotation-canvas-'.$questionId.'" class="annotation-canvas center-block">
1764
                                </div>
1765
                                <script>
1766
                                    AnnotationQuestion({
1767
                                        questionId: '.$questionId.',
1768
                                        exerciseId: '.$exerciseId.',
1769
                                        relPath: \''.$relPath.'\',
1770
                                        courseId: '.$course_id.',
1771
                                    });
1772
                                </script>
1773
                            </div>
1774
                            <div class="col-sm-4 col-md-3">
1775
                                <div class="well well-sm" id="annotation-toolbar-'.$questionId.'">
1776
                                    <div class="btn-toolbar">
1777
                                        <div class="btn-group" data-toggle="buttons">
1778
                                            <label class="btn btn--plain active"
1779
                                                aria-label="'.get_lang('Add annotation path').'">
1780
                                                <input
1781
                                                    type="radio" value="0"
1782
                                                    name="'.$questionId.'-options" autocomplete="off" checked>
1783
                                                <span class="fas fa-pencil-alt" aria-hidden="true"></span>
1784
                                            </label>
1785
                                            <label class="btn btn--plain"
1786
                                                aria-label="'.get_lang('Add annotation text').'">
1787
                                                <input
1788
                                                    type="radio" value="1"
1789
                                                    name="'.$questionId.'-options" autocomplete="off">
1790
                                                <span class="fa fa-font fa-fw" aria-hidden="true"></span>
1791
                                            </label>
1792
                                        </div>
1793
                                    </div>
1794
                                    <ul class="list-unstyled"></ul>
1795
                                </div>
1796
                            </div>
1797
                        </div>
1798
                    </div>
1799
                ';
1800
            }
1801
            $objAnswerTmp = new Answer($questionId);
1802
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
1803
            unset($objAnswerTmp, $objQuestionTmp);
1804
        }
1805
1806
        return $nbrAnswers;
1807
    }
1808
1809
    /**
1810
     * Displays a table listing the quizzes where a question is used.
1811
     */
1812
    public static function showTestsWhereQuestionIsUsed(int $questionId, int $excludeTestId = 0): void
1813
    {
1814
        $em = Database::getManager();
1815
        $quizRepo = $em->getRepository(CQuiz::class);
1816
        $quizzes = $quizRepo->findQuizzesUsingQuestion($questionId, $excludeTestId);
1817
1818
        if (empty($quizzes)) {
1819
            echo '';
1820
            return;
1821
        }
1822
1823
        $result = [];
1824
1825
        foreach ($quizzes as $quiz) {
1826
            $link = $quiz->getFirstResourceLink();
1827
            $course = $link?->getCourse();
1828
            $session = $link?->getSession();
1829
            $courseId = $course?->getId() ?? 0;
1830
            $sessionId = $session?->getId() ?? 0;
1831
1832
            $url = api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.
1833
                'cid='.$courseId.'&sid='.$sessionId.'&gid=0&gradebook=0&origin='.
1834
                '&exerciseId='.$quiz->getIid().'&r=1';
1835
1836
1837
            $result[] = [
1838
                $course?->getTitle() ?? '-',
1839
                $session?->getTitle() ?? '-',
1840
                $quiz->getTitle(),
1841
                '<a href="'.$url.'">'.Display::getMdiIcon(
1842
                    'order-bool-ascending-variant',
1843
                    'ch-tool-icon',
1844
                    null,
1845
                    ICON_SIZE_SMALL,
1846
                    get_lang('Edit')
1847
                ).'</a>',
1848
            ];
1849
        }
1850
1851
        $headers = [
1852
            get_lang('Course'),
1853
            get_lang('Session'),
1854
            get_lang('Test'),
1855
            get_lang('Link to test edition'),
1856
        ];
1857
1858
        $title = Display::div(
1859
            get_lang('Question also used in the following tests'),
1860
            ['class' => 'section-title', 'style' => 'margin-top: 25px; border-bottom: none']
1861
        );
1862
1863
        echo $title.Display::table($headers, $result);
1864
    }
1865
1866
    /**
1867
     * @param int $exeId
1868
     *
1869
     * @return array
1870
     */
1871
    public static function get_exercise_track_exercise_info($exeId)
1872
    {
1873
        $quizTable = Database::get_course_table(TABLE_QUIZ_TEST);
1874
        $trackExerciseTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1875
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
1876
        $exeId = (int) $exeId;
1877
        $result = [];
1878
        if (!empty($exeId)) {
1879
            $sql = " SELECT q.*, tee.*
1880
                FROM $quizTable as q
1881
                INNER JOIN $trackExerciseTable as tee
1882
                ON q.iid = tee.exe_exo_id
1883
                WHERE
1884
                    tee.exe_id = $exeId";
1885
1886
            $sqlResult = Database::query($sql);
1887
            if (Database::num_rows($sqlResult)) {
1888
                $result = Database::fetch_assoc($sqlResult);
1889
                $result['duration_formatted'] = '';
1890
                if (!empty($result['exe_duration'])) {
1891
                    $time = api_format_time($result['exe_duration'], 'js');
1892
                    $result['duration_formatted'] = $time;
1893
                }
1894
            }
1895
        }
1896
1897
        return $result;
1898
    }
1899
1900
    /**
1901
     * Validates the time control key.
1902
     *
1903
     * @param int $lp_id
1904
     * @param int $lp_item_id
1905
     *
1906
     * @return bool
1907
     */
1908
    public static function exercise_time_control_is_valid(Exercise $exercise, $lp_id = 0, $lp_item_id = 0)
1909
    {
1910
        $exercise_id = $exercise->getId();
1911
        $expiredTime = $exercise->expired_time;
1912
1913
        if (!empty($expiredTime)) {
1914
            $current_expired_time_key = self::get_time_control_key(
1915
                $exercise_id,
1916
                $lp_id,
1917
                $lp_item_id
1918
            );
1919
            if (isset($_SESSION['expired_time'][$current_expired_time_key])) {
1920
                $current_time = time();
1921
                $expired_time = api_strtotime(
1922
                    $_SESSION['expired_time'][$current_expired_time_key]->format('Y-m-d H:i:s'),
1923
                    'UTC'
1924
                );
1925
                $total_time_allowed = $expired_time + 30;
1926
                if ($total_time_allowed < $current_time) {
1927
                    return false;
1928
                }
1929
1930
                return true;
1931
            }
1932
1933
            return false;
1934
        }
1935
1936
        return true;
1937
    }
1938
1939
    /**
1940
     * Deletes the time control token.
1941
     *
1942
     * @param int $exercise_id
1943
     * @param int $lp_id
1944
     * @param int $lp_item_id
1945
     */
1946
    public static function exercise_time_control_delete(
1947
        $exercise_id,
1948
        $lp_id = 0,
1949
        $lp_item_id = 0
1950
    ) {
1951
        $current_expired_time_key = self::get_time_control_key(
1952
            $exercise_id,
1953
            $lp_id,
1954
            $lp_item_id
1955
        );
1956
        unset($_SESSION['expired_time'][$current_expired_time_key]);
1957
    }
1958
1959
    /**
1960
     * Generates the time control key.
1961
     *
1962
     * @param int $exercise_id
1963
     * @param int $lp_id
1964
     * @param int $lp_item_id
1965
     *
1966
     * @return string
1967
     */
1968
    public static function get_time_control_key(
1969
        $exercise_id,
1970
        $lp_id = 0,
1971
        $lp_item_id = 0
1972
    ) {
1973
        $exercise_id = (int) $exercise_id;
1974
        $lp_id = (int) $lp_id;
1975
        $lp_item_id = (int) $lp_item_id;
1976
1977
        return
1978
            api_get_course_int_id().'_'.
1979
            api_get_session_id().'_'.
1980
            $exercise_id.'_'.
1981
            api_get_user_id().'_'.
1982
            $lp_id.'_'.
1983
            $lp_item_id;
1984
    }
1985
1986
    /**
1987
     * Get session time control.
1988
     *
1989
     * @param int $exercise_id
1990
     * @param int $lp_id
1991
     * @param int $lp_item_id
1992
     *
1993
     * @return int
1994
     */
1995
    public static function get_session_time_control_key(
1996
        $exercise_id,
1997
        $lp_id = 0,
1998
        $lp_item_id = 0
1999
    ) {
2000
        $return_value = 0;
2001
        $time_control_key = self::get_time_control_key(
2002
            $exercise_id,
2003
            $lp_id,
2004
            $lp_item_id
2005
        );
2006
        if (isset($_SESSION['expired_time']) && isset($_SESSION['expired_time'][$time_control_key])) {
2007
            $return_value = $_SESSION['expired_time'][$time_control_key];
2008
        }
2009
2010
        return $return_value;
2011
    }
2012
2013
    /**
2014
     * Gets count of exam results.
2015
     *
2016
     * @param int   $exerciseId
2017
     * @param array $conditions
2018
     * @param int   $courseId
2019
     * @param bool  $showSession
2020
     *
2021
     * @return array
2022
     */
2023
    public static function get_count_exam_results($exerciseId, $conditions, $courseId, $showSession = false)
2024
    {
2025
        $count = self::get_exam_results_data(
2026
            null,
2027
            null,
2028
            null,
2029
            null,
2030
            $exerciseId,
2031
            $conditions,
2032
            true,
2033
            $courseId,
2034
            $showSession
2035
        );
2036
2037
        return $count;
2038
    }
2039
2040
    /**
2041
     * Gets the exam'data results.
2042
     *
2043
     * @todo this function should be moved in a library  + no global calls
2044
     *
2045
     * @param int    $from
2046
     * @param int    $number_of_items
2047
     * @param int    $column
2048
     * @param string $direction
2049
     * @param int    $exercise_id
2050
     * @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...
2051
     * @param bool   $get_count
2052
     * @param int    $courseId
2053
     * @param bool   $showSessionField
2054
     * @param bool   $showExerciseCategories
2055
     * @param array  $userExtraFieldsToAdd
2056
     * @param bool   $useCommaAsDecimalPoint
2057
     * @param bool   $roundValues
2058
     * @param bool   $getOnyIds
2059
     *
2060
     * @return array
2061
     */
2062
    public static function get_exam_results_data(
2063
        $from,
2064
        $number_of_items,
2065
        $column,
2066
        $direction,
2067
        $exercise_id,
2068
        $extra_where_conditions = null,
2069
        $get_count = false,
2070
        $courseId = null,
2071
        $showSessionField = false,
2072
        $showExerciseCategories = false,
2073
        $userExtraFieldsToAdd = [],
2074
        $useCommaAsDecimalPoint = false,
2075
        $roundValues = false,
2076
        $getOnyIds = false
2077
    ) {
2078
        //@todo replace all this globals
2079
        global $filter;
2080
        $courseId = (int) $courseId;
2081
        $course = api_get_course_entity($courseId);
2082
        if (null === $course) {
2083
            return [];
2084
        }
2085
2086
        $sessionId = api_get_session_id();
2087
        $exercise_id = (int) $exercise_id;
2088
2089
        $is_allowedToEdit =
2090
            api_is_allowed_to_edit(null, true) ||
2091
            api_is_allowed_to_edit(true) ||
2092
            api_is_drh() ||
2093
            api_is_student_boss() ||
2094
            api_is_session_admin();
2095
        $TBL_USER = Database::get_main_table(TABLE_MAIN_USER);
2096
        $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
2097
        $TBL_GROUP_REL_USER = Database::get_course_table(TABLE_GROUP_USER);
2098
        $TBL_GROUP = Database::get_course_table(TABLE_GROUP);
2099
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2100
        $tblTrackAttemptQualify = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY);
2101
2102
        $session_id_and = '';
2103
        $sessionCondition = '';
2104
        if (!$showSessionField) {
2105
            $session_id_and = api_get_session_condition($sessionId, true, false, 'te.session_id');
2106
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'ttte.session_id');
2107
        }
2108
2109
        $exercise_where = '';
2110
        if (!empty($exercise_id)) {
2111
            $exercise_where .= ' AND te.exe_exo_id = '.$exercise_id.'  ';
2112
        }
2113
2114
        // sql for chamilo-type tests for teacher / tutor view
2115
        $sql_inner_join_tbl_track_exercices = "
2116
        (
2117
            SELECT DISTINCT ttte.*, if(tr.exe_id,1, 0) as revised
2118
            FROM $TBL_TRACK_EXERCISES ttte
2119
            LEFT JOIN $tblTrackAttemptQualify tr
2120
            ON (ttte.exe_id = tr.exe_id) AND tr.author > 0
2121
            WHERE
2122
                c_id = $courseId AND
2123
                exe_exo_id = $exercise_id
2124
                $sessionCondition
2125
        )";
2126
2127
        if ($is_allowedToEdit) {
2128
            //@todo fix to work with COURSE_RELATION_TYPE_RRHH in both queries
2129
            // Hack in order to filter groups
2130
            $sql_inner_join_tbl_user = '';
2131
            if (strpos($extra_where_conditions, 'group_id')) {
2132
                $sql_inner_join_tbl_user = "
2133
                (
2134
                    SELECT
2135
                        u.id as user_id,
2136
                        firstname,
2137
                        lastname,
2138
                        official_code,
2139
                        email,
2140
                        username,
2141
                        g.name as group_name,
2142
                        g.id as group_id
2143
                    FROM $TBL_USER u
2144
                    INNER JOIN $TBL_GROUP_REL_USER gru
2145
                    ON (gru.user_id = u.id AND gru.c_id= $courseId )
2146
                    INNER JOIN $TBL_GROUP g
2147
                    ON (gru.group_id = g.id AND g.c_id= $courseId )
2148
                    WHERE u.active <> ".USER_SOFT_DELETED."
2149
                )";
2150
            }
2151
2152
            if (strpos($extra_where_conditions, 'group_all')) {
2153
                $extra_where_conditions = str_replace(
2154
                    "AND (  group_id = 'group_all'  )",
2155
                    '',
2156
                    $extra_where_conditions
2157
                );
2158
                $extra_where_conditions = str_replace(
2159
                    "AND group_id = 'group_all'",
2160
                    '',
2161
                    $extra_where_conditions
2162
                );
2163
                $extra_where_conditions = str_replace(
2164
                    "group_id = 'group_all' AND",
2165
                    '',
2166
                    $extra_where_conditions
2167
                );
2168
2169
                $sql_inner_join_tbl_user = "
2170
                (
2171
                    SELECT
2172
                        u.id as user_id,
2173
                        firstname,
2174
                        lastname,
2175
                        official_code,
2176
                        email,
2177
                        username,
2178
                        '' as group_name,
2179
                        '' as group_id
2180
                    FROM $TBL_USER u
2181
                    WHERE u.active <> ".USER_SOFT_DELETED."
2182
                )";
2183
                $sql_inner_join_tbl_user = null;
2184
            }
2185
2186
            if (strpos($extra_where_conditions, 'group_none')) {
2187
                $extra_where_conditions = str_replace(
2188
                    "AND (  group_id = 'group_none'  )",
2189
                    "AND (  group_id is null  )",
2190
                    $extra_where_conditions
2191
                );
2192
                $extra_where_conditions = str_replace(
2193
                    "AND group_id = 'group_none'",
2194
                    "AND (  group_id is null  )",
2195
                    $extra_where_conditions
2196
                );
2197
                $sql_inner_join_tbl_user = "
2198
            (
2199
                SELECT
2200
                    u.id as user_id,
2201
                    firstname,
2202
                    lastname,
2203
                    official_code,
2204
                    email,
2205
                    username,
2206
                    g.name as group_name,
2207
                    g.iid as group_id
2208
                FROM $TBL_USER u
2209
                LEFT OUTER JOIN $TBL_GROUP_REL_USER gru
2210
                ON (gru.user_id = u.id AND gru.c_id= $courseId )
2211
                LEFT OUTER JOIN $TBL_GROUP g
2212
                ON (gru.group_id = g.id AND g.c_id = $courseId )
2213
                WHERE u.active <> ".USER_SOFT_DELETED."
2214
            )";
2215
            }
2216
2217
            // All
2218
            $is_empty_sql_inner_join_tbl_user = false;
2219
            if (empty($sql_inner_join_tbl_user)) {
2220
                $is_empty_sql_inner_join_tbl_user = true;
2221
                $sql_inner_join_tbl_user = "
2222
            (
2223
                SELECT u.id as user_id, firstname, lastname, email, username, ' ' as group_name, '' as group_id, official_code
2224
                FROM $TBL_USER u
2225
                WHERE u.active <> ".USER_SOFT_DELETED." AND u.status NOT IN(".api_get_users_status_ignored_in_reports('string').")
2226
            )";
2227
            }
2228
2229
            $sqlFromOption = " , $TBL_GROUP_REL_USER AS gru ";
2230
            $sqlWhereOption = "  AND gru.c_id = $courseId AND gru.user_id = user.id ";
2231
            $first_and_last_name = api_is_western_name_order() ? "firstname, lastname" : "lastname, firstname";
2232
2233
            if ($get_count) {
2234
                $sql_select = 'SELECT count(te.exe_id) ';
2235
            } else {
2236
                $sql_select = "SELECT DISTINCT
2237
                    user.user_id,
2238
                    $first_and_last_name,
2239
                    official_code,
2240
                    ce.title,
2241
                    username,
2242
                    te.score,
2243
                    te.max_score,
2244
                    te.exe_date,
2245
                    te.exe_id,
2246
                    te.session_id,
2247
                    email as exemail,
2248
                    te.start_date,
2249
                    ce.expired_time,
2250
                    steps_counter,
2251
                    exe_user_id,
2252
                    te.exe_duration,
2253
                    te.status as completion_status,
2254
                    propagate_neg,
2255
                    revised,
2256
                    group_name,
2257
                    group_id,
2258
                    orig_lp_id,
2259
                    te.user_ip";
2260
            }
2261
2262
            $sql = " $sql_select
2263
                FROM $TBL_EXERCISES AS ce
2264
                INNER JOIN $sql_inner_join_tbl_track_exercices AS te
2265
                ON (te.exe_exo_id = ce.iid)
2266
                INNER JOIN $sql_inner_join_tbl_user AS user
2267
                ON (user.user_id = exe_user_id)
2268
                WHERE
2269
                    te.c_id = $courseId $session_id_and AND
2270
                    ce.active <> -1
2271
                    $exercise_where
2272
                    $extra_where_conditions
2273
                ";
2274
        }
2275
2276
        if (empty($sql)) {
2277
            return false;
2278
        }
2279
2280
        if ($get_count) {
2281
            $resx = Database::query($sql);
2282
            $rowx = Database::fetch_row($resx, 'ASSOC');
2283
2284
            return $rowx[0];
2285
        }
2286
2287
        $teacher_list = CourseManager::get_teacher_list_from_course_code($course->getCode());
2288
        $teacher_id_list = [];
2289
        if (!empty($teacher_list)) {
2290
            foreach ($teacher_list as $teacher) {
2291
                $teacher_id_list[] = $teacher['user_id'];
2292
            }
2293
        }
2294
2295
        $scoreDisplay = new ScoreDisplay();
2296
        $decimalSeparator = '.';
2297
        $thousandSeparator = ',';
2298
2299
        if ($useCommaAsDecimalPoint) {
2300
            $decimalSeparator = ',';
2301
            $thousandSeparator = '';
2302
        }
2303
2304
        $listInfo = [];
2305
        $column = !empty($column) ? Database::escape_string($column) : null;
2306
        $from = (int) $from;
2307
        $number_of_items = (int) $number_of_items;
2308
        $direction = !in_array(strtolower(trim($direction)), ['asc', 'desc']) ? 'asc' : $direction;
2309
2310
        if (!empty($column)) {
2311
            $sql .= " ORDER BY `$column` $direction ";
2312
        }
2313
2314
        if (!$getOnyIds) {
2315
            $sql .= " LIMIT $from, $number_of_items";
2316
        }
2317
2318
        $results = [];
2319
        $resx = Database::query($sql);
2320
        while ($rowx = Database::fetch_assoc($resx)) {
2321
            $results[] = $rowx;
2322
        }
2323
2324
        $group_list = GroupManager::get_group_list(null, $course);
2325
        $clean_group_list = [];
2326
        if (!empty($group_list)) {
2327
            foreach ($group_list as $group) {
2328
                $clean_group_list[$group['iid']] = $group['title'];
2329
            }
2330
        }
2331
2332
        $lp_list_obj = new LearnpathList(api_get_user_id());
2333
        $lp_list = $lp_list_obj->get_flat_list();
2334
        $oldIds = array_column($lp_list, 'lp_old_id', 'iid');
2335
2336
        if (is_array($results)) {
2337
            $users_array_id = [];
2338
            $from_gradebook = false;
2339
            if (isset($_GET['gradebook']) && 'view' === $_GET['gradebook']) {
2340
                $from_gradebook = true;
2341
            }
2342
            $sizeof = count($results);
2343
            $locked = api_resource_is_locked_by_gradebook(
2344
                $exercise_id,
2345
                LINK_EXERCISE
2346
            );
2347
2348
            $timeNow = strtotime(api_get_utc_datetime());
2349
            // Looping results
2350
            for ($i = 0; $i < $sizeof; $i++) {
2351
                $revised = $results[$i]['revised'];
2352
                if ('incomplete' === $results[$i]['completion_status']) {
2353
                    // If the exercise was incomplete, we need to determine
2354
                    // if it is still into the time allowed, or if its
2355
                    // allowed time has expired and it can be closed
2356
                    // (it's "unclosed")
2357
                    $minutes = $results[$i]['expired_time'];
2358
                    if (0 == $minutes) {
2359
                        // There's no time limit, so obviously the attempt
2360
                        // can still be "ongoing", but the teacher should
2361
                        // be able to choose to close it, so mark it as
2362
                        // "unclosed" instead of "ongoing"
2363
                        $revised = 2;
2364
                    } else {
2365
                        $allowedSeconds = $minutes * 60;
2366
                        $timeAttemptStarted = strtotime($results[$i]['start_date']);
2367
                        $secondsSinceStart = $timeNow - $timeAttemptStarted;
2368
                        if ($secondsSinceStart > $allowedSeconds) {
2369
                            $revised = 2; // mark as "unclosed"
2370
                        } else {
2371
                            $revised = 3; // mark as "ongoing"
2372
                        }
2373
                    }
2374
                }
2375
2376
                if ($from_gradebook && ($is_allowedToEdit)) {
2377
                    if (in_array(
2378
                        $results[$i]['username'].$results[$i]['firstname'].$results[$i]['lastname'],
2379
                        $users_array_id
2380
                    )) {
2381
                        continue;
2382
                    }
2383
                    $users_array_id[] = $results[$i]['username'].$results[$i]['firstname'].$results[$i]['lastname'];
2384
                }
2385
2386
                $lp_obj = isset($results[$i]['orig_lp_id']) && isset($lp_list[$results[$i]['orig_lp_id']]) ? $lp_list[$results[$i]['orig_lp_id']] : null;
2387
                if (empty($lp_obj)) {
2388
                    // Try to get the old id (id instead of iid)
2389
                    $lpNewId = isset($results[$i]['orig_lp_id']) && isset($oldIds[$results[$i]['orig_lp_id']]) ? $oldIds[$results[$i]['orig_lp_id']] : null;
2390
                    if ($lpNewId) {
2391
                        $lp_obj = isset($lp_list[$lpNewId]) ? $lp_list[$lpNewId] : null;
2392
                    }
2393
                }
2394
                $lp_name = null;
2395
                if ($lp_obj) {
2396
                    $url = api_get_path(WEB_CODE_PATH).
2397
                        'lp/lp_controller.php?'.api_get_cidreq().'&action=view&lp_id='.$results[$i]['orig_lp_id'];
2398
                    $lp_name = Display::url(
2399
                        $lp_obj['lp_name'],
2400
                        $url,
2401
                        ['target' => '_blank']
2402
                    );
2403
                }
2404
2405
                // Add all groups by user
2406
                $group_name_list = '';
2407
                if ($is_empty_sql_inner_join_tbl_user) {
2408
                    $group_list = GroupManager::get_group_ids(
2409
                        api_get_course_int_id(),
2410
                        $results[$i]['user_id']
2411
                    );
2412
2413
                    foreach ($group_list as $id) {
2414
                        if (isset($clean_group_list[$id])) {
2415
                            $group_name_list .= $clean_group_list[$id].'<br/>';
2416
                        }
2417
                    }
2418
                    $results[$i]['group_name'] = $group_name_list;
2419
                }
2420
2421
                $results[$i]['exe_duration'] = !empty($results[$i]['exe_duration']) ? round($results[$i]['exe_duration'] / 60) : 0;
2422
                $id = $results[$i]['exe_id'];
2423
                $dt = api_convert_and_format_date($results[$i]['max_score']);
2424
2425
                // we filter the results if we have the permission to
2426
                $result_disabled = 0;
2427
                if (isset($results[$i]['results_disabled'])) {
2428
                    $result_disabled = (int) $results[$i]['results_disabled'];
2429
                }
2430
                if (0 == $result_disabled) {
2431
                    $my_res = $results[$i]['score'];
2432
                    $my_total = $results[$i]['max_score'];
2433
                    $results[$i]['start_date'] = api_get_local_time($results[$i]['start_date']);
2434
                    $results[$i]['exe_date'] = api_get_local_time($results[$i]['exe_date']);
2435
2436
                    if (!$results[$i]['propagate_neg'] && $my_res < 0) {
2437
                        $my_res = 0;
2438
                    }
2439
2440
                    $score = self::show_score(
2441
                        $my_res,
2442
                        $my_total,
2443
                        true,
2444
                        true,
2445
                        false,
2446
                        false,
2447
                        $decimalSeparator,
2448
                        $thousandSeparator,
2449
                        $roundValues
2450
                    );
2451
2452
                    $actions = '<div class="pull-right">';
2453
                    if ($is_allowedToEdit) {
2454
                        if (isset($teacher_id_list)) {
2455
                            if (in_array(
2456
                                $results[$i]['exe_user_id'],
2457
                                $teacher_id_list
2458
                            )) {
2459
                                $actions .= Display::getMdiIcon('human-male-board', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Trainer'));
2460
                            }
2461
                        }
2462
                        $revisedLabel = '';
2463
                        switch ($revised) {
2464
                            case 0:
2465
                                $actions .= "<a href='exercise_show.php?".api_get_cidreq()."&action=qualify&id=$id'>".
2466
                                    Display::getMdiIcon(ActionIcon::GRADE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Grade activity')
2467
                                    );
2468
                                $actions .= '</a>';
2469
                                $revisedLabel = Display::label(
2470
                                    get_lang('Not validated'),
2471
                                    'info'
2472
                                );
2473
                                break;
2474
                            case 1:
2475
                                $actions .= "<a href='exercise_show.php?".api_get_cidreq()."&action=edit&id=$id'>".
2476
                                    Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit'));
2477
                                $actions .= '</a>';
2478
                                $revisedLabel = Display::label(
2479
                                    get_lang('Validated'),
2480
                                    'success'
2481
                                );
2482
                                break;
2483
                            case 2: //finished but not marked as such
2484
                                $actions .= '<a href="exercise_report.php?'
2485
                                    .api_get_cidreq()
2486
                                    .'&exerciseId='
2487
                                    .$exercise_id
2488
                                    .'&a=close&id='
2489
                                    .$id
2490
                                    .'">'.
2491
                                    Display::getMdiIcon(ActionIcon::LOCK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Mark attempt as closed'));
2492
                                $actions .= '</a>';
2493
                                $revisedLabel = Display::label(
2494
                                    get_lang('Unclosed'),
2495
                                    'warning'
2496
                                );
2497
                                break;
2498
                            case 3: //still ongoing
2499
                                $actions .= Display::getMdiIcon('clock', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Attempt still going on. Please wait.'));
2500
                                $actions .= '';
2501
                                $revisedLabel = Display::label(
2502
                                    get_lang('Ongoing'),
2503
                                    'danger'
2504
                                );
2505
                                break;
2506
                        }
2507
2508
                        if (2 == $filter) {
2509
                            $actions .= ' <a href="exercise_history.php?'.api_get_cidreq().'&exe_id='.$id.'">'.
2510
                                Display::getMdiIcon('clipboard-text-clock', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('View changes history')
2511
                                ).'</a>';
2512
                        }
2513
2514
                        // Admin can always delete the attempt
2515
                        if ((false == $locked || api_is_platform_admin()) && !api_is_student_boss()) {
2516
                            $ip = Tracking::get_ip_from_user_event(
2517
                                $results[$i]['exe_user_id'],
2518
                                api_get_utc_datetime(),
2519
                                false
2520
                            );
2521
                            $actions .= '<a href="http://www.whatsmyip.org/ip-geo-location/?ip='.$ip.'" target="_blank">'
2522
                                .Display::getMdiIcon('information', 'ch-tool-icon', null, ICON_SIZE_SMALL, $ip)
2523
                                .'</a>';
2524
2525
                            $recalculateUrl = api_get_path(WEB_CODE_PATH).'exercise/recalculate.php?'.
2526
                                api_get_cidreq().'&'.
2527
                                http_build_query([
2528
                                    'id' => $id,
2529
                                    'exercise' => $exercise_id,
2530
                                    'user' => $results[$i]['exe_user_id'],
2531
                                ]);
2532
                            $actions .= Display::url(
2533
                                Display::getMdiIcon(ActionIcon::REFRESH, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Recalculate results')),
2534
                                $recalculateUrl,
2535
                                [
2536
                                    'data-exercise' => $exercise_id,
2537
                                    'data-user' => $results[$i]['exe_user_id'],
2538
                                    'data-id' => $id,
2539
                                    'class' => 'exercise-recalculate',
2540
                                ]
2541
                            );
2542
2543
                            $exportPdfUrl = api_get_path(WEB_CODE_PATH).'exercise/exercise_report.php?'.
2544
                                api_get_cidreq().'&exerciseId='.$exercise_id.'&action=export_pdf&attemptId='.$id.'&userId='.(int) $results[$i]['exe_user_id'];
2545
                            $actions .= '<a href="'.$exportPdfUrl.'" target="_blank">'
2546
                                .Display::getMdiIcon(ActionIcon::EXPORT_PDF, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Export to PDF'))
2547
                                .'</a>';
2548
2549
                            $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'];
2550
                            $emailLink = '<a href="'.$sendMailUrl.'">'
2551
                                .Display::getMdiIcon(ActionIcon::SEND_SINGLE_EMAIL, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Send by e-mail'))
2552
                                .'</a>';
2553
2554
                            $filterByUser = isset($_GET['filter_by_user']) ? (int) $_GET['filter_by_user'] : 0;
2555
                            $delete_link = '<a
2556
                                href="exercise_report.php?'.api_get_cidreq().'&filter_by_user='.$filterByUser.'&filter='.$filter.'&exerciseId='.$exercise_id.'&delete=delete&did='.$id.'"
2557
                                onclick=
2558
                                "javascript:if(!confirm(\''.sprintf(addslashes(get_lang('Delete attempt?')), $results[$i]['username'], $dt).'\')) return false;"
2559
                                >';
2560
                            $delete_link .= Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, addslashes(get_lang('Delete'))).'</a>';
2561
2562
                            if (api_is_drh() && !api_is_platform_admin()) {
2563
                                $delete_link = null;
2564
                            }
2565
                            if (api_is_session_admin()) {
2566
                                $delete_link = '';
2567
                            }
2568
                            if (3 == $revised) {
2569
                                $delete_link = null;
2570
                            }
2571
                            if (1 !== $revised) {
2572
                                $emailLink = '';
2573
                            }
2574
                            $actions .= $delete_link;
2575
                            $actions .= $emailLink;
2576
                        }
2577
                    } else {
2578
                        $attempt_url = api_get_path(WEB_CODE_PATH).'exercise/result.php?'.api_get_cidreq().'&id='.$results[$i]['exe_id'].'&sid='.$sessionId;
2579
                        $attempt_link = Display::url(
2580
                            get_lang('Show'),
2581
                            $attempt_url,
2582
                            [
2583
                                'class' => 'ajax btn btn--plain',
2584
                                'data-title' => get_lang('Show'),
2585
                            ]
2586
                        );
2587
                        $actions .= $attempt_link;
2588
                    }
2589
                    $actions .= '</div>';
2590
2591
                    if (!empty($userExtraFieldsToAdd)) {
2592
                        foreach ($userExtraFieldsToAdd as $variable) {
2593
                            $extraFieldValue = new ExtraFieldValue('user');
2594
                            $values = $extraFieldValue->get_values_by_handler_and_field_variable(
2595
                                $results[$i]['user_id'],
2596
                                $variable
2597
                            );
2598
                            if (isset($values['value'])) {
2599
                                $results[$i][$variable] = $values['value'];
2600
                            }
2601
                        }
2602
                    }
2603
2604
                    $exeId = $results[$i]['exe_id'];
2605
                    $results[$i]['id'] = $exeId;
2606
                    $category_list = [];
2607
                    if ($is_allowedToEdit) {
2608
                        $sessionName = '';
2609
                        $sessionStartAccessDate = '';
2610
                        if (!empty($results[$i]['session_id'])) {
2611
                            $sessionInfo = api_get_session_info($results[$i]['session_id']);
2612
                            if (!empty($sessionInfo)) {
2613
                                $sessionName = $sessionInfo['title'];
2614
                                $sessionStartAccessDate = api_get_local_time($sessionInfo['access_start_date']);
2615
                            }
2616
                        }
2617
2618
                        $objExercise = new Exercise($courseId);
2619
                        if ($showExerciseCategories) {
2620
                            // Getting attempt info
2621
                            $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
2622
                            if (!empty($exercise_stat_info['data_tracking'])) {
2623
                                $question_list = explode(',', $exercise_stat_info['data_tracking']);
2624
                                if (!empty($question_list)) {
2625
                                    foreach ($question_list as $questionId) {
2626
                                        $objQuestionTmp = Question::read($questionId, $objExercise->course);
2627
                                        // We're inside *one* question. Go through each possible answer for this question
2628
                                        $result = $objExercise->manage_answer(
2629
                                            $exeId,
2630
                                            $questionId,
2631
                                            null,
2632
                                            'exercise_result',
2633
                                            false,
2634
                                            false,
2635
                                            true,
2636
                                            false,
2637
                                            $objExercise->selectPropagateNeg(),
2638
                                            null,
2639
                                            true
2640
                                        );
2641
2642
                                        $my_total_score = $result['score'];
2643
                                        $my_total_weight = $result['weight'];
2644
2645
                                        // Category report
2646
                                        $category_was_added_for_this_test = false;
2647
                                        if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
2648
                                            if (!isset($category_list[$objQuestionTmp->category]['score'])) {
2649
                                                $category_list[$objQuestionTmp->category]['score'] = 0;
2650
                                            }
2651
                                            if (!isset($category_list[$objQuestionTmp->category]['total'])) {
2652
                                                $category_list[$objQuestionTmp->category]['total'] = 0;
2653
                                            }
2654
                                            $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
2655
                                            $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
2656
                                            $category_was_added_for_this_test = true;
2657
                                        }
2658
2659
                                        if (isset($objQuestionTmp->category_list) &&
2660
                                            !empty($objQuestionTmp->category_list)
2661
                                        ) {
2662
                                            foreach ($objQuestionTmp->category_list as $category_id) {
2663
                                                $category_list[$category_id]['score'] += $my_total_score;
2664
                                                $category_list[$category_id]['total'] += $my_total_weight;
2665
                                                $category_was_added_for_this_test = true;
2666
                                            }
2667
                                        }
2668
2669
                                        // No category for this question!
2670
                                        if (false == $category_was_added_for_this_test) {
2671
                                            if (!isset($category_list['none']['score'])) {
2672
                                                $category_list['none']['score'] = 0;
2673
                                            }
2674
                                            if (!isset($category_list['none']['total'])) {
2675
                                                $category_list['none']['total'] = 0;
2676
                                            }
2677
2678
                                            $category_list['none']['score'] += $my_total_score;
2679
                                            $category_list['none']['total'] += $my_total_weight;
2680
                                        }
2681
                                    }
2682
                                }
2683
                            }
2684
                        }
2685
2686
                        foreach ($category_list as $categoryId => $result) {
2687
                            $scoreToDisplay = self::show_score(
2688
                                $result['score'],
2689
                                $result['total'],
2690
                                true,
2691
                                true,
2692
                                false,
2693
                                false,
2694
                                $decimalSeparator,
2695
                                $thousandSeparator,
2696
                                $roundValues
2697
                            );
2698
                            $results[$i]['category_'.$categoryId] = $scoreToDisplay;
2699
                            $results[$i]['category_'.$categoryId.'_score_percentage'] = self::show_score(
2700
                                $result['score'],
2701
                                $result['total'],
2702
                                true,
2703
                                true,
2704
                                true, // $show_only_percentage = false
2705
                                true, // hide % sign
2706
                                $decimalSeparator,
2707
                                $thousandSeparator,
2708
                                $roundValues
2709
                            );
2710
                            $results[$i]['category_'.$categoryId.'_only_score'] = $result['score'];
2711
                            $results[$i]['category_'.$categoryId.'_total'] = $result['total'];
2712
                        }
2713
                        $results[$i]['session'] = $sessionName;
2714
                        $results[$i]['session_access_start_date'] = $sessionStartAccessDate;
2715
                        $results[$i]['status'] = $revisedLabel;
2716
                        $results[$i]['score'] = $score;
2717
                        $results[$i]['score_percentage'] = self::show_score(
2718
                            $my_res,
2719
                            $my_total,
2720
                            true,
2721
                            true,
2722
                            true,
2723
                            true,
2724
                            $decimalSeparator,
2725
                            $thousandSeparator,
2726
                            $roundValues
2727
                        );
2728
2729
                        if ($roundValues) {
2730
                            $whole = floor($my_res); // 1
2731
                            $fraction = $my_res - $whole; // .25
2732
                            if ($fraction >= 0.5) {
2733
                                $onlyScore = ceil($my_res);
2734
                            } else {
2735
                                $onlyScore = round($my_res);
2736
                            }
2737
                        } else {
2738
                            $onlyScore = $scoreDisplay->format_score(
2739
                                $my_res,
2740
                                false,
2741
                                $decimalSeparator,
2742
                                $thousandSeparator
2743
                            );
2744
                        }
2745
2746
                        $results[$i]['only_score'] = $onlyScore;
2747
2748
                        if ($roundValues) {
2749
                            $whole = floor($my_total); // 1
2750
                            $fraction = $my_total - $whole; // .25
2751
                            if ($fraction >= 0.5) {
2752
                                $onlyTotal = ceil($my_total);
2753
                            } else {
2754
                                $onlyTotal = round($my_total);
2755
                            }
2756
                        } else {
2757
                            $onlyTotal = $scoreDisplay->format_score(
2758
                                $my_total,
2759
                                false,
2760
                                $decimalSeparator,
2761
                                $thousandSeparator
2762
                            );
2763
                        }
2764
                        $results[$i]['total'] = $onlyTotal;
2765
                        $results[$i]['lp'] = $lp_name;
2766
                        $results[$i]['actions'] = $actions;
2767
                        $listInfo[] = $results[$i];
2768
                    } else {
2769
                        $results[$i]['status'] = $revisedLabel;
2770
                        $results[$i]['score'] = $score;
2771
                        $results[$i]['actions'] = $actions;
2772
                        $listInfo[] = $results[$i];
2773
                    }
2774
                }
2775
            }
2776
        }
2777
2778
        return $listInfo;
2779
    }
2780
2781
    /**
2782
     * Returns email content for a specific attempt.
2783
     */
2784
    public static function getEmailContentForAttempt(int $attemptId): array
2785
    {
2786
        $trackExerciseInfo = self::get_exercise_track_exercise_info($attemptId);
2787
2788
        if (empty($trackExerciseInfo)) {
2789
            return [
2790
                'to' => '',
2791
                'subject' => 'No exercise info found',
2792
                'message' => 'Attempt ID not found or invalid.',
2793
            ];
2794
        }
2795
2796
        $studentId = $trackExerciseInfo['exe_user_id'];
2797
        $courseInfo = api_get_course_info();
2798
        $teacherId = api_get_user_id();
2799
2800
        if (
2801
            empty($trackExerciseInfo['orig_lp_id']) ||
2802
            empty($trackExerciseInfo['orig_lp_item_id'])
2803
        ) {
2804
            $url = api_get_path(WEB_CODE_PATH).'exercise/result.php?id='.$trackExerciseInfo['exe_id'].'&'.api_get_cidreq()
2805
                .'&show_headers=1&id_session='.api_get_session_id();
2806
        } else {
2807
            $url = api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?action=view&item_id='
2808
                .$trackExerciseInfo['orig_lp_item_id'].'&lp_id='.$trackExerciseInfo['orig_lp_id'].'&'.api_get_cidreq()
2809
                .'&id_session='.api_get_session_id();
2810
        }
2811
2812
        $message = self::getEmailNotification(
2813
            $teacherId,
2814
            $courseInfo,
2815
            $trackExerciseInfo['title'],
2816
            $url
2817
        );
2818
2819
        return [
2820
            'to' => $studentId,
2821
            'subject' => get_lang('Corrected test result'),
2822
            'message' => $message,
2823
        ];
2824
    }
2825
2826
    /**
2827
     * Sends the exercise result email to the student.
2828
     */
2829
    public static function sendExerciseResultByEmail(int $attemptId): void
2830
    {
2831
        $content = self::getEmailContentForAttempt($attemptId);
2832
2833
        if (empty($content['to'])) {
2834
            return;
2835
        }
2836
2837
        MessageManager::send_message_simple(
2838
            $content['to'],
2839
            $content['subject'],
2840
            $content['message'],
2841
            api_get_user_id()
2842
        );
2843
    }
2844
2845
    /**
2846
     * Returns all reviewed attempts for a given exercise and session.
2847
     */
2848
    public static function getReviewedAttemptsInfo(int $exerciseId, int $sessionId): array
2849
    {
2850
        $courseId = api_get_course_int_id();
2851
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2852
        $qualifyTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY);
2853
2854
        $sessionCondition = api_get_session_condition($sessionId, true, false, 't.session_id');
2855
2856
        $sql = "
2857
            SELECT DISTINCT t.exe_id
2858
            FROM $trackTable t
2859
            INNER JOIN $qualifyTable q ON (t.exe_id = q.exe_id AND q.author > 0)
2860
            WHERE
2861
                t.c_id = $courseId AND
2862
                t.exe_exo_id = $exerciseId
2863
                $sessionCondition
2864
        ";
2865
2866
        return Database::store_result(Database::query($sql));
2867
    }
2868
2869
    /**
2870
     * @param $score
2871
     * @param $weight
2872
     *
2873
     * @return array
2874
     */
2875
    public static function convertScoreToPlatformSetting($score, $weight)
2876
    {
2877
        $maxNote = api_get_setting('exercise_max_score');
2878
        $minNote = api_get_setting('exercise_min_score');
2879
2880
        if ('' != $maxNote && '' != $minNote) {
2881
            if (!empty($weight) && (float) $weight !== (float) 0) {
2882
                $score = $minNote + ($maxNote - $minNote) * $score / $weight;
2883
            } else {
2884
                $score = $minNote;
2885
            }
2886
            $weight = $maxNote;
2887
        }
2888
2889
        return ['score' => $score, 'weight' => $weight];
2890
    }
2891
2892
    /**
2893
     * Converts the score with the exercise_max_note and exercise_min_score
2894
     * the platform settings + formats the results using the float_format function.
2895
     *
2896
     * @param float  $score
2897
     * @param float  $weight
2898
     * @param bool   $show_percentage       show percentage or not
2899
     * @param bool   $use_platform_settings use or not the platform settings
2900
     * @param bool   $show_only_percentage
2901
     * @param bool   $hidePercentageSign    hide "%" sign
2902
     * @param string $decimalSeparator
2903
     * @param string $thousandSeparator
2904
     * @param bool   $roundValues           This option rounds the float values into a int using ceil()
2905
     * @param bool   $removeEmptyDecimals
2906
     *
2907
     * @return string an html with the score modified
2908
     */
2909
    public static function show_score(
2910
        $score,
2911
        $weight,
2912
        $show_percentage = true,
2913
        $use_platform_settings = true,
2914
        $show_only_percentage = false,
2915
        $hidePercentageSign = false,
2916
        $decimalSeparator = '.',
2917
        $thousandSeparator = ',',
2918
        $roundValues = false,
2919
        $removeEmptyDecimals = false
2920
    ) {
2921
        if (is_null($score) && is_null($weight)) {
2922
            return '-';
2923
        }
2924
2925
        $decimalSeparator = empty($decimalSeparator) ? '.' : $decimalSeparator;
2926
        $thousandSeparator = empty($thousandSeparator) ? ',' : $thousandSeparator;
2927
2928
        if ($use_platform_settings) {
2929
            $result = self::convertScoreToPlatformSetting($score, $weight);
2930
            $score = $result['score'];
2931
            $weight = $result['weight'];
2932
        }
2933
2934
        $percentage = (100 * $score) / (0 != $weight ? $weight : 1);
2935
        // Formats values
2936
        $percentage = float_format($percentage, 1);
2937
        $score = float_format($score, 1);
2938
        $weight = float_format($weight, 1);
2939
2940
        if ($roundValues) {
2941
            $whole = floor($percentage); // 1
2942
            $fraction = $percentage - $whole; // .25
2943
2944
            // Formats values
2945
            if ($fraction >= 0.5) {
2946
                $percentage = ceil($percentage);
2947
            } else {
2948
                $percentage = round($percentage);
2949
            }
2950
2951
            $whole = floor($score); // 1
2952
            $fraction = $score - $whole; // .25
2953
            if ($fraction >= 0.5) {
2954
                $score = ceil($score);
2955
            } else {
2956
                $score = round($score);
2957
            }
2958
2959
            $whole = floor($weight); // 1
2960
            $fraction = $weight - $whole; // .25
2961
            if ($fraction >= 0.5) {
2962
                $weight = ceil($weight);
2963
            } else {
2964
                $weight = round($weight);
2965
            }
2966
        } else {
2967
            // Formats values
2968
            $percentage = float_format($percentage, 1, $decimalSeparator, $thousandSeparator);
2969
            $score = float_format($score, 1, $decimalSeparator, $thousandSeparator);
2970
            $weight = float_format($weight, 1, $decimalSeparator, $thousandSeparator);
2971
        }
2972
2973
        if ($show_percentage) {
2974
            $percentageSign = ' %';
2975
            if ($hidePercentageSign) {
2976
                $percentageSign = '';
2977
            }
2978
            $html = $percentage."$percentageSign ($score / $weight)";
2979
            if ($show_only_percentage) {
2980
                $html = $percentage.$percentageSign;
2981
            }
2982
        } else {
2983
            if ($removeEmptyDecimals) {
2984
                if (ScoreDisplay::hasEmptyDecimals($weight)) {
2985
                    $weight = round($weight);
2986
                }
2987
            }
2988
            $html = $score.' / '.$weight;
2989
        }
2990
2991
        // Over write score
2992
        $scoreBasedInModel = self::convertScoreToModel($percentage);
2993
        if (!empty($scoreBasedInModel)) {
2994
            $html = $scoreBasedInModel;
2995
        }
2996
2997
        // Ignore other formats and use the configuration['exercise_score_format'] value
2998
        // But also keep the round values settings.
2999
        $format = (int) api_get_setting('exercise.exercise_score_format');
3000
        if (!empty($format)) {
3001
            $html = ScoreDisplay::instance()->display_score([$score, $weight], $format);
3002
        }
3003
3004
        return Display::span($html, ['class' => 'score_exercise']);
3005
    }
3006
3007
    /**
3008
     * @param array $model
3009
     * @param float $percentage
3010
     *
3011
     * @return string
3012
     */
3013
    public static function getModelStyle($model, $percentage)
3014
    {
3015
        return '<span class="'.$model['css_class'].' p-2">'.$model['name'].'</span>';
3016
    }
3017
3018
    /**
3019
     * @param float $percentage value between 0 and 100
3020
     *
3021
     * @return string
3022
     */
3023
    public static function convertScoreToModel($percentage)
3024
    {
3025
        $model = self::getCourseScoreModel();
3026
        if (!empty($model)) {
3027
            $scoreWithGrade = [];
3028
            foreach ($model['score_list'] as $item) {
3029
                if ($percentage >= $item['min'] && $percentage <= $item['max']) {
3030
                    $scoreWithGrade = $item;
3031
                    break;
3032
                }
3033
            }
3034
3035
            if (!empty($scoreWithGrade)) {
3036
                return self::getModelStyle($scoreWithGrade, $percentage);
3037
            }
3038
        }
3039
3040
        return '';
3041
    }
3042
3043
    /**
3044
     * @return array
3045
     */
3046
    public static function getCourseScoreModel()
3047
    {
3048
        $modelList = self::getScoreModels();
3049
3050
        if (empty($modelList)) {
3051
            return [];
3052
        }
3053
3054
        $courseInfo = api_get_course_info();
3055
        if (!empty($courseInfo)) {
3056
            $scoreModelId = api_get_course_setting('score_model_id');
3057
            if (-1 != $scoreModelId) {
3058
                $modelIdList = array_column($modelList['models'], 'id');
3059
                if (in_array($scoreModelId, $modelIdList)) {
3060
                    foreach ($modelList['models'] as $item) {
3061
                        if ($item['id'] == $scoreModelId) {
3062
                            return $item;
3063
                        }
3064
                    }
3065
                }
3066
            }
3067
        }
3068
3069
        return [];
3070
    }
3071
3072
    /**
3073
     * @return array
3074
     */
3075
    public static function getScoreModels()
3076
    {
3077
        return api_get_setting('exercise.score_grade_model', true);
3078
    }
3079
3080
    /**
3081
     * @param float  $score
3082
     * @param float  $weight
3083
     * @param string $passPercentage
3084
     *
3085
     * @return bool
3086
     */
3087
    public static function isSuccessExerciseResult($score, $weight, $passPercentage)
3088
    {
3089
        $percentage = float_format(
3090
            ($score / (0 != $weight ? $weight : 1)) * 100,
3091
            1
3092
        );
3093
        if (isset($passPercentage) && !empty($passPercentage)) {
3094
            if ($percentage >= $passPercentage) {
3095
                return true;
3096
            }
3097
        }
3098
3099
        return false;
3100
    }
3101
3102
    /**
3103
     * @param string $name
3104
     * @param $weight
3105
     * @param $selected
3106
     *
3107
     * @return bool
3108
     */
3109
    public static function addScoreModelInput(
3110
        FormValidator $form,
3111
        $name,
3112
        $weight,
3113
        $selected
3114
    ) {
3115
        $model = self::getCourseScoreModel();
3116
        if (empty($model)) {
3117
            return false;
3118
        }
3119
3120
        /** @var HTML_QuickForm_select $element */
3121
        $element = $form->createElement(
3122
            'select',
3123
            $name,
3124
            get_lang('Score'),
3125
            [],
3126
            ['class' => 'exercise_mark_select']
3127
        );
3128
3129
        foreach ($model['score_list'] as $item) {
3130
            $i = api_number_format($item['score_to_qualify'] / 100 * $weight, 2);
3131
            $label = self::getModelStyle($item, $i);
3132
            $attributes = [
3133
                'class' => $item['css_class'],
3134
            ];
3135
            if ($selected == $i) {
3136
                $attributes['selected'] = 'selected';
3137
            }
3138
            $element->addOption($label, $i, $attributes);
3139
        }
3140
        $form->addElement($element);
3141
    }
3142
3143
    /**
3144
     * @return string
3145
     */
3146
    public static function getJsCode()
3147
    {
3148
        // Filling the scores with the right colors.
3149
        $models = self::getCourseScoreModel();
3150
        $cssListToString = '';
3151
        if (!empty($models)) {
3152
            $cssList = array_column($models['score_list'], 'css_class');
3153
            $cssListToString = implode(' ', $cssList);
3154
        }
3155
3156
        if (empty($cssListToString)) {
3157
            return '';
3158
        }
3159
        $js = <<<EOT
3160
3161
        function updateSelect(element) {
3162
            var spanTag = element.parent().find('span.filter-option');
3163
            var value = element.val();
3164
            var selectId = element.attr('id');
3165
            var optionClass = $('#' + selectId + ' option[value="'+value+'"]').attr('class');
3166
            spanTag.removeClass('$cssListToString');
3167
            spanTag.addClass(optionClass);
3168
        }
3169
3170
        $(function() {
3171
            // Loading values
3172
            $('.exercise_mark_select').on('loaded.bs.select', function() {
3173
                updateSelect($(this));
3174
            });
3175
            // On change
3176
            $('.exercise_mark_select').on('changed.bs.select', function() {
3177
                updateSelect($(this));
3178
            });
3179
        });
3180
EOT;
3181
3182
        return $js;
3183
    }
3184
3185
    /**
3186
     * @param float  $score
3187
     * @param float  $weight
3188
     * @param string $pass_percentage
3189
     *
3190
     * @return string
3191
     */
3192
    public static function showSuccessMessage($score, $weight, $pass_percentage)
3193
    {
3194
        $res = '';
3195
        if (self::isPassPercentageEnabled($pass_percentage)) {
3196
            $isSuccess = self::isSuccessExerciseResult(
3197
                $score,
3198
                $weight,
3199
                $pass_percentage
3200
            );
3201
3202
            if ($isSuccess) {
3203
                $html = get_lang('Congratulations you passed the test!');
3204
                $icon = Display::getMdiIcon('check-circle', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Correct'));
3205
            } else {
3206
                $html = get_lang('You didn\'t reach the minimum score');
3207
                $icon = Display::getMdiIcon('alert', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Wrong'));
3208
            }
3209
            $html = Display::tag('h4', $html);
3210
            $html .= Display::tag(
3211
                'h5',
3212
                $icon,
3213
                ['style' => 'width:40px; padding:2px 10px 0px 0px']
3214
            );
3215
            $res = $html;
3216
        }
3217
3218
        return $res;
3219
    }
3220
3221
    /**
3222
     * Return true if pass_pourcentage activated (we use the pass pourcentage feature
3223
     * return false if pass_percentage = 0 (we don't use the pass pourcentage feature.
3224
     *
3225
     * @param $value
3226
     *
3227
     * @return bool
3228
     *              In this version, pass_percentage and show_success_message are disabled if
3229
     *              pass_percentage is set to 0
3230
     */
3231
    public static function isPassPercentageEnabled($value)
3232
    {
3233
        return $value > 0;
3234
    }
3235
3236
    /**
3237
     * Converts a numeric value in a percentage example 0.66666 to 66.67 %.
3238
     *
3239
     * @param $value
3240
     *
3241
     * @return float Converted number
3242
     */
3243
    public static function convert_to_percentage($value)
3244
    {
3245
        $return = '-';
3246
        if ('' != $value) {
3247
            $return = float_format($value * 100, 1).' %';
3248
        }
3249
3250
        return $return;
3251
    }
3252
3253
    /**
3254
     * Getting all active exercises from a course from a session
3255
     * (if a session_id is provided we will show all the exercises in the course +
3256
     * all exercises in the session).
3257
     *
3258
     * @param array  $course_info
3259
     * @param int    $session_id
3260
     * @param bool   $check_publication_dates
3261
     * @param string $search                  Search exercise name
3262
     * @param bool   $search_all_sessions     Search exercises in all sessions
3263
     * @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...
3264
     *                  1 = only active exercises,
3265
     *                  2 = all exercises
3266
     *                  3 = active <> -1
3267
     *
3268
     * @return CQuiz[]
3269
     */
3270
    public static function get_all_exercises(
3271
        $course_info = null,
3272
        $session_id = 0,
3273
        $check_publication_dates = false,
3274
        $search = '',
3275
        $search_all_sessions = false,
3276
        $active = 2
3277
    ) {
3278
        $course_id = api_get_course_int_id();
3279
        if (!empty($course_info) && !empty($course_info['real_id'])) {
3280
            $course_id = $course_info['real_id'];
3281
        }
3282
3283
        if (-1 == $session_id) {
3284
            $session_id = 0;
3285
        }
3286
        $course = api_get_course_entity($course_id);
3287
        $session = api_get_session_entity($session_id);
3288
3289
        if (null === $course) {
3290
            return [];
3291
        }
3292
3293
        $repo = Container::getQuizRepository();
3294
3295
        return $repo->findAllByCourse($course, $session, (string) $search, $active)
3296
            ->getQuery()
3297
            ->getResult();
3298
    }
3299
3300
    /**
3301
     * Getting all exercises (active only or all)
3302
     * from a course from a session
3303
     * (if a session_id is provided we will show all the exercises in the
3304
     * course + all exercises in the session).
3305
     */
3306
    public static function get_all_exercises_for_course_id(
3307
        int $courseId,
3308
        int $sessionId = 0,
3309
        bool $onlyActiveExercises = true
3310
    ): array {
3311
3312
        if (!($courseId > 0)) {
3313
            return [];
3314
        }
3315
3316
        $course = api_get_course_entity($courseId);
3317
        $session = api_get_session_entity($sessionId);
3318
3319
        $repo = Container::getQuizRepository();
3320
3321
        $qb = $repo->getResourcesByCourse($course, $session);
3322
3323
        if ($onlyActiveExercises) {
3324
            $qb->andWhere('resource.active = 1');
3325
        } else {
3326
            $qb->andWhere('resource.active IN (1, 0)');
3327
        }
3328
3329
        $qb->orderBy('resource.title', 'ASC');
3330
3331
        $exercises = $qb->getQuery()->getResult();
3332
3333
        $exerciseList = [];
3334
        foreach ($exercises as $exercise) {
3335
            $exerciseList[] = [
3336
                'iid' => $exercise->getIid(),
3337
                'title' => $exercise->getTitle(),
3338
            ];
3339
        }
3340
3341
        return $exerciseList;
3342
    }
3343
3344
    /**
3345
     * Gets the position of the score based in a given score (result/weight)
3346
     * and the exe_id based in the user list
3347
     * (NO Exercises in LPs ).
3348
     *
3349
     * @param float  $my_score      user score to be compared *attention*
3350
     *                              $my_score = score/weight and not just the score
3351
     * @param int    $my_exe_id     exe id of the exercise
3352
     *                              (this is necessary because if 2 students have the same score the one
3353
     *                              with the minor exe_id will have a best position, just to be fair and FIFO)
3354
     * @param int    $exercise_id
3355
     * @param string $course_code
3356
     * @param int    $session_id
3357
     * @param array  $user_list
3358
     * @param bool   $return_string
3359
     *
3360
     * @return int the position of the user between his friends in a course
3361
     *             (or course within a session)
3362
     */
3363
    public static function get_exercise_result_ranking(
3364
        $my_score,
3365
        $my_exe_id,
3366
        $exercise_id,
3367
        $course_code,
3368
        $session_id = 0,
3369
        $user_list = [],
3370
        $return_string = true
3371
    ) {
3372
        //No score given we return
3373
        if (is_null($my_score)) {
3374
            return '-';
3375
        }
3376
        if (empty($user_list)) {
3377
            return '-';
3378
        }
3379
3380
        $best_attempts = [];
3381
        foreach ($user_list as $user_data) {
3382
            $user_id = $user_data['user_id'];
3383
            $best_attempts[$user_id] = self::get_best_attempt_by_user(
3384
                $user_id,
3385
                $exercise_id,
3386
                $course_code,
3387
                $session_id
3388
            );
3389
        }
3390
3391
        if (empty($best_attempts)) {
3392
            return 1;
3393
        } else {
3394
            $position = 1;
3395
            $my_ranking = [];
3396
            foreach ($best_attempts as $user_id => $result) {
3397
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3398
                    $my_ranking[$user_id] = $result['score'] / $result['max_score'];
3399
                } else {
3400
                    $my_ranking[$user_id] = 0;
3401
                }
3402
            }
3403
            //if (!empty($my_ranking)) {
3404
            asort($my_ranking);
3405
            $position = count($my_ranking);
3406
            if (!empty($my_ranking)) {
3407
                foreach ($my_ranking as $user_id => $ranking) {
3408
                    if ($my_score >= $ranking) {
3409
                        if ($my_score == $ranking && isset($best_attempts[$user_id]['exe_id'])) {
3410
                            $exe_id = $best_attempts[$user_id]['exe_id'];
3411
                            if ($my_exe_id < $exe_id) {
3412
                                $position--;
3413
                            }
3414
                        } else {
3415
                            $position--;
3416
                        }
3417
                    }
3418
                }
3419
            }
3420
            //}
3421
            $return_value = [
3422
                'position' => $position,
3423
                'count' => count($my_ranking),
3424
            ];
3425
3426
            if ($return_string) {
3427
                if (!empty($position) && !empty($my_ranking)) {
3428
                    $return_value = $position.'/'.count($my_ranking);
3429
                } else {
3430
                    $return_value = '-';
3431
                }
3432
            }
3433
3434
            return $return_value;
3435
        }
3436
    }
3437
3438
    /**
3439
     * Gets the position of the score based in a given score (result/weight) and the exe_id based in all attempts
3440
     * (NO Exercises in LPs ) old functionality by attempt.
3441
     *
3442
     * @param   float   user score to be compared attention => score/weight
3443
     * @param   int     exe id of the exercise
3444
     * (this is necessary because if 2 students have the same score the one
3445
     * with the minor exe_id will have a best position, just to be fair and FIFO)
3446
     * @param   int     exercise id
3447
     * @param   string  course code
3448
     * @param   int     session id
3449
     * @param bool $return_string
3450
     *
3451
     * @return int the position of the user between his friends in a course (or course within a session)
3452
     */
3453
    public static function get_exercise_result_ranking_by_attempt(
3454
        $my_score,
3455
        $my_exe_id,
3456
        $exercise_id,
3457
        $courseId,
3458
        $session_id = 0,
3459
        $return_string = true
3460
    ) {
3461
        if (empty($session_id)) {
3462
            $session_id = 0;
3463
        }
3464
        if (is_null($my_score)) {
3465
            return '-';
3466
        }
3467
        $user_results = Event::get_all_exercise_results(
3468
            $exercise_id,
3469
            $courseId,
3470
            $session_id,
3471
            false
3472
        );
3473
        $position_data = [];
3474
        if (empty($user_results)) {
3475
            return 1;
3476
        } else {
3477
            $position = 1;
3478
            $my_ranking = [];
3479
            foreach ($user_results as $result) {
3480
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3481
                    $my_ranking[$result['exe_id']] = $result['score'] / $result['max_score'];
3482
                } else {
3483
                    $my_ranking[$result['exe_id']] = 0;
3484
                }
3485
            }
3486
            asort($my_ranking);
3487
            $position = count($my_ranking);
3488
            if (!empty($my_ranking)) {
3489
                foreach ($my_ranking as $exe_id => $ranking) {
3490
                    if ($my_score >= $ranking) {
3491
                        if ($my_score == $ranking) {
3492
                            if ($my_exe_id < $exe_id) {
3493
                                $position--;
3494
                            }
3495
                        } else {
3496
                            $position--;
3497
                        }
3498
                    }
3499
                }
3500
            }
3501
            $return_value = [
3502
                'position' => $position,
3503
                'count' => count($my_ranking),
3504
            ];
3505
3506
            if ($return_string) {
3507
                if (!empty($position) && !empty($my_ranking)) {
3508
                    return $position.'/'.count($my_ranking);
3509
                }
3510
            }
3511
3512
            return $return_value;
3513
        }
3514
    }
3515
3516
    /**
3517
     * Get the best attempt in a exercise (NO Exercises in LPs ).
3518
     *
3519
     * @param int $exercise_id
3520
     * @param int $courseId
3521
     * @param int $session_id
3522
     *
3523
     * @return array
3524
     */
3525
    public static function get_best_attempt_in_course($exercise_id, $courseId, $session_id)
3526
    {
3527
        $user_results = Event::get_all_exercise_results(
3528
            $exercise_id,
3529
            $courseId,
3530
            $session_id,
3531
            false
3532
        );
3533
3534
        $best_score_data = [];
3535
        $best_score = 0;
3536
        if (!empty($user_results)) {
3537
            foreach ($user_results as $result) {
3538
                if (!empty($result['max_score']) &&
3539
                    0 != intval($result['max_score'])
3540
                ) {
3541
                    $score = $result['score'] / $result['max_score'];
3542
                    if ($score >= $best_score) {
3543
                        $best_score = $score;
3544
                        $best_score_data = $result;
3545
                    }
3546
                }
3547
            }
3548
        }
3549
3550
        return $best_score_data;
3551
    }
3552
3553
    /**
3554
     * Get the best score in a exercise (NO Exercises in LPs ).
3555
     *
3556
     * @param int $user_id
3557
     * @param int $exercise_id
3558
     * @param int $courseId
3559
     * @param int $session_id
3560
     *
3561
     * @return array
3562
     */
3563
    public static function get_best_attempt_by_user(
3564
        $user_id,
3565
        $exercise_id,
3566
        $courseId,
3567
        $session_id
3568
    ) {
3569
        $user_results = Event::get_all_exercise_results(
3570
            $exercise_id,
3571
            $courseId,
3572
            $session_id,
3573
            false,
3574
            $user_id
3575
        );
3576
        $best_score_data = [];
3577
        $best_score = 0;
3578
        if (!empty($user_results)) {
3579
            foreach ($user_results as $result) {
3580
                if (!empty($result['max_score']) && 0 != (float) $result['max_score']) {
3581
                    $score = $result['score'] / $result['max_score'];
3582
                    if ($score >= $best_score) {
3583
                        $best_score = $score;
3584
                        $best_score_data = $result;
3585
                    }
3586
                }
3587
            }
3588
        }
3589
3590
        return $best_score_data;
3591
    }
3592
3593
    /**
3594
     * Get average score (NO Exercises in LPs ).
3595
     *
3596
     * @param    int    exercise id
3597
     * @param int $courseId
3598
     * @param    int    session id
3599
     *
3600
     * @return float Average score
3601
     */
3602
    public static function get_average_score($exercise_id, $courseId, $session_id)
3603
    {
3604
        $user_results = Event::get_all_exercise_results(
3605
            $exercise_id,
3606
            $courseId,
3607
            $session_id
3608
        );
3609
        $avg_score = 0;
3610
        if (!empty($user_results)) {
3611
            foreach ($user_results as $result) {
3612
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3613
                    $score = $result['score'] / $result['max_score'];
3614
                    $avg_score += $score;
3615
                }
3616
            }
3617
            $avg_score = float_format($avg_score / count($user_results), 1);
3618
        }
3619
3620
        return $avg_score;
3621
    }
3622
3623
    /**
3624
     * Get average score by score (NO Exercises in LPs ).
3625
     *
3626
     * @param int $courseId
3627
     * @param    int    session id
3628
     *
3629
     * @return float Average score
3630
     */
3631
    public static function get_average_score_by_course($courseId, $session_id)
3632
    {
3633
        $user_results = Event::get_all_exercise_results_by_course(
3634
            $courseId,
3635
            $session_id,
3636
            false
3637
        );
3638
        $avg_score = 0;
3639
        if (!empty($user_results)) {
3640
            foreach ($user_results as $result) {
3641
                if (!empty($result['max_score']) && 0 != intval(
3642
                        $result['max_score']
3643
                    )
3644
                ) {
3645
                    $score = $result['score'] / $result['max_score'];
3646
                    $avg_score += $score;
3647
                }
3648
            }
3649
            // We assume that all max_score
3650
            $avg_score = $avg_score / count($user_results);
3651
        }
3652
3653
        return $avg_score;
3654
    }
3655
3656
    /**
3657
     * @param int $user_id
3658
     * @param int $courseId
3659
     * @param int $session_id
3660
     *
3661
     * @return float|int
3662
     */
3663
    public static function get_average_score_by_course_by_user(
3664
        $user_id,
3665
        $courseId,
3666
        $session_id
3667
    ) {
3668
        $user_results = Event::get_all_exercise_results_by_user(
3669
            $user_id,
3670
            $courseId,
3671
            $session_id
3672
        );
3673
        $avg_score = 0;
3674
        if (!empty($user_results)) {
3675
            foreach ($user_results as $result) {
3676
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3677
                    $score = $result['score'] / $result['max_score'];
3678
                    $avg_score += $score;
3679
                }
3680
            }
3681
            // We assume that all max_score
3682
            $avg_score = ($avg_score / count($user_results));
3683
        }
3684
3685
        return $avg_score;
3686
    }
3687
3688
    /**
3689
     * Get average score by score (NO Exercises in LPs ).
3690
     *
3691
     * @param int $exercise_id
3692
     * @param int $courseId
3693
     * @param int $session_id
3694
     * @param int $user_count
3695
     *
3696
     * @return float Best average score
3697
     */
3698
    public static function get_best_average_score_by_exercise(
3699
        $exercise_id,
3700
        $courseId,
3701
        $session_id,
3702
        $user_count
3703
    ) {
3704
        $user_results = Event::get_best_exercise_results_by_user(
3705
            $exercise_id,
3706
            $courseId,
3707
            $session_id
3708
        );
3709
        $avg_score = 0;
3710
        if (!empty($user_results)) {
3711
            foreach ($user_results as $result) {
3712
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3713
                    $score = $result['score'] / $result['max_score'];
3714
                    $avg_score += $score;
3715
                }
3716
            }
3717
            // We asumme that all max_score
3718
            if (!empty($user_count)) {
3719
                $avg_score = float_format($avg_score / $user_count, 1) * 100;
3720
            } else {
3721
                $avg_score = 0;
3722
            }
3723
        }
3724
3725
        return $avg_score;
3726
    }
3727
3728
    /**
3729
     * Get average score by score (NO Exercises in LPs ).
3730
     *
3731
     * @param int $exercise_id
3732
     * @param int $courseId
3733
     * @param int $session_id
3734
     *
3735
     * @return float Best average score
3736
     */
3737
    public static function getBestScoreByExercise(
3738
        $exercise_id,
3739
        $courseId,
3740
        $session_id
3741
    ) {
3742
        $user_results = Event::get_best_exercise_results_by_user(
3743
            $exercise_id,
3744
            $courseId,
3745
            $session_id
3746
        );
3747
        $avg_score = 0;
3748
        if (!empty($user_results)) {
3749
            foreach ($user_results as $result) {
3750
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3751
                    $score = $result['score'] / $result['max_score'];
3752
                    $avg_score += $score;
3753
                }
3754
            }
3755
        }
3756
3757
        return $avg_score;
3758
    }
3759
3760
    /**
3761
     * Get student results (only in completed exercises) stats by question.
3762
     *
3763
     * @throws \Doctrine\DBAL\Exception
3764
     */
3765
    public static function getStudentStatsByQuestion(
3766
        int $questionId,
3767
        int $exerciseId,
3768
        string $courseCode,
3769
        int $sessionId,
3770
        bool $onlyStudent = false
3771
    ): array
3772
    {
3773
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3774
        $trackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3775
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3776
3777
        $questionId = (int) $questionId;
3778
        $exerciseId = (int) $exerciseId;
3779
        $courseCode = Database::escape_string($courseCode);
3780
        $sessionId = (int) $sessionId;
3781
        $courseId = api_get_course_int_id($courseCode);
3782
3783
        $sql = "SELECT MAX(marks) as max, MIN(marks) as min, AVG(marks) as average
3784
                FROM $trackExercises e ";
3785
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
3786
        if ($onlyStudent) {
3787
            $courseCondition = '';
3788
            if (empty($sessionId)) {
3789
                $courseCondition = "
3790
                INNER JOIN $courseUser c
3791
                ON (
3792
                    e.exe_user_id = c.user_id AND
3793
                    e.c_id = c.c_id AND
3794
                    c.status = ".STUDENT." AND
3795
                    relation_type <> 2
3796
                )";
3797
            } else {
3798
                $sessionRelCourse = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3799
                $courseCondition = "
3800
            INNER JOIN $sessionRelCourse sc
3801
            ON (
3802
                        e.exe_user_id = sc.user_id AND
3803
                        e.c_id = sc.c_id AND
3804
                        e.session_id = sc.session_id AND
3805
                        sc.status = ".SessionEntity::STUDENT."
3806
                )";
3807
            }
3808
            $sql .= $courseCondition;
3809
        }
3810
        $sql .= "
3811
    		INNER JOIN $trackAttempt a
3812
    		ON (
3813
    		    a.exe_id = e.exe_id
3814
            )
3815
    		WHERE
3816
    		    exe_exo_id 	= $exerciseId AND
3817
                e.c_id = $courseId AND
3818
                question_id = $questionId AND
3819
                e.status = ''
3820
                $sessionCondition
3821
            LIMIT 1";
3822
        $result = Database::query($sql);
3823
        $return = [];
3824
        if ($result) {
3825
            $return = Database::fetch_assoc($result);
3826
        }
3827
3828
        return $return;
3829
    }
3830
3831
    /**
3832
     * Get the correct answer count for a fill blanks question.
3833
     *
3834
     * @param int $question_id
3835
     * @param int $exercise_id
3836
     *
3837
     * @return array
3838
     */
3839
    public static function getNumberStudentsFillBlanksAnswerCount(
3840
        $question_id,
3841
        $exercise_id
3842
    ) {
3843
        $listStudentsId = [];
3844
        $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
3845
            api_get_course_id(),
3846
            true
3847
        );
3848
        foreach ($listAllStudentInfo as $i => $listStudentInfo) {
3849
            $listStudentsId[] = $listStudentInfo['user_id'];
3850
        }
3851
3852
        $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3853
            $exercise_id,
3854
            $question_id,
3855
            $listStudentsId,
3856
            '1970-01-01',
3857
            '3000-01-01'
3858
        );
3859
3860
        $arrayCount = [];
3861
3862
        foreach ($listFillTheBlankResult as $resultCount) {
3863
            foreach ($resultCount as $index => $count) {
3864
                //this is only for declare the array index per answer
3865
                $arrayCount[$index] = 0;
3866
            }
3867
        }
3868
3869
        foreach ($listFillTheBlankResult as $resultCount) {
3870
            foreach ($resultCount as $index => $count) {
3871
                $count = (0 === $count) ? 1 : 0;
3872
                $arrayCount[$index] += $count;
3873
            }
3874
        }
3875
3876
        return $arrayCount;
3877
    }
3878
3879
    /**
3880
     * Get the number of questions with answers.
3881
     *
3882
     * @param int    $question_id
3883
     * @param int    $exercise_id
3884
     * @param string $course_code
3885
     * @param int    $session_id
3886
     * @param string $questionType
3887
     *
3888
     * @return int
3889
     */
3890
    public static function get_number_students_question_with_answer_count(
3891
        $question_id,
3892
        $exercise_id,
3893
        $course_code,
3894
        $session_id,
3895
        $questionType = ''
3896
    ) {
3897
        $track_exercises   = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3898
        $track_attempt     = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3899
        $courseUser        = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3900
        $courseTable       = Database::get_main_table(TABLE_MAIN_COURSE);
3901
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3902
3903
        $question_id = (int) $question_id;
3904
        $exercise_id = (int) $exercise_id;
3905
        $courseId    = (int) api_get_course_int_id($course_code);
3906
        $session_id  = (int) $session_id;
3907
3908
        if (in_array($questionType, [FILL_IN_BLANKS, FILL_IN_BLANKS_COMBINATION], true)) {
3909
            $listStudentsId     = [];
3910
            $listAllStudentInfo = CourseManager::get_student_list_from_course_code(api_get_course_id(), true);
3911
            foreach ($listAllStudentInfo as $listStudentInfo) {
3912
                $listStudentsId[] = (int) $listStudentInfo['user_id'];
3913
            }
3914
3915
            $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3916
                $exercise_id,
3917
                $question_id,
3918
                $listStudentsId,
3919
                '1970-01-01',
3920
                '3000-01-01'
3921
            );
3922
3923
            return FillBlanks::getNbResultFillBlankAll($listFillTheBlankResult);
3924
        }
3925
3926
        if (empty($session_id)) {
3927
            $courseCondition = "
3928
            INNER JOIN $courseUser cu
3929
                ON cu.c_id = c.id AND cu.user_id = exe_user_id";
3930
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3931
        } else {
3932
            $courseCondition = "
3933
            INNER JOIN $courseUserSession cu
3934
                ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
3935
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
3936
        }
3937
3938
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
3939
        $sql = "SELECT DISTINCT exe_user_id
3940
            FROM $track_exercises e
3941
            INNER JOIN $track_attempt a
3942
                ON (a.exe_id = e.exe_id AND a.c_id = e.c_id)
3943
            INNER JOIN $courseTable c
3944
                ON c.id = e.c_id
3945
            $courseCondition
3946
            WHERE
3947
                exe_exo_id  = $exercise_id AND
3948
                e.c_id      = $courseId AND
3949
                question_id = $question_id AND
3950
                answer <> '0' AND
3951
                e.status = ''
3952
                $courseConditionWhere
3953
                $sessionCondition
3954
    ";
3955
3956
        $result = Database::query($sql);
3957
3958
        return $result ? (int) Database::num_rows($result) : 0;
3959
    }
3960
3961
    /**
3962
     * Get number of answers to hotspot questions.
3963
     */
3964
    public static function getNumberStudentsAnswerHotspotCount(
3965
        int    $answerId,
3966
        int    $questionId,
3967
        int    $exerciseId,
3968
        string $courseCode,
3969
        int $sessionId
3970
    ): int
3971
    {
3972
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3973
        $trackHotspot = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
3974
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3975
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
3976
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3977
3978
        $questionId = (int) $questionId;
3979
        $answerId = (int) $answerId;
3980
        $exerciseId = (int) $exerciseId;
3981
        $courseId = api_get_course_int_id($courseCode);
3982
        $sessionId = (int) $sessionId;
3983
3984
        if (empty($sessionId)) {
3985
            $courseCondition = "
3986
            INNER JOIN $courseUser cu
3987
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
3988
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3989
        } else {
3990
            $courseCondition = "
3991
            INNER JOIN $courseUserSession cu
3992
            ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
3993
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
3994
        }
3995
3996
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
3997
        $sql = "SELECT DISTINCT exe_user_id
3998
                FROM $trackExercises e
3999
                INNER JOIN $trackHotspot a
4000
                ON (a.hotspot_exe_id = e.exe_id)
4001
                INNER JOIN $courseTable c
4002
                ON (a.c_id = c.id)
4003
                $courseCondition
4004
                WHERE
4005
                    exe_exo_id              = $exerciseId AND
4006
                    a.c_id 	= $courseId AND
4007
                    hotspot_answer_id       = $answerId AND
4008
                    hotspot_question_id     = $questionId AND
4009
                    hotspot_correct         =  1 AND
4010
                    e.status                = ''
4011
                    $courseConditionWhere
4012
                    $sessionCondition
4013
            ";
4014
        $result = Database::query($sql);
4015
        $return = 0;
4016
        if ($result) {
4017
            $return = Database::num_rows($result);
4018
        }
4019
4020
        return $return;
4021
    }
4022
4023
    /**
4024
     * @param int    $answer_id
4025
     * @param int    $question_id
4026
     * @param int    $exercise_id
4027
     * @param string $course_code
4028
     * @param int    $session_id
4029
     * @param string $question_type
4030
     * @param string $correct_answer
4031
     * @param string $current_answer
4032
     *
4033
     * @return int
4034
     */
4035
    public static function get_number_students_answer_count(
4036
        $answer_id,
4037
        $question_id,
4038
        $exercise_id,
4039
        $course_code,
4040
        $session_id,
4041
        $question_type = null,
4042
        $correct_answer = null,
4043
        $current_answer = null
4044
    ) {
4045
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4046
        $track_attempt   = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4047
        $courseTable     = Database::get_main_table(TABLE_MAIN_COURSE);
4048
        $courseUser      = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4049
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4050
4051
        $question_id = (int) $question_id;
4052
        $answer_id   = (int) $answer_id;
4053
        $exercise_id = (int) $exercise_id;
4054
        $courseId    = (int) api_get_course_int_id($course_code);
4055
        $session_id  = (int) $session_id;
4056
4057
        switch ($question_type) {
4058
            case FILL_IN_BLANKS:
4059
            case FILL_IN_BLANKS_COMBINATION:
4060
                $answer_condition = '';
4061
                $select_condition = ' e.exe_id, answer ';
4062
                break;
4063
            case MATCHING:
4064
            case MATCHING_COMBINATION:
4065
            case MATCHING_DRAGGABLE:
4066
            case MATCHING_DRAGGABLE_COMBINATION:
4067
            default:
4068
                $answer_condition = " answer = $answer_id AND ";
4069
                $select_condition = ' DISTINCT exe_user_id ';
4070
        }
4071
4072
        if (empty($session_id)) {
4073
            $courseCondition = "
4074
            INNER JOIN $courseUser cu
4075
                ON cu.c_id = e.c_id AND cu.user_id = e.exe_user_id";
4076
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4077
        } else {
4078
            $courseCondition = "
4079
            INNER JOIN $courseUserSession cu
4080
                ON (cu.c_id = e.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4081
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
4082
        }
4083
4084
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
4085
        $sql = "SELECT $select_condition
4086
            FROM $track_exercises e
4087
            INNER JOIN $track_attempt a
4088
                ON (a.exe_id = e.exe_id AND a.c_id = e.c_id)
4089
            INNER JOIN $courseTable c
4090
                ON c.id = e.c_id
4091
            $courseCondition
4092
            WHERE
4093
                exe_exo_id = $exercise_id AND
4094
                e.c_id = $courseId AND
4095
                $answer_condition
4096
                question_id = $question_id AND
4097
                e.status = ''
4098
                $courseConditionWhere
4099
                $sessionCondition
4100
    ";
4101
4102
        $result = Database::query($sql);
4103
        $return = 0;
4104
        if ($result) {
4105
            switch ($question_type) {
4106
                case FILL_IN_BLANKS:
4107
                case FILL_IN_BLANKS_COMBINATION:
4108
                    $good_answers = 0;
4109
                    while ($row = Database::fetch_assoc($result)) {
4110
                        $fill_blank = self::check_fill_in_blanks(
4111
                            $correct_answer,
4112
                            $row['answer'],
4113
                            $current_answer
4114
                        );
4115
                        if (isset($fill_blank[$current_answer]) && 1 == (int) $fill_blank[$current_answer]) {
4116
                            $good_answers++;
4117
                        }
4118
                    }
4119
4120
                    return $good_answers;
4121
4122
                case MATCHING:
4123
                case MATCHING_COMBINATION:
4124
                case MATCHING_DRAGGABLE:
4125
                case MATCHING_DRAGGABLE_COMBINATION:
4126
                default:
4127
                    $return = Database::num_rows($result);
4128
            }
4129
        }
4130
4131
        return $return;
4132
    }
4133
4134
    /**
4135
     * Get the number of times an answer was selected.
4136
     */
4137
    public static function getCountOfAnswers(
4138
        int $answerId,
4139
        int $questionId,
4140
        int $exerciseId,
4141
        string $courseCode,
4142
        int $sessionId,
4143
        $questionType = null,
4144
    ): int
4145
    {
4146
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4147
        $trackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4148
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4149
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4150
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4151
4152
        $answerId = (int) $answerId;
4153
        $questionId = (int) $questionId;
4154
        $exerciseId = (int) $exerciseId;
4155
        $courseId = api_get_course_int_id($courseCode);
4156
        $sessionId = (int) $sessionId;
4157
        $return = 0;
4158
4159
        $answerCondition = match ($questionType) {
4160
            FILL_IN_BLANKS => '',
4161
            default => " answer = $answerId AND ",
4162
        };
4163
4164
        if (empty($sessionId)) {
4165
            $courseCondition = "
4166
            INNER JOIN $courseUser cu
4167
            ON cu.c_id = c.id AND cu.user_id = exe_user_id";
4168
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4169
        } else {
4170
            $courseCondition = "
4171
            INNER JOIN $courseUserSession cu
4172
            ON (cu.c_id = a.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4173
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
4174
        }
4175
4176
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
4177
        $sql = "SELECT count(a.answer) as total
4178
                FROM $trackExercises e
4179
                INNER JOIN $trackAttempt a
4180
                ON (
4181
                    a.exe_id = e.exe_id
4182
                )
4183
                INNER JOIN $courseTable c
4184
                ON c.id = e.c_id
4185
                $courseCondition
4186
                WHERE
4187
                    exe_exo_id = $exerciseId AND
4188
                    e.c_id = $courseId AND
4189
                    $answerCondition
4190
                    question_id = $questionId AND
4191
                    e.status = ''
4192
                    $courseConditionWhere
4193
                    $sessionCondition
4194
            ";
4195
        $result = Database::query($sql);
4196
        if ($result) {
4197
            $count = Database::fetch_array($result);
4198
            $return = (int) $count['total'];
4199
        }
4200
        return $return;
4201
    }
4202
4203
    /**
4204
     * @param array  $answer
4205
     * @param string $user_answer
4206
     *
4207
     * @return array
4208
     */
4209
    public static function check_fill_in_blanks($answer, $user_answer, $current_answer)
4210
    {
4211
        // the question is encoded like this
4212
        // [A] B [C] D [E] F::10,10,10@1
4213
        // number 1 before the "@" means that is a switchable fill in blank question
4214
        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4215
        // means that is a normal fill blank question
4216
        // first we explode the "::"
4217
        $pre_array = explode('::', $answer);
4218
        // is switchable fill blank or not
4219
        $last = count($pre_array) - 1;
4220
        $is_set_switchable = explode('@', $pre_array[$last]);
4221
        $switchable_answer_set = false;
4222
        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
4223
            $switchable_answer_set = true;
4224
        }
4225
        $answer = '';
4226
        for ($k = 0; $k < $last; $k++) {
4227
            $answer .= $pre_array[$k];
4228
        }
4229
        // splits weightings that are joined with a comma
4230
        $answerWeighting = explode(',', $is_set_switchable[0]);
4231
4232
        // we save the answer because it will be modified
4233
        //$temp = $answer;
4234
        $temp = $answer;
4235
4236
        $answer = '';
4237
        $j = 0;
4238
        //initialise answer tags
4239
        $user_tags = $correct_tags = $real_text = [];
4240
        // the loop will stop at the end of the text
4241
        while (1) {
4242
            // quits the loop if there are no more blanks (detect '[')
4243
            if (false === ($pos = api_strpos($temp, '['))) {
4244
                // adds the end of the text
4245
                $answer = $temp;
4246
                $real_text[] = $answer;
4247
                break; //no more "blanks", quit the loop
4248
            }
4249
            // adds the piece of text that is before the blank
4250
            //and ends with '[' into a general storage array
4251
            $real_text[] = api_substr($temp, 0, $pos + 1);
4252
            $answer .= api_substr($temp, 0, $pos + 1);
4253
            //take the string remaining (after the last "[" we found)
4254
            $temp = api_substr($temp, $pos + 1);
4255
            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4256
            if (false === ($pos = api_strpos($temp, ']'))) {
4257
                // adds the end of the text
4258
                $answer .= $temp;
4259
                break;
4260
            }
4261
4262
            $str = $user_answer;
4263
4264
            preg_match_all('#\[([^[]*)\]#', $str, $arr);
4265
            $str = str_replace('\r\n', '', $str);
4266
            $choices = $arr[1];
4267
            $choice = [];
4268
            $check = false;
4269
            $i = 0;
4270
            foreach ($choices as $item) {
4271
                if ($current_answer === $item) {
4272
                    $check = true;
4273
                }
4274
                if ($check) {
4275
                    $choice[] = $item;
4276
                    $i++;
4277
                }
4278
                if (3 == $i) {
4279
                    break;
4280
                }
4281
            }
4282
            $tmp = api_strrpos($choice[$j], ' / ');
4283
4284
            if (false !== $tmp) {
4285
                $choice[$j] = api_substr($choice[$j], 0, $tmp);
4286
            }
4287
4288
            $choice[$j] = trim($choice[$j]);
4289
4290
            //Needed to let characters ' and " to work as part of an answer
4291
            $choice[$j] = stripslashes($choice[$j]);
4292
4293
            $user_tags[] = api_strtolower($choice[$j]);
4294
            //put the contents of the [] answer tag into correct_tags[]
4295
            $correct_tags[] = api_strtolower(api_substr($temp, 0, $pos));
4296
            $j++;
4297
            $temp = api_substr($temp, $pos + 1);
4298
        }
4299
4300
        $answer = '';
4301
        $real_correct_tags = $correct_tags;
4302
        $chosen_list = [];
4303
        $good_answer = [];
4304
4305
        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...
4306
            if (!$switchable_answer_set) {
4307
                //needed to parse ' and " characters
4308
                $user_tags[$i] = stripslashes($user_tags[$i]);
4309
                if ($correct_tags[$i] == $user_tags[$i]) {
4310
                    $good_answer[$correct_tags[$i]] = 1;
4311
                } elseif (!empty($user_tags[$i])) {
4312
                    $good_answer[$correct_tags[$i]] = 0;
4313
                } else {
4314
                    $good_answer[$correct_tags[$i]] = 0;
4315
                }
4316
            } else {
4317
                // switchable fill in the blanks
4318
                if (in_array($user_tags[$i], $correct_tags)) {
4319
                    $correct_tags = array_diff($correct_tags, $chosen_list);
4320
                    $good_answer[$correct_tags[$i]] = 1;
4321
                } elseif (!empty($user_tags[$i])) {
4322
                    $good_answer[$correct_tags[$i]] = 0;
4323
                } else {
4324
                    $good_answer[$correct_tags[$i]] = 0;
4325
                }
4326
            }
4327
            // adds the correct word, followed by ] to close the blank
4328
            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4329
            if (isset($real_text[$i + 1])) {
4330
                $answer .= $real_text[$i + 1];
4331
            }
4332
        }
4333
4334
        return $good_answer;
4335
    }
4336
4337
    /**
4338
     * Return an HTML select menu with the student groups.
4339
     *
4340
     * @param string $name     is the name and the id of the <select>
4341
     * @param string $default  default value for option
4342
     * @param string $onchange
4343
     *
4344
     * @return string the html code of the <select>
4345
     */
4346
    public static function displayGroupMenu($name, $default, $onchange = "")
4347
    {
4348
        // check the default value of option
4349
        $tabSelected = [$default => " selected='selected' "];
4350
        $res = "<select name='$name' id='$name' onchange='".$onchange."' >";
4351
        $res .= "<option value='-1'".$tabSelected["-1"].">-- ".get_lang('All groups')." --</option>";
4352
        $res .= "<option value='0'".$tabSelected["0"].">- ".get_lang('Not in a group')." -</option>";
4353
        $groups = GroupManager::get_group_list();
4354
        $currentCatId = 0;
4355
        $countGroups = count($groups);
4356
        for ($i = 0; $i < $countGroups; $i++) {
4357
            $category = GroupManager::get_category_from_group($groups[$i]['iid']);
4358
            if ($category['id'] != $currentCatId) {
4359
                $res .= "<option value='-1' disabled='disabled'>".$category['title']."</option>";
4360
                $currentCatId = $category['id'];
4361
            }
4362
            $res .= "<option ".$tabSelected[$groups[$i]['id']]."style='margin-left:40px' value='".
4363
                $groups[$i]["iid"]."'>".
4364
                $groups[$i]["name"].
4365
                "</option>";
4366
        }
4367
        $res .= "</select>";
4368
4369
        return $res;
4370
    }
4371
4372
    /**
4373
     * @param int $exe_id
4374
     */
4375
    public static function create_chat_exercise_session($exe_id)
4376
    {
4377
        if (!isset($_SESSION['current_exercises'])) {
4378
            $_SESSION['current_exercises'] = [];
4379
        }
4380
        $_SESSION['current_exercises'][$exe_id] = true;
4381
    }
4382
4383
    /**
4384
     * @param int $exe_id
4385
     */
4386
    public static function delete_chat_exercise_session($exe_id)
4387
    {
4388
        if (isset($_SESSION['current_exercises'])) {
4389
            $_SESSION['current_exercises'][$exe_id] = false;
4390
        }
4391
    }
4392
4393
    /**
4394
     * Display the exercise results.
4395
     *
4396
     * @param Exercise $objExercise
4397
     * @param int      $exeId
4398
     * @param bool     $save_user_result save users results (true) or just show the results (false)
4399
     * @param string   $remainingMessage
4400
     * @param bool     $allowSignature
4401
     * @param bool     $allowExportPdf
4402
     * @param bool     $isExport
4403
     */
4404
    public static function displayQuestionListByAttempt(
4405
        $objExercise,
4406
        $exeId,
4407
        $save_user_result = false,
4408
        $remainingMessage = '',
4409
        $allowSignature = false,
4410
        $allowExportPdf = false,
4411
        $isExport = false
4412
    ) {
4413
        $origin = api_get_origin();
4414
        $courseId = api_get_course_int_id();
4415
        $courseCode = api_get_course_id();
4416
        $sessionId = api_get_session_id();
4417
4418
        // Getting attempt info
4419
        $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
4420
4421
        // Getting question list
4422
        $question_list = [];
4423
        $studentInfo = [];
4424
        if (!empty($exercise_stat_info['data_tracking'])) {
4425
            $studentInfo = api_get_user_info($exercise_stat_info['exe_user_id']);
4426
            $question_list = explode(',', $exercise_stat_info['data_tracking']);
4427
        } else {
4428
            // Try getting the question list only if save result is off
4429
            if (false == $save_user_result) {
4430
                $question_list = $objExercise->get_validated_question_list();
4431
            }
4432
            if (in_array(
4433
                $objExercise->getFeedbackType(),
4434
                [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4435
            )) {
4436
                $question_list = $objExercise->get_validated_question_list();
4437
            }
4438
        }
4439
4440
        if ($objExercise->getResultAccess()) {
4441
            if (false === $objExercise->hasResultsAccess($exercise_stat_info)) {
4442
                echo Display::return_message(
4443
                    sprintf(get_lang('You have passed the %s minutes limit to see the results.'), $objExercise->getResultsAccess())
4444
                );
4445
4446
                return false;
4447
            }
4448
4449
            if (!empty($objExercise->getResultAccess())) {
4450
                $url = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$objExercise->id;
4451
                echo $objExercise->returnTimeLeftDiv();
4452
                echo $objExercise->showSimpleTimeControl(
4453
                    $objExercise->getResultAccessTimeDiff($exercise_stat_info),
4454
                    $url
4455
                );
4456
            }
4457
        }
4458
4459
        $counter = 1;
4460
        $total_score = $total_weight = 0;
4461
        $exerciseContent = null;
4462
4463
        // Hide results
4464
        $show_results = false;
4465
        $show_only_score = false;
4466
        if (in_array($objExercise->results_disabled,
4467
            [
4468
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4469
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
4470
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4471
            ]
4472
        )) {
4473
            $show_results = true;
4474
        }
4475
4476
        if (in_array(
4477
            $objExercise->results_disabled,
4478
            [
4479
                RESULT_DISABLE_SHOW_SCORE_ONLY,
4480
                RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES,
4481
                RESULT_DISABLE_RANKING,
4482
            ]
4483
        )
4484
        ) {
4485
            $show_only_score = true;
4486
        }
4487
4488
        // Not display expected answer, but score, and feedback
4489
        $show_all_but_expected_answer = false;
4490
        if (RESULT_DISABLE_SHOW_SCORE_ONLY == $objExercise->results_disabled &&
4491
            EXERCISE_FEEDBACK_TYPE_END == $objExercise->getFeedbackType()
4492
        ) {
4493
            $show_all_but_expected_answer = true;
4494
            $show_results = true;
4495
            $show_only_score = false;
4496
        }
4497
4498
        $showTotalScoreAndUserChoicesInLastAttempt = true;
4499
        $showTotalScore = true;
4500
        $showQuestionScore = true;
4501
        $attemptResult = [];
4502
4503
        if (in_array(
4504
            $objExercise->results_disabled,
4505
            [
4506
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
4507
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
4508
                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
4509
            ])
4510
        ) {
4511
            $show_only_score = true;
4512
            $show_results = true;
4513
            $numberAttempts = 0;
4514
            if ($objExercise->attempts > 0) {
4515
                $attempts = Event::getExerciseResultsByUser(
4516
                    api_get_user_id(),
4517
                    $objExercise->id,
4518
                    $courseId,
4519
                    $sessionId,
4520
                    $exercise_stat_info['orig_lp_id'],
4521
                    $exercise_stat_info['orig_lp_item_id'],
4522
                    'desc'
4523
                );
4524
                if ($attempts) {
4525
                    $numberAttempts = count($attempts);
4526
                }
4527
4528
                if ($save_user_result) {
4529
                    $numberAttempts++;
4530
                }
4531
4532
                $showTotalScore = false;
4533
                if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT == $objExercise->results_disabled) {
4534
                    $showTotalScore = true;
4535
                }
4536
                $showTotalScoreAndUserChoicesInLastAttempt = false;
4537
                if ($numberAttempts >= $objExercise->attempts) {
4538
                    $showTotalScore = true;
4539
                    $show_results = true;
4540
                    $show_only_score = false;
4541
                    $showTotalScoreAndUserChoicesInLastAttempt = true;
4542
                }
4543
4544
                if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $objExercise->results_disabled) {
4545
                    $showTotalScore = true;
4546
                    $show_results = true;
4547
                    $show_only_score = false;
4548
                    $showTotalScoreAndUserChoicesInLastAttempt = false;
4549
                    if ($numberAttempts >= $objExercise->attempts) {
4550
                        $showTotalScoreAndUserChoicesInLastAttempt = true;
4551
                    }
4552
4553
                    // Check if the current attempt is the last.
4554
                    if (false === $save_user_result && !empty($attempts)) {
4555
                        $showTotalScoreAndUserChoicesInLastAttempt = false;
4556
                        $position = 1;
4557
                        foreach ($attempts as $attempt) {
4558
                            if ($exeId == $attempt['exe_id']) {
4559
                                break;
4560
                            }
4561
                            $position++;
4562
                        }
4563
4564
                        if ($position == $objExercise->attempts) {
4565
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
4566
                        }
4567
                    }
4568
                }
4569
            }
4570
4571
            if (RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK ==
4572
                $objExercise->results_disabled
4573
            ) {
4574
                $show_only_score = false;
4575
                $show_results = true;
4576
                $show_all_but_expected_answer = false;
4577
                $showTotalScore = false;
4578
                $showQuestionScore = false;
4579
                if ($numberAttempts >= $objExercise->attempts) {
4580
                    $showTotalScore = true;
4581
                    $showQuestionScore = true;
4582
                }
4583
            }
4584
        }
4585
4586
        // When exporting to PDF hide feedback/comment/score show warning in hotspot.
4587
        if ($allowExportPdf && $isExport) {
4588
            $showTotalScore = false;
4589
            $showQuestionScore = false;
4590
            $objExercise->feedback_type = 2;
4591
            $objExercise->hideComment = true;
4592
            $objExercise->hideNoAnswer = true;
4593
            $objExercise->results_disabled = 0;
4594
            $objExercise->hideExpectedAnswer = true;
4595
            $show_results = true;
4596
        }
4597
4598
        if ('embeddable' !== $origin &&
4599
            !empty($exercise_stat_info['exe_user_id']) &&
4600
            !empty($studentInfo)
4601
        ) {
4602
            // Shows exercise header.
4603
            echo $objExercise->showExerciseResultHeader(
4604
                $studentInfo,
4605
                $exercise_stat_info,
4606
                $save_user_result,
4607
                $allowSignature,
4608
                $allowExportPdf
4609
            );
4610
        }
4611
4612
        $question_list_answers = [];
4613
        $category_list = [];
4614
        $loadChoiceFromSession = false;
4615
        $fromDatabase = true;
4616
        $exerciseResult = null;
4617
        $exerciseResultCoordinates = null;
4618
        $delineationResults = null;
4619
        if (true === $save_user_result && in_array(
4620
            $objExercise->getFeedbackType(),
4621
            [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4622
        )) {
4623
            $loadChoiceFromSession = true;
4624
            $fromDatabase = false;
4625
            $exerciseResult = Session::read('exerciseResult');
4626
            $exerciseResultCoordinates = Session::read('exerciseResultCoordinates');
4627
            $delineationResults = Session::read('hotspot_delineation_result');
4628
            $delineationResults = isset($delineationResults[$objExercise->id]) ? $delineationResults[$objExercise->id] : null;
4629
        }
4630
4631
        $countPendingQuestions = 0;
4632
        $result = [];
4633
        $panelsByParent = [];
4634
        // Loop over all question to show results for each of them, one by one
4635
        if (!empty($question_list)) {
4636
            foreach ($question_list as $questionId) {
4637
                // Creates a temporary Question object
4638
                $objQuestionTmp = Question::read($questionId, $objExercise->course);
4639
                // This variable came from exercise_submit_modal.php
4640
                ob_start();
4641
                $choice = null;
4642
                $delineationChoice = null;
4643
                if ($loadChoiceFromSession) {
4644
                    $choice = isset($exerciseResult[$questionId]) ? $exerciseResult[$questionId] : null;
4645
                    $delineationChoice = isset($delineationResults[$questionId]) ? $delineationResults[$questionId] : null;
4646
                }
4647
4648
                // We're inside *one* question. Go through each possible answer for this question
4649
                $result = $objExercise->manage_answer(
4650
                    $exeId,
4651
                    $questionId,
4652
                    $choice,
4653
                    'exercise_result',
4654
                    $exerciseResultCoordinates,
4655
                    $save_user_result,
4656
                    $fromDatabase,
4657
                    $show_results,
4658
                    $objExercise->selectPropagateNeg(),
4659
                    $delineationChoice,
4660
                    $showTotalScoreAndUserChoicesInLastAttempt
4661
                );
4662
4663
                if (empty($result)) {
4664
                    continue;
4665
                }
4666
4667
                $total_score += $result['score'];
4668
                $total_weight += $result['weight'];
4669
4670
                $question_list_answers[] = [
4671
                    'question' => $result['open_question'],
4672
                    'answer' => $result['open_answer'],
4673
                    'answer_type' => $result['answer_type'],
4674
                    'generated_oral_file' => $result['generated_oral_file'],
4675
                ];
4676
4677
                $my_total_score = $result['score'];
4678
                $my_total_weight = $result['weight'];
4679
                $scorePassed = self::scorePassed($my_total_score, $my_total_weight);
4680
4681
                // Category report
4682
                $category_was_added_for_this_test = false;
4683
                if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
4684
                    if (!isset($category_list[$objQuestionTmp->category]['score'])) {
4685
                        $category_list[$objQuestionTmp->category]['score'] = 0;
4686
                    }
4687
                    if (!isset($category_list[$objQuestionTmp->category]['total'])) {
4688
                        $category_list[$objQuestionTmp->category]['total'] = 0;
4689
                    }
4690
                    if (!isset($category_list[$objQuestionTmp->category]['total_questions'])) {
4691
                        $category_list[$objQuestionTmp->category]['total_questions'] = 0;
4692
                    }
4693
                    if (!isset($category_list[$objQuestionTmp->category]['passed'])) {
4694
                        $category_list[$objQuestionTmp->category]['passed'] = 0;
4695
                    }
4696
                    if (!isset($category_list[$objQuestionTmp->category]['wrong'])) {
4697
                        $category_list[$objQuestionTmp->category]['wrong'] = 0;
4698
                    }
4699
                    if (!isset($category_list[$objQuestionTmp->category]['no_answer'])) {
4700
                        $category_list[$objQuestionTmp->category]['no_answer'] = 0;
4701
                    }
4702
4703
                    $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
4704
                    $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
4705
                    if ($scorePassed) {
4706
                        // Only count passed if score is not empty
4707
                        if (!empty($my_total_score)) {
4708
                            $category_list[$objQuestionTmp->category]['passed']++;
4709
                        }
4710
                    } else {
4711
                        if ($result['user_answered']) {
4712
                            $category_list[$objQuestionTmp->category]['wrong']++;
4713
                        } else {
4714
                            $category_list[$objQuestionTmp->category]['no_answer']++;
4715
                        }
4716
                    }
4717
4718
                    $category_list[$objQuestionTmp->category]['total_questions']++;
4719
                    $category_was_added_for_this_test = true;
4720
                }
4721
                if (isset($objQuestionTmp->category_list) && !empty($objQuestionTmp->category_list)) {
4722
                    foreach ($objQuestionTmp->category_list as $category_id) {
4723
                        $category_list[$category_id]['score'] += $my_total_score;
4724
                        $category_list[$category_id]['total'] += $my_total_weight;
4725
                        $category_was_added_for_this_test = true;
4726
                    }
4727
                }
4728
4729
                // No category for this question!
4730
                if (false == $category_was_added_for_this_test) {
4731
                    if (!isset($category_list['none']['score'])) {
4732
                        $category_list['none']['score'] = 0;
4733
                    }
4734
                    if (!isset($category_list['none']['total'])) {
4735
                        $category_list['none']['total'] = 0;
4736
                    }
4737
4738
                    $category_list['none']['score'] += $my_total_score;
4739
                    $category_list['none']['total'] += $my_total_weight;
4740
                }
4741
4742
                if (0 == $objExercise->selectPropagateNeg() && $my_total_score < 0) {
4743
                    $my_total_score = 0;
4744
                }
4745
4746
                $comnt = null;
4747
                if ($show_results) {
4748
                    $comnt = Event::get_comments($exeId, $questionId);
4749
                    $teacherAudio = self::getOralFeedbackAudio(
4750
                        $exeId,
4751
                        $questionId
4752
                    );
4753
4754
                    if (!empty($comnt) || $teacherAudio) {
4755
                        echo '<b>'.get_lang('Feedback').'</b>';
4756
                    }
4757
4758
                    if (!empty($comnt)) {
4759
                        echo self::getFeedbackText($comnt);
4760
                    }
4761
4762
                    if ($teacherAudio) {
4763
                        echo $teacherAudio;
4764
                    }
4765
                }
4766
4767
                $calculatedScore = [
4768
                    'result' => self::show_score(
4769
                        $my_total_score,
4770
                        $my_total_weight,
4771
                        false
4772
                    ),
4773
                    'pass' => $scorePassed,
4774
                    'score' => $my_total_score,
4775
                    'weight' => $my_total_weight,
4776
                    'comments' => $comnt,
4777
                    'user_answered' => $result['user_answered'],
4778
                ];
4779
4780
                $score = [];
4781
                if ($show_results) {
4782
                    $score = $calculatedScore;
4783
                }
4784
                if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
4785
                    $reviewScore = [
4786
                        'score' => $my_total_score,
4787
                        'comments' => Event::get_comments($exeId, $questionId),
4788
                    ];
4789
                    $check = $objQuestionTmp->isQuestionWaitingReview($reviewScore);
4790
                    if (false === $check) {
4791
                        $countPendingQuestions++;
4792
                    }
4793
                }
4794
4795
                $contents = ob_get_clean();
4796
                $questionContent = '';
4797
                if ($show_results) {
4798
                    $questionContent = '<div class="question-answer-result">';
4799
                    if (false === $showQuestionScore) {
4800
                        $score = [];
4801
                    }
4802
4803
                    // Shows question title an description
4804
                    $questionContent .= $objQuestionTmp->return_header(
4805
                        $objExercise,
4806
                        $counter,
4807
                        $score
4808
                    );
4809
                }
4810
                $counter++;
4811
                $questionContent .= $contents;
4812
                if ($show_results) {
4813
                    $questionContent .= '</div>';
4814
                }
4815
4816
                $calculatedScore['question_content'] = $questionContent;
4817
                $attemptResult[] = $calculatedScore;
4818
                $parentId = intval($objQuestionTmp->parent_id ?: 0);
4819
                $panelsByParent[$parentId][] = Display::panel($questionContent);
4820
            }
4821
4822
            foreach ($panelsByParent as $pid => $panels) {
4823
                if ($pid !== 0) {
4824
                    $mediaQ = Question::read($pid, $objExercise->course);
4825
                    echo '<div class="media-group">';
4826
                    echo '<div class="media-content">';
4827
                    ob_start();
4828
                    $objExercise->manage_answer(
4829
                        $exeId,
4830
                        $pid,
4831
                        null,
4832
                        'exercise_show',
4833
                        [],
4834
                        false,
4835
                        true,
4836
                        $show_results,
4837
                        $objExercise->selectPropagateNeg()
4838
                    );
4839
                    echo ob_get_clean();
4840
                    echo '</div>';
4841
                    if (!empty($mediaQ->description)) {
4842
                        echo '<div class="media-description">'
4843
                            . $mediaQ->description
4844
                            . '</div>';
4845
                    }
4846
                    echo '<div class="media-children">';
4847
                }
4848
4849
                foreach ($panels as $panelHtml) {
4850
                    echo $panelHtml;
4851
                }
4852
4853
                if ($pid !== 0) {
4854
                    echo '</div></div>';
4855
                }
4856
            }
4857
        }
4858
4859
        // Display text when test is finished #4074 and for LP #4227
4860
        $endOfMessage = $objExercise->getFinishText($total_score, $total_weight);
4861
        if (!empty($endOfMessage)) {
4862
            echo Display::div(
4863
                $endOfMessage,
4864
                ['id' => 'quiz_end_message']
4865
            );
4866
        }
4867
4868
        $totalScoreText = null;
4869
        $certificateBlock = '';
4870
        if (($show_results || $show_only_score) && $showTotalScore) {
4871
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4872
                echo '<h1 style="text-align : center; margin : 20px 0;">'.get_lang('Your results').'</h1><br />';
4873
            }
4874
            $totalScoreText .= '<div class="question_row_score">';
4875
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4876
                $totalScoreText .= self::getQuestionDiagnosisRibbon(
4877
                    $objExercise,
4878
                    $total_score,
4879
                    $total_weight,
4880
                    true
4881
                );
4882
            } else {
4883
                $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
4884
                if ('true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)) {
4885
                    $formula = $pluginEvaluation->getFormulaForExercise($objExercise->getId());
4886
4887
                    if (!empty($formula)) {
4888
                        $total_score = $pluginEvaluation->getResultWithFormula($exeId, $formula);
4889
                        $total_weight = $pluginEvaluation->getMaxScore();
4890
                    }
4891
                }
4892
4893
                $totalScoreText .= self::getTotalScoreRibbon(
4894
                    $objExercise,
4895
                    $total_score,
4896
                    $total_weight,
4897
                    true,
4898
                    $countPendingQuestions
4899
                );
4900
            }
4901
            $totalScoreText .= '</div>';
4902
4903
            if (!empty($studentInfo)) {
4904
                $certificateBlock = self::generateAndShowCertificateBlock(
4905
                    $total_score,
4906
                    $total_weight,
4907
                    $objExercise,
4908
                    $studentInfo['id'],
4909
                    $courseId,
4910
                    $sessionId
4911
                );
4912
            }
4913
        }
4914
4915
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4916
            $chartMultiAnswer = MultipleAnswerTrueFalseDegreeCertainty::displayStudentsChartResults(
4917
                $exeId,
4918
                $objExercise
4919
            );
4920
            echo $chartMultiAnswer;
4921
        }
4922
4923
        if (!empty($category_list) &&
4924
            ($show_results || $show_only_score || RESULT_DISABLE_RADAR == $objExercise->results_disabled)
4925
        ) {
4926
            // Adding total
4927
            $category_list['total'] = [
4928
                'score' => $total_score,
4929
                'total' => $total_weight,
4930
            ];
4931
            echo TestCategory::get_stats_table_by_attempt($objExercise, $category_list);
4932
        }
4933
4934
        if ($show_all_but_expected_answer) {
4935
            $exerciseContent .= Display::return_message(get_lang('Note: This test has been setup to hide the expected answers.'));
4936
        }
4937
4938
        // Remove audio auto play from questions on results page - refs BT#7939
4939
        $exerciseContent = preg_replace(
4940
            ['/autoplay[\=\".+\"]+/', '/autostart[\=\".+\"]+/'],
4941
            '',
4942
            $exerciseContent
4943
        );
4944
4945
        echo $certificateBlock;
4946
4947
        // Ofaj change BT#11784
4948
        if (('true' === api_get_setting('exercise.quiz_show_description_on_results_page')) &&
4949
            !empty($objExercise->description)
4950
        ) {
4951
            echo Display::div($objExercise->description, ['class' => 'exercise_description']);
4952
        }
4953
4954
        echo $exerciseContent;
4955
        if (!$show_only_score) {
4956
            echo $totalScoreText;
4957
        }
4958
4959
        if ($save_user_result) {
4960
            // Tracking of results
4961
            if ($exercise_stat_info) {
4962
                $learnpath_id = $exercise_stat_info['orig_lp_id'];
4963
                $learnpath_item_id = $exercise_stat_info['orig_lp_item_id'];
4964
                $learnpath_item_view_id = $exercise_stat_info['orig_lp_item_view_id'];
4965
4966
                if (api_is_allowed_to_session_edit()) {
4967
                    Event::updateEventExercise(
4968
                        $exercise_stat_info['exe_id'],
4969
                        $objExercise->getId(),
4970
                        $total_score,
4971
                        $total_weight,
4972
                        $sessionId,
4973
                        $learnpath_id,
4974
                        $learnpath_item_id,
4975
                        $learnpath_item_view_id,
4976
                        $exercise_stat_info['exe_duration'],
4977
                        $question_list
4978
                    );
4979
4980
                    $allowStats = ('true' === api_get_setting('gradebook.allow_gradebook_stats'));
4981
                    if ($allowStats) {
4982
                        $objExercise->generateStats(
4983
                            $objExercise->getId(),
4984
                            api_get_course_info(),
4985
                            $sessionId
4986
                        );
4987
                    }
4988
                }
4989
            }
4990
4991
            // Send notification at the end
4992
            if (!api_is_allowed_to_edit(null, true) &&
4993
                !api_is_excluded_user_type()
4994
            ) {
4995
                $objExercise->send_mail_notification_for_exam(
4996
                    'end',
4997
                    $question_list_answers,
4998
                    $origin,
4999
                    $exeId,
5000
                    $total_score,
5001
                    $total_weight
5002
                );
5003
            }
5004
        }
5005
5006
        if (in_array(
5007
            $objExercise->selectResultsDisabled(),
5008
            [RESULT_DISABLE_RANKING, RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING]
5009
        )) {
5010
            echo Display::page_header(get_lang('Ranking'), null, 'h4');
5011
            echo self::displayResultsInRanking(
5012
                $objExercise,
5013
                api_get_user_id(),
5014
                $courseId,
5015
                $sessionId
5016
            );
5017
        }
5018
5019
        if (!empty($remainingMessage)) {
5020
            echo Display::return_message($remainingMessage, 'normal', false);
5021
        }
5022
5023
        $failedAnswersCount = 0;
5024
        $wrongQuestionHtml = '';
5025
        $all = '';
5026
        foreach ($attemptResult as $item) {
5027
            if (false === $item['pass']) {
5028
                $failedAnswersCount++;
5029
                $wrongQuestionHtml .= $item['question_content'].'<br />';
5030
            }
5031
            $all .= $item['question_content'].'<br />';
5032
        }
5033
5034
        $passed = self::isPassPercentageAttemptPassed(
5035
            $objExercise,
5036
            $total_score,
5037
            $total_weight
5038
        );
5039
5040
        $percentage = 0;
5041
        if (!empty($total_weight)) {
5042
            $percentage = ($total_score / $total_weight) * 100;
5043
        }
5044
5045
        return [
5046
            'category_list' => $category_list,
5047
            'attempts_result_list' => $attemptResult, // array of results
5048
            'exercise_passed' => $passed, // boolean
5049
            'total_answers_count' => count($attemptResult), // int
5050
            'failed_answers_count' => $failedAnswersCount, // int
5051
            'failed_answers_html' => $wrongQuestionHtml,
5052
            'all_answers_html' => $all,
5053
            'total_score' => $total_score,
5054
            'total_weight' => $total_weight,
5055
            'total_percentage' => $percentage,
5056
            'count_pending_questions' => $countPendingQuestions,
5057
        ];
5058
    }
5059
5060
    /**
5061
     * Display the ranking of results in a exercise.
5062
     *
5063
     * @param Exercise $exercise
5064
     * @param int      $currentUserId
5065
     * @param int      $courseId
5066
     * @param int      $sessionId
5067
     *
5068
     * @return string
5069
     */
5070
    public static function displayResultsInRanking($exercise, $currentUserId, $courseId, $sessionId = 0)
5071
    {
5072
        $exerciseId = $exercise->iId;
5073
        $data = self::exerciseResultsInRanking($exerciseId, $courseId, $sessionId);
5074
5075
        $table = new HTML_Table(['class' => 'table table-hover table-striped table-bordered']);
5076
        $table->setHeaderContents(0, 0, get_lang('Position'), ['class' => 'text-right']);
5077
        $table->setHeaderContents(0, 1, get_lang('Username'));
5078
        $table->setHeaderContents(0, 2, get_lang('Score'), ['class' => 'text-right']);
5079
        $table->setHeaderContents(0, 3, get_lang('Date'), ['class' => 'text-center']);
5080
5081
        foreach ($data as $r => $item) {
5082
            if (!isset($item[1])) {
5083
                continue;
5084
            }
5085
            $selected = $item[1]->getId() == $currentUserId;
5086
5087
            foreach ($item as $c => $value) {
5088
                $table->setCellContents($r + 1, $c, $value);
5089
5090
                $attrClass = '';
5091
5092
                if (in_array($c, [0, 2])) {
5093
                    $attrClass = 'text-right';
5094
                } elseif (3 == $c) {
5095
                    $attrClass = 'text-center';
5096
                }
5097
5098
                if ($selected) {
5099
                    $attrClass .= ' warning';
5100
                }
5101
5102
                $table->setCellAttributes($r + 1, $c, ['class' => $attrClass]);
5103
            }
5104
        }
5105
5106
        return $table->toHtml();
5107
    }
5108
5109
    /**
5110
     * Get the ranking for results in a exercise.
5111
     * Function used internally by ExerciseLib::displayResultsInRanking.
5112
     *
5113
     * @param int $exerciseId
5114
     * @param int $courseId
5115
     * @param int $sessionId
5116
     *
5117
     * @return array
5118
     */
5119
    public static function exerciseResultsInRanking($exerciseId, $courseId, $sessionId = 0)
5120
    {
5121
        $em = Database::getManager();
5122
5123
        $dql = 'SELECT DISTINCT u.id FROM ChamiloCoreBundle:TrackEExercise te JOIN te.user u WHERE te.quiz = :id AND te.course = :cId';
5124
        $dql .= api_get_session_condition($sessionId, true, false, 'te.session');
5125
5126
        $result = $em
5127
            ->createQuery($dql)
5128
            ->setParameters(['id' => $exerciseId, 'cId' => $courseId])
5129
            ->getScalarResult();
5130
5131
        $data = [];
5132
5133
        foreach ($result as $item) {
5134
            $attempt = self::get_best_attempt_by_user($item['id'], $exerciseId, $courseId, $sessionId);
5135
            if (!empty($attempt) && isset($attempt['score']) && isset($attempt['exe_date'])) {
5136
                $data[] = $attempt;
5137
            }
5138
        }
5139
5140
        if (empty($data)) {
5141
            return [];
5142
        }
5143
5144
        usort(
5145
            $data,
5146
            function ($a, $b) {
5147
                if ($a['score'] != $b['score']) {
5148
                    return $a['score'] > $b['score'] ? -1 : 1;
5149
                }
5150
5151
                if ($a['exe_date'] != $b['exe_date']) {
5152
                    return $a['exe_date'] < $b['exe_date'] ? -1 : 1;
5153
                }
5154
5155
                return 0;
5156
            }
5157
        );
5158
5159
        // flags to display the same position in case of tie
5160
        $lastScore = $data[0]['score'];
5161
        $position = 1;
5162
        $data = array_map(
5163
            function ($item) use (&$lastScore, &$position) {
5164
                if ($item['score'] < $lastScore) {
5165
                    $position++;
5166
                }
5167
5168
                $lastScore = $item['score'];
5169
5170
                return [
5171
                    $position,
5172
                    api_get_user_entity($item['exe_user_id']),
5173
                    self::show_score($item['score'], $item['max_score'], true, true, true),
5174
                    api_convert_and_format_date($item['exe_date'], DATE_TIME_FORMAT_SHORT),
5175
                ];
5176
            },
5177
            $data
5178
        );
5179
5180
        return $data;
5181
    }
5182
5183
    /**
5184
     * Get a special ribbon on top of "degree of certainty" questions (
5185
     * variation from getTotalScoreRibbon() for other question types).
5186
     *
5187
     * @param Exercise $objExercise
5188
     * @param float    $score
5189
     * @param float    $weight
5190
     * @param bool     $checkPassPercentage
5191
     *
5192
     * @return string
5193
     */
5194
    public static function getQuestionDiagnosisRibbon($objExercise, $score, $weight, $checkPassPercentage = false)
5195
    {
5196
        $displayChartDegree = true;
5197
        $ribbon = $displayChartDegree ? '<div class="ribbon">' : '';
5198
5199
        if ($checkPassPercentage) {
5200
            $passPercentage = $objExercise->selectPassPercentage();
5201
            $isSuccess = self::isSuccessExerciseResult($score, $weight, $passPercentage);
5202
            // Color the final test score if pass_percentage activated
5203
            $ribbonTotalSuccessOrError = '';
5204
            if (self::isPassPercentageEnabled($passPercentage)) {
5205
                if ($isSuccess) {
5206
                    $ribbonTotalSuccessOrError = ' ribbon-total-success';
5207
                } else {
5208
                    $ribbonTotalSuccessOrError = ' ribbon-total-error';
5209
                }
5210
            }
5211
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total '.$ribbonTotalSuccessOrError.'">' : '';
5212
        } else {
5213
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total">' : '';
5214
        }
5215
5216
        if ($displayChartDegree) {
5217
            $ribbon .= '<h3>'.get_lang('Score for the test').':&nbsp;';
5218
            $ribbon .= self::show_score($score, $weight, false, true);
5219
            $ribbon .= '</h3>';
5220
            $ribbon .= '</div>';
5221
        }
5222
5223
        if ($checkPassPercentage) {
5224
            $ribbon .= self::showSuccessMessage(
5225
                $score,
5226
                $weight,
5227
                $objExercise->selectPassPercentage()
5228
            );
5229
        }
5230
5231
        $ribbon .= $displayChartDegree ? '</div>' : '';
5232
5233
        return $ribbon;
5234
    }
5235
5236
    public static function isPassPercentageAttemptPassed($objExercise, $score, $weight)
5237
    {
5238
        $passPercentage = $objExercise->selectPassPercentage();
5239
5240
        return self::isSuccessExerciseResult($score, $weight, $passPercentage);
5241
    }
5242
5243
    /**
5244
     * @param float $score
5245
     * @param float $weight
5246
     * @param bool  $checkPassPercentage
5247
     * @param int   $countPendingQuestions
5248
     *
5249
     * @return string
5250
     */
5251
    public static function getTotalScoreRibbon(
5252
        Exercise $objExercise,
5253
        $score,
5254
        $weight,
5255
        $checkPassPercentage = false,
5256
        $countPendingQuestions = 0
5257
    ) {
5258
        $hide = (int) $objExercise->getPageConfigurationAttribute('hide_total_score');
5259
        if (1 === $hide) {
5260
            return '';
5261
        }
5262
5263
        $passPercentage = $objExercise->selectPassPercentage();
5264
        $ribbon = '<div class="title-score">';
5265
        if ($checkPassPercentage) {
5266
            $isSuccess = self::isSuccessExerciseResult(
5267
                $score,
5268
                $weight,
5269
                $passPercentage
5270
            );
5271
            // Color the final test score if pass_percentage activated
5272
            $class = '';
5273
            if (self::isPassPercentageEnabled($passPercentage)) {
5274
                if ($isSuccess) {
5275
                    $class = ' ribbon-total-success';
5276
                } else {
5277
                    $class = ' ribbon-total-error';
5278
                }
5279
            }
5280
            $ribbon .= '<div class="total '.$class.'">';
5281
        } else {
5282
            $ribbon .= '<div class="total">';
5283
        }
5284
        $ribbon .= '<h3>'.get_lang('Score for the test').':&nbsp;';
5285
        $ribbon .= self::show_score($score, $weight, false, true);
5286
        $ribbon .= '</h3>';
5287
        $ribbon .= '</div>';
5288
        if ($checkPassPercentage) {
5289
            $ribbon .= self::showSuccessMessage(
5290
                $score,
5291
                $weight,
5292
                $passPercentage
5293
            );
5294
        }
5295
        $ribbon .= '</div>';
5296
5297
        if (!empty($countPendingQuestions)) {
5298
            $ribbon .= '<br />';
5299
            $ribbon .= Display::return_message(
5300
                sprintf(
5301
                    get_lang('Temporary score: %s open question(s) not corrected yet.'),
5302
                    $countPendingQuestions
5303
                ),
5304
                'warning'
5305
            );
5306
        }
5307
5308
        return $ribbon;
5309
    }
5310
5311
    /**
5312
     * @param int $countLetter
5313
     *
5314
     * @return mixed
5315
     */
5316
    public static function detectInputAppropriateClass($countLetter)
5317
    {
5318
        $limits = [
5319
            0 => 'input-mini',
5320
            10 => 'input-mini',
5321
            15 => 'input-medium',
5322
            20 => 'input-xlarge',
5323
            40 => 'input-xlarge',
5324
            60 => 'input-xxlarge',
5325
            100 => 'input-xxlarge',
5326
            200 => 'input-xxlarge',
5327
        ];
5328
5329
        foreach ($limits as $size => $item) {
5330
            if ($countLetter <= $size) {
5331
                return $item;
5332
            }
5333
        }
5334
5335
        return $limits[0];
5336
    }
5337
5338
    /**
5339
     * @param int    $senderId
5340
     * @param array  $course_info
5341
     * @param string $test
5342
     * @param string $url
5343
     *
5344
     * @return string
5345
     */
5346
    public static function getEmailNotification($senderId, $course_info, $test, $url)
5347
    {
5348
        $teacher_info = api_get_user_info($senderId);
5349
        $fromName = api_get_person_name(
5350
            $teacher_info['firstname'],
5351
            $teacher_info['lastname'],
5352
            null,
5353
            PERSON_NAME_EMAIL_ADDRESS
5354
        );
5355
5356
        $params = [
5357
            'course_title' => Security::remove_XSS($course_info['name']),
5358
            'test_title' => Security::remove_XSS($test),
5359
            'url' => $url,
5360
            'teacher_name' => $fromName,
5361
        ];
5362
5363
        return Container::getTwig()->render(
5364
            '@ChamiloCore/Mailer/Exercise/result_alert_body.html.twig',
5365
            $params
5366
        );
5367
    }
5368
5369
    /**
5370
     * @return string
5371
     */
5372
    public static function getNotCorrectedYetText()
5373
    {
5374
        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');
5375
    }
5376
5377
    /**
5378
     * @param string $message
5379
     *
5380
     * @return string
5381
     */
5382
    public static function getFeedbackText($message)
5383
    {
5384
        return Display::return_message($message, 'warning', false);
5385
    }
5386
5387
    /**
5388
     * Get the recorder audio component for save a teacher audio feedback.
5389
     *
5390
     * @param int $attemptId
5391
     * @param int $questionId
5392
     *
5393
     * @return string
5394
     */
5395
    public static function getOralFeedbackForm($attemptId, $questionId)
5396
    {
5397
        $view = new Template('', false, false, false, false, false, false);
5398
        $view->assign('type', Asset::EXERCISE_FEEDBACK);
5399
        $view->assign('question_id', $questionId);
5400
        $view->assign('t_exercise_id', $attemptId);
5401
        $template = $view->get_template('exercise/oral_expression.html.twig');
5402
5403
        return $view->fetch($template);
5404
    }
5405
5406
    /**
5407
     * Retrieves the generated audio files for an oral question in an exercise attempt.
5408
     *
5409
     * @param int  $trackExerciseId The ID of the tracked exercise.
5410
     * @param int  $questionId      The ID of the question.
5411
     * @param bool $returnUrls      (Optional) If set to true, only the URLs of the audio files are returned. Default is false.
5412
     *
5413
     * @return array|string If $returnUrls is true, returns an array of URLs of the audio files. Otherwise, returns an HTML string with audio tags.
5414
     */
5415
    public static function getOralFileAudio(int $trackExerciseId, int $questionId, bool $returnUrls = false): array|string
5416
    {
5417
        /** @var TrackEExercise $trackExercise */
5418
        $trackExercise = Container::getTrackEExerciseRepository()->find($trackExerciseId);
5419
5420
        if (null === $trackExercise) {
5421
            return $returnUrls ? [] : '';
5422
        }
5423
5424
        $questionAttempt = $trackExercise->getAttemptByQuestionId($questionId);
5425
5426
        if (null === $questionAttempt) {
5427
            return $returnUrls ? [] : '';
5428
        }
5429
5430
        $basePath = rtrim(api_get_path(WEB_PATH), '/');
5431
        $assetRepo = Container::getAssetRepository();
5432
5433
        if ($returnUrls) {
5434
            $urls = [];
5435
            foreach ($questionAttempt->getAttemptFiles() as $attemptFile) {
5436
                $urls[] = $basePath.$assetRepo->getAssetUrl($attemptFile->getAsset());
5437
            }
5438
5439
            return $urls;
5440
        } else {
5441
            $html = '';
5442
            foreach ($questionAttempt->getAttemptFiles() as $attemptFile) {
5443
                $html .= Display::tag(
5444
                    'audio',
5445
                    '',
5446
                    [
5447
                        'src' => $basePath.$assetRepo->getAssetUrl($attemptFile->getAsset()),
5448
                        'controls' => '',
5449
                    ]
5450
                );
5451
            }
5452
5453
            return $html;
5454
        }
5455
    }
5456
5457
    /**
5458
     * Get the audio component for a teacher audio feedback.
5459
     */
5460
    public static function getOralFeedbackAudio(int $attemptId, int $questionId): string
5461
    {
5462
        /** @var TrackEExercise $tExercise */
5463
        $tExercise = Container::getTrackEExerciseRepository()->find($attemptId);
5464
5465
        if (null === $tExercise) {
5466
            return '';
5467
        }
5468
5469
        $qAttempt = $tExercise->getAttemptByQuestionId($questionId);
5470
5471
        if (null === $qAttempt) {
5472
            return '';
5473
        }
5474
5475
        $html = '';
5476
5477
        $assetRepo = Container::getAssetRepository();
5478
5479
        foreach ($qAttempt->getAttemptFeedbacks() as $attemptFeedback) {
5480
            $html .= Display::tag(
5481
                'audio',
5482
                '',
5483
                [
5484
                    'src' => $assetRepo->getAssetUrl($attemptFeedback->getAsset()),
5485
                    'controls' => '',
5486
                ]
5487
5488
            );
5489
        }
5490
5491
        return $html;
5492
    }
5493
5494
    public static function getUploadAnswerFiles(int $trackExerciseId, int $questionId, bool $returnUrls = false)
5495
    {
5496
        $trackExercise = Container::getTrackEExerciseRepository()->find($trackExerciseId);
5497
        if (!$trackExercise) { return $returnUrls ? [] : ''; }
5498
        $attempt = $trackExercise->getAttemptByQuestionId($questionId);
5499
        if (!$attempt) { return $returnUrls ? [] : ''; }
5500
5501
        $assetRepo = Container::getAssetRepository();
5502
        $basePath = rtrim(api_get_path(WEB_PATH), '/');
5503
5504
        if ($returnUrls) {
5505
            $urls = [];
5506
            foreach ($attempt->getAttemptFiles() as $af) {
5507
                $urls[] = $basePath.$assetRepo->getAssetUrl($af->getAsset());
5508
            }
5509
            return $urls;
5510
        }
5511
5512
        $html = '';
5513
        foreach ($attempt->getAttemptFiles() as $af) {
5514
            $url = $basePath.$assetRepo->getAssetUrl($af->getAsset());
5515
            $html .= Display::url(basename($url), $url, ['target' => '_blank']).'<br />';
5516
        }
5517
        return $html;
5518
    }
5519
5520
    public static function getNotificationSettings(): array
5521
    {
5522
        return [
5523
            2 => get_lang('Paranoid: E-mail teacher when a student starts an exercise'),
5524
            1 => get_lang('Aware: E-mail teacher when a student ends an exercise'), // default
5525
            3 => get_lang('Relaxed open: E-mail teacher when a student ends an exercise, only if an open question is answered'),
5526
            4 => get_lang('Relaxed audio: E-mail teacher when a student ends an exercise, only if an oral question is answered'),
5527
        ];
5528
    }
5529
5530
    /**
5531
     * Get the additional actions added in exercise_additional_teacher_modify_actions configuration.
5532
     *
5533
     * @param int $exerciseId
5534
     * @param int $iconSize
5535
     *
5536
     * @return string
5537
     */
5538
    public static function getAdditionalTeacherActions($exerciseId, $iconSize = ICON_SIZE_SMALL)
5539
    {
5540
        $additionalActions = api_get_setting('exercise.exercise_additional_teacher_modify_actions', true) ?: [];
5541
        $actions = [];
5542
5543
        if (is_array($additionalActions)) {
5544
            foreach ($additionalActions as $additionalAction) {
5545
                $actions[] = call_user_func(
5546
                    $additionalAction,
5547
                    $exerciseId,
5548
                    $iconSize
5549
                );
5550
            }
5551
        }
5552
5553
        return implode(PHP_EOL, $actions);
5554
    }
5555
5556
    /**
5557
     * @param int $userId
5558
     * @param int $courseId
5559
     * @param int $sessionId
5560
     *
5561
     * @return int
5562
     */
5563
    public static function countAnsweredQuestionsByUserAfterTime(DateTime $time, $userId, $courseId, $sessionId)
5564
    {
5565
        $em = Database::getManager();
5566
5567
        if (empty($sessionId)) {
5568
            $sessionId = null;
5569
        }
5570
5571
        $time = api_get_utc_datetime($time->format('Y-m-d H:i:s'), false, true);
5572
5573
        $result = $em
5574
            ->createQuery('
5575
                SELECT COUNT(ea) FROM ChamiloCoreBundle:TrackEAttempt ea
5576
                WHERE ea.userId = :user AND ea.cId = :course AND ea.sessionId = :session
5577
                    AND ea.tms > :time
5578
            ')
5579
            ->setParameters(['user' => $userId, 'course' => $courseId, 'session' => $sessionId, 'time' => $time])
5580
            ->getSingleScalarResult();
5581
5582
        return $result;
5583
    }
5584
5585
    /**
5586
     * @param int $userId
5587
     * @param int $numberOfQuestions
5588
     * @param int $courseId
5589
     * @param int $sessionId
5590
     *
5591
     * @throws \Doctrine\ORM\Query\QueryException
5592
     *
5593
     * @return bool
5594
     */
5595
    public static function isQuestionsLimitPerDayReached($userId, $numberOfQuestions, $courseId, $sessionId)
5596
    {
5597
        $questionsLimitPerDay = (int) api_get_course_setting('quiz_question_limit_per_day');
5598
5599
        if ($questionsLimitPerDay <= 0) {
5600
            return false;
5601
        }
5602
5603
        $midnightTime = ChamiloHelper::getServerMidnightTime();
5604
5605
        $answeredQuestionsCount = self::countAnsweredQuestionsByUserAfterTime(
5606
            $midnightTime,
5607
            $userId,
5608
            $courseId,
5609
            $sessionId
5610
        );
5611
5612
        return ($answeredQuestionsCount + $numberOfQuestions) > $questionsLimitPerDay;
5613
    }
5614
5615
    /**
5616
     * Check if an exercise complies with the requirements to be embedded in the mobile app or a video.
5617
     * By making sure it is set on one question per page and it only contains unique-answer or multiple-answer questions
5618
     * or unique-answer image. And that the exam does not have immediate feedback.
5619
     *
5620
     * @return bool
5621
     */
5622
    public static function isQuizEmbeddable(CQuiz $exercise)
5623
    {
5624
        $em = Database::getManager();
5625
5626
        if (ONE_PER_PAGE != $exercise->getType() ||
5627
            in_array($exercise->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])
5628
        ) {
5629
            return false;
5630
        }
5631
5632
        $countAll = $em
5633
            ->createQuery('SELECT COUNT(qq)
5634
                FROM ChamiloCourseBundle:CQuizQuestion qq
5635
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5636
                   WITH qq.iid = qrq.question
5637
                WHERE qrq.quiz = :id'
5638
            )
5639
            ->setParameter('id', $exercise->getIid())
5640
            ->getSingleScalarResult();
5641
5642
        $countOfAllowed = $em
5643
            ->createQuery('SELECT COUNT(qq)
5644
                FROM ChamiloCourseBundle:CQuizQuestion qq
5645
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5646
                   WITH qq.iid = qrq.question
5647
                WHERE qrq.quiz = :id AND qq.type IN (:types)'
5648
            )
5649
            ->setParameters(
5650
                [
5651
                    'id' => $exercise->getIid(),
5652
                    'types' => [UNIQUE_ANSWER, MULTIPLE_ANSWER, UNIQUE_ANSWER_IMAGE],
5653
                ]
5654
            )
5655
            ->getSingleScalarResult();
5656
5657
        return $countAll === $countOfAllowed;
5658
    }
5659
5660
    /**
5661
     * Generate a certificate linked to current quiz and.
5662
     * Return the HTML block with links to download and view the certificate.
5663
     *
5664
     * @param float $totalScore
5665
     * @param float $totalWeight
5666
     * @param int   $studentId
5667
     * @param int   $courseId
5668
     * @param int   $sessionId
5669
     *
5670
     * @return string
5671
     */
5672
    public static function generateAndShowCertificateBlock(
5673
        $totalScore,
5674
        $totalWeight,
5675
        Exercise $objExercise,
5676
        $studentId,
5677
        $courseId,
5678
        $sessionId = 0
5679
    ) {
5680
        if (('true' !== api_get_setting('exercise.quiz_generate_certificate_ending')) ||
5681
            !self::isSuccessExerciseResult($totalScore, $totalWeight, $objExercise->selectPassPercentage())
5682
        ) {
5683
            return '';
5684
        }
5685
5686
        $repo = Container::getGradeBookCategoryRepository();
5687
        /** @var GradebookCategory $category */
5688
        $category = $repo->findOneBy(
5689
            ['course' => $courseId, 'session' => $sessionId]
5690
        );
5691
5692
        if (null === $category) {
5693
            return '';
5694
        }
5695
5696
        /*$category = Category::load(null, null, $courseCode, null, null, $sessionId, 'ORDER By id');
5697
        if (empty($category)) {
5698
            return '';
5699
        }*/
5700
        $categoryId = $category->getId();
5701
        /*$link = LinkFactory::load(
5702
            null,
5703
            null,
5704
            $objExercise->getId(),
5705
            null,
5706
            $courseCode,
5707
            $categoryId
5708
        );*/
5709
5710
        if (empty($category->getLinks()->count())) {
5711
            return '';
5712
        }
5713
5714
        $resourceDeletedMessage = Category::show_message_resource_delete($courseId);
5715
        if (!empty($resourceDeletedMessage) || api_is_allowed_to_edit() || api_is_excluded_user_type()) {
5716
            return '';
5717
        }
5718
5719
        $certificate = Category::generateUserCertificate($category, $studentId);
5720
        if (!is_array($certificate)) {
5721
            return '';
5722
        }
5723
5724
        return Category::getDownloadCertificateBlock($certificate);
5725
    }
5726
5727
    /**
5728
     * @param int $exerciseId
5729
     */
5730
    public static function getExerciseTitleById($exerciseId)
5731
    {
5732
        $em = Database::getManager();
5733
5734
        return $em
5735
            ->createQuery('SELECT cq.title
5736
                FROM ChamiloCourseBundle:CQuiz cq
5737
                WHERE cq.iid = :iid'
5738
            )
5739
            ->setParameter('iid', $exerciseId)
5740
            ->getSingleScalarResult();
5741
    }
5742
5743
    /**
5744
     * @param int $exeId      ID from track_e_exercises
5745
     * @param int $userId     User ID
5746
     * @param int $exerciseId Exercise ID
5747
     * @param int $courseId   Optional. Coure ID.
5748
     *
5749
     * @return TrackEExercise|null
5750
     */
5751
    public static function recalculateResult($exeId, $userId, $exerciseId, $courseId = 0)
5752
    {
5753
        if (empty($userId) || empty($exerciseId)) {
5754
            return null;
5755
        }
5756
5757
        $em = Database::getManager();
5758
        /** @var TrackEExercise $trackedExercise */
5759
        $trackedExercise = $em->getRepository(TrackEExercise::class)->find($exeId);
5760
5761
        if (empty($trackedExercise)) {
5762
            return null;
5763
        }
5764
5765
        if ($trackedExercise->getUser()->getId() != $userId ||
5766
            $trackedExercise->getQuiz()?->getIid() != $exerciseId
5767
        ) {
5768
            return null;
5769
        }
5770
5771
        $questionList = $trackedExercise->getDataTracking();
5772
5773
        if (empty($questionList)) {
5774
            return null;
5775
        }
5776
5777
        $questionList = explode(',', $questionList);
5778
5779
        $exercise = new Exercise($courseId);
5780
        $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : [];
5781
5782
        if (false === $exercise->read($exerciseId)) {
5783
            return null;
5784
        }
5785
5786
        $totalScore = 0;
5787
        $totalWeight = 0;
5788
5789
        $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
5790
5791
        $formula = 'true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)
5792
            ? $pluginEvaluation->getFormulaForExercise($exerciseId)
5793
            : 0;
5794
5795
        if (empty($formula)) {
5796
            foreach ($questionList as $questionId) {
5797
                $question = Question::read($questionId, $courseInfo);
5798
5799
                if (false === $question) {
5800
                    continue;
5801
                }
5802
5803
                $totalWeight += $question->selectWeighting();
5804
5805
                // We're inside *one* question. Go through each possible answer for this question
5806
                $result = $exercise->manage_answer(
5807
                    $exeId,
5808
                    $questionId,
5809
                    [],
5810
                    'exercise_result',
5811
                    [],
5812
                    false,
5813
                    true,
5814
                    false,
5815
                    $exercise->selectPropagateNeg(),
5816
                    [],
5817
                    [],
5818
                    true
5819
                );
5820
5821
                //  Adding the new score.
5822
                $totalScore += $result['score'];
5823
            }
5824
        } else {
5825
            $totalScore = $pluginEvaluation->getResultWithFormula($exeId, $formula);
5826
            $totalWeight = $pluginEvaluation->getMaxScore();
5827
        }
5828
5829
        $trackedExercise
5830
            ->setScore($totalScore)
5831
            ->setMaxScore($totalWeight);
5832
5833
        $em->persist($trackedExercise);
5834
        $em->flush();
5835
        $lpItemId = $trackedExercise->getOrigLpItemId();
5836
        $lpId = $trackedExercise->getOrigLpId();
5837
        $lpItemViewId = $trackedExercise->getOrigLpItemViewId();
5838
        if ($lpId && $lpItemId && $lpItemViewId) {
5839
            $lpItem = $em->getRepository(CLpItem::class)->find($lpItemId);
5840
            if ($lpItem && 'quiz' === $lpItem->getItemType()) {
5841
                $lpItemView = $em->getRepository(CLpItemView::class)->find($lpItemViewId);
5842
                if ($lpItemView) {
5843
                    $lpItemView->setScore($totalScore);
5844
                    $em->persist($lpItemView);
5845
                    $em->flush();
5846
                }
5847
            }
5848
        }
5849
5850
        return $trackedExercise;
5851
    }
5852
5853
    public static function getTotalQuestionAnswered($courseId, $exerciseId, $questionId, $onlyStudents = false): int
5854
    {
5855
        $courseId = (int) $courseId;
5856
        $exerciseId = (int) $exerciseId;
5857
        $questionId = (int) $questionId;
5858
5859
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5860
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5861
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
5862
        $courseUserJoin = "";
5863
        $studentsWhere = "";
5864
        if ($onlyStudents) {
5865
            $courseUserJoin = "
5866
            INNER JOIN $courseUser cu
5867
            ON cu.c_id = te.c_id AND cu.user_id = exe_user_id";
5868
            $studentsWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
5869
        }
5870
5871
        $sql = "SELECT count(distinct (te.exe_id)) total
5872
            FROM $attemptTable t
5873
            INNER JOIN $trackTable te
5874
            ON (t.exe_id = te.exe_id)
5875
            $courseUserJoin
5876
            WHERE
5877
                te.c_id = $courseId AND
5878
                exe_exo_id = $exerciseId AND
5879
                t.question_id = $questionId AND
5880
                te.status != 'incomplete'
5881
                $studentsWhere
5882
        ";
5883
        $queryTotal = Database::query($sql);
5884
        $totalRow = Database::fetch_assoc($queryTotal);
5885
        $total = 0;
5886
        if ($totalRow) {
5887
            $total = (int) $totalRow['total'];
5888
        }
5889
5890
        return $total;
5891
    }
5892
5893
    public static function getWrongQuestionResults($courseId, $exerciseId, $sessionId = 0, $limit = 10)
5894
    {
5895
        $courseId = (int) $courseId;
5896
        $exerciseId = (int) $exerciseId;
5897
        $limit = (int) $limit;
5898
5899
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
5900
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5901
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5902
5903
        $sessionCondition = '';
5904
        if (!empty($sessionId)) {
5905
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
5906
        }
5907
5908
        $sql = "SELECT q.question, question_id, count(q.iid) count
5909
                FROM $attemptTable t
5910
                INNER JOIN $questionTable q
5911
                ON (q.iid = t.question_id)
5912
                INNER JOIN $trackTable te
5913
                ON (t.exe_id = te.exe_id)
5914
                WHERE
5915
                    te.c_id = $courseId AND
5916
                    t.marks != q.ponderation AND
5917
                    exe_exo_id = $exerciseId AND
5918
                    status != 'incomplete'
5919
                    $sessionCondition
5920
                GROUP BY q.iid
5921
                ORDER BY count DESC
5922
                LIMIT $limit
5923
        ";
5924
5925
        $result = Database::query($sql);
5926
5927
        return Database::store_result($result, 'ASSOC');
5928
    }
5929
5930
    public static function getExerciseResultsCount($type, $courseId, $exerciseId, $sessionId = 0)
5931
    {
5932
        $courseId = (int) $courseId;
5933
        $exerciseId = (int) $exerciseId;
5934
5935
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5936
5937
        $sessionCondition = '';
5938
        if (!empty($sessionId)) {
5939
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
5940
        }
5941
5942
        $selectCount = 'count(DISTINCT te.exe_id)';
5943
        $scoreCondition = '';
5944
        switch ($type) {
5945
            case 'correct_student':
5946
                $selectCount = 'count(DISTINCT te.exe_user_id)';
5947
                $scoreCondition = ' AND score = max_score ';
5948
                break;
5949
            case 'wrong_student':
5950
                $selectCount = 'count(DISTINCT te.exe_user_id)';
5951
                $scoreCondition = ' AND score != max_score ';
5952
                break;
5953
            case 'correct':
5954
                $scoreCondition = ' AND score = max_score ';
5955
                break;
5956
            case 'wrong':
5957
                $scoreCondition = ' AND score != max_score ';
5958
                break;
5959
        }
5960
5961
        $sql = "SELECT $selectCount count
5962
                FROM $trackTable te
5963
                WHERE
5964
                    c_id = $courseId AND
5965
                    exe_exo_id = $exerciseId AND
5966
                    status != 'incomplete'
5967
                    $scoreCondition
5968
                    $sessionCondition
5969
        ";
5970
        $result = Database::query($sql);
5971
        $totalRow = Database::fetch_assoc($result);
5972
        $total = 0;
5973
        if ($totalRow) {
5974
            $total = (int) $totalRow['count'];
5975
        }
5976
5977
        return $total;
5978
    }
5979
5980
    public static function parseContent($content, $stats, Exercise $exercise, $trackInfo, $currentUserId = 0)
5981
    {
5982
        $wrongAnswersCount = $stats['failed_answers_count'];
5983
        $attemptDate = substr($trackInfo['exe_date'], 0, 10);
5984
        $exerciseId = $exercise->iId;
5985
        $resultsStudentUrl = api_get_path(WEB_CODE_PATH).
5986
            'exercise/result.php?id='.$exerciseId.'&'.api_get_cidreq();
5987
        $resultsTeacherUrl = api_get_path(WEB_CODE_PATH).
5988
            'exercise/exercise_show.php?action=edit&id='.$exerciseId.'&'.api_get_cidreq();
5989
5990
        $content = str_replace(
5991
            [
5992
                '((exercise_error_count))',
5993
                '((all_answers_html))',
5994
                '((all_answers_teacher_html))',
5995
                '((exercise_title))',
5996
                '((exercise_attempt_date))',
5997
                '((link_to_test_result_page_student))',
5998
                '((link_to_test_result_page_teacher))',
5999
            ],
6000
            [
6001
                $wrongAnswersCount,
6002
                $stats['all_answers_html'],
6003
                $stats['all_answers_teacher_html'],
6004
                $exercise->get_formated_title(),
6005
                $attemptDate,
6006
                $resultsStudentUrl,
6007
                $resultsTeacherUrl,
6008
            ],
6009
            $content
6010
        );
6011
6012
        $currentUserId = empty($currentUserId) ? api_get_user_id() : (int) $currentUserId;
6013
6014
        $content = AnnouncementManager::parseContent(
6015
            $currentUserId,
6016
            $content,
6017
            api_get_course_id(),
6018
            api_get_session_id()
6019
        );
6020
6021
        return $content;
6022
    }
6023
6024
    public static function sendNotification(
6025
        $currentUserId,
6026
        $objExercise,
6027
        $exercise_stat_info,
6028
        $courseInfo,
6029
        $attemptCountToSend,
6030
        $stats,
6031
        $statsTeacher
6032
    ) {
6033
        $notifications = api_get_configuration_value('exercise_finished_notification_settings');
6034
        if (empty($notifications)) {
6035
            return false;
6036
        }
6037
6038
        $studentId = $exercise_stat_info['exe_user_id'];
6039
        $exerciseExtraFieldValue = new ExtraFieldValue('exercise');
6040
        $wrongAnswersCount = $stats['failed_answers_count'];
6041
        $exercisePassed = $stats['exercise_passed'];
6042
        $countPendingQuestions = $stats['count_pending_questions'];
6043
        $stats['all_answers_teacher_html'] = $statsTeacher['all_answers_html'];
6044
6045
        // If there are no pending questions (Open questions).
6046
        if (0 === $countPendingQuestions) {
6047
            /*$extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6048
                $objExercise->iId,
6049
                'signature_mandatory'
6050
            );
6051
6052
            if ($extraFieldData && isset($extraFieldData['value']) && 1 === (int) $extraFieldData['value']) {
6053
                if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($objExercise)) {
6054
                    $signature = ExerciseSignaturePlugin::getSignature($studentId, $exercise_stat_info);
6055
                    if (false !== $signature) {
6056
                        //return false;
6057
                    }
6058
                }
6059
            }*/
6060
6061
            // Notifications.
6062
            $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6063
                $objExercise->iId,
6064
                'notifications'
6065
            );
6066
            $exerciseNotification = '';
6067
            if ($extraFieldData && isset($extraFieldData['value'])) {
6068
                $exerciseNotification = $extraFieldData['value'];
6069
            }
6070
6071
            $subject = sprintf(get_lang('Failure on attempt %s at %s'), $attemptCountToSend, $courseInfo['title']);
6072
            if ($exercisePassed) {
6073
                $subject = sprintf(get_lang('Validation of exercise at %s'), $courseInfo['title']);
6074
            }
6075
6076
            if ($exercisePassed) {
6077
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6078
                    $objExercise->iId,
6079
                    'MailSuccess'
6080
                );
6081
            } else {
6082
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6083
                    $objExercise->iId,
6084
                    'MailAttempt'.$attemptCountToSend
6085
                );
6086
            }
6087
6088
            // Blocking exercise.
6089
            $blockPercentageExtra = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6090
                $objExercise->iId,
6091
                'blocking_percentage'
6092
            );
6093
            $blockPercentage = false;
6094
            if ($blockPercentageExtra && isset($blockPercentageExtra['value']) && $blockPercentageExtra['value']) {
6095
                $blockPercentage = $blockPercentageExtra['value'];
6096
            }
6097
            if ($blockPercentage) {
6098
                $passBlock = $stats['total_percentage'] > $blockPercentage;
6099
                if (false === $passBlock) {
6100
                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6101
                        $objExercise->iId,
6102
                        'MailIsBlockByPercentage'
6103
                    );
6104
                }
6105
            }
6106
6107
            $extraFieldValueUser = new ExtraFieldValue('user');
6108
6109
            if ($extraFieldData && isset($extraFieldData['value'])) {
6110
                $content = $extraFieldData['value'];
6111
                $content = self::parseContent($content, $stats, $objExercise, $exercise_stat_info, $studentId);
6112
                //if (false === $exercisePassed) {
6113
                if (0 !== $wrongAnswersCount) {
6114
                    $content .= $stats['failed_answers_html'];
6115
                }
6116
6117
                $sendMessage = true;
6118
                if (!empty($exerciseNotification)) {
6119
                    foreach ($notifications as $name => $notificationList) {
6120
                        if ($exerciseNotification !== $name) {
6121
                            continue;
6122
                        }
6123
                        foreach ($notificationList as $notificationName => $attemptData) {
6124
                            if ('student_check' === $notificationName) {
6125
                                $sendMsgIfInList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : '';
6126
                                if (!empty($sendMsgIfInList)) {
6127
                                    foreach ($sendMsgIfInList as $skipVariable => $skipValues) {
6128
                                        $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
6129
                                            $studentId,
6130
                                            $skipVariable
6131
                                        );
6132
6133
                                        if (empty($userExtraFieldValue)) {
6134
                                            $sendMessage = false;
6135
                                            break;
6136
                                        } else {
6137
                                            $sendMessage = false;
6138
                                            if (isset($userExtraFieldValue['value']) &&
6139
                                                in_array($userExtraFieldValue['value'], $skipValues)
6140
                                            ) {
6141
                                                $sendMessage = true;
6142
                                                break;
6143
                                            }
6144
                                        }
6145
                                    }
6146
                                }
6147
                                break;
6148
                            }
6149
                        }
6150
                    }
6151
                }
6152
6153
                // Send to student.
6154
                if ($sendMessage) {
6155
                    MessageManager::send_message($currentUserId, $subject, $content);
6156
                }
6157
            }
6158
6159
            if (!empty($exerciseNotification)) {
6160
                foreach ($notifications as $name => $notificationList) {
6161
                    if ($exerciseNotification !== $name) {
6162
                        continue;
6163
                    }
6164
                    foreach ($notificationList as $attemptData) {
6165
                        $skipNotification = false;
6166
                        $skipNotificationList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : [];
6167
                        if (!empty($skipNotificationList)) {
6168
                            foreach ($skipNotificationList as $skipVariable => $skipValues) {
6169
                                $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
6170
                                    $studentId,
6171
                                    $skipVariable
6172
                                );
6173
6174
                                if (empty($userExtraFieldValue)) {
6175
                                    $skipNotification = true;
6176
                                    break;
6177
                                } else {
6178
                                    if (isset($userExtraFieldValue['value'])) {
6179
                                        if (!in_array($userExtraFieldValue['value'], $skipValues)) {
6180
                                            $skipNotification = true;
6181
                                            break;
6182
                                        }
6183
                                    } else {
6184
                                        $skipNotification = true;
6185
                                        break;
6186
                                    }
6187
                                }
6188
                            }
6189
                        }
6190
6191
                        if ($skipNotification) {
6192
                            continue;
6193
                        }
6194
6195
                        $email = isset($attemptData['email']) ? $attemptData['email'] : '';
6196
                        $emailList = explode(',', $email);
6197
                        if (empty($emailList)) {
6198
                            continue;
6199
                        }
6200
                        $attempts = isset($attemptData['attempts']) ? $attemptData['attempts'] : [];
6201
                        foreach ($attempts as $attempt) {
6202
                            $sendMessage = false;
6203
                            if (isset($attempt['attempt']) && $attemptCountToSend !== (int) $attempt['attempt']) {
6204
                                continue;
6205
                            }
6206
6207
                            if (!isset($attempt['status'])) {
6208
                                continue;
6209
                            }
6210
6211
                            if ($blockPercentage && isset($attempt['is_block_by_percentage'])) {
6212
                                if ($attempt['is_block_by_percentage']) {
6213
                                    if ($passBlock) {
6214
                                        continue;
6215
                                    }
6216
                                } else {
6217
                                    if (false === $passBlock) {
6218
                                        continue;
6219
                                    }
6220
                                }
6221
                            }
6222
6223
                            switch ($attempt['status']) {
6224
                                case 'passed':
6225
                                    if ($exercisePassed) {
6226
                                        $sendMessage = true;
6227
                                    }
6228
                                    break;
6229
                                case 'failed':
6230
                                    if (false === $exercisePassed) {
6231
                                        $sendMessage = true;
6232
                                    }
6233
                                    break;
6234
                                case 'all':
6235
                                    $sendMessage = true;
6236
                                    break;
6237
                            }
6238
6239
                            if ($sendMessage) {
6240
                                $attachments = [];
6241
                                if (isset($attempt['add_pdf']) && $attempt['add_pdf']) {
6242
                                    // Get pdf content
6243
                                    $pdfExtraData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6244
                                        $objExercise->iId,
6245
                                        $attempt['add_pdf']
6246
                                    );
6247
6248
                                    if ($pdfExtraData && isset($pdfExtraData['value'])) {
6249
                                        $pdfContent = self::parseContent(
6250
                                            $pdfExtraData['value'],
6251
                                            $stats,
6252
                                            $objExercise,
6253
                                            $exercise_stat_info,
6254
                                            $studentId
6255
                                        );
6256
6257
                                        @$pdf = new PDF();
6258
                                        $filename = get_lang('Test');
6259
                                        $pdfPath = @$pdf->content_to_pdf(
6260
                                            "<html><body>$pdfContent</body></html>",
6261
                                            null,
6262
                                            $filename,
6263
                                            api_get_course_id(),
6264
                                            'F',
6265
                                            false,
6266
                                            null,
6267
                                            false,
6268
                                            true
6269
                                        );
6270
                                        $attachments[] = ['filename' => $filename, 'path' => $pdfPath];
6271
                                    }
6272
                                }
6273
6274
                                $content = isset($attempt['content_default']) ? $attempt['content_default'] : '';
6275
                                if (isset($attempt['content'])) {
6276
                                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6277
                                        $objExercise->iId,
6278
                                        $attempt['content']
6279
                                    );
6280
                                    if ($extraFieldData && isset($extraFieldData['value']) && !empty($extraFieldData['value'])) {
6281
                                        $content = $extraFieldData['value'];
6282
                                    }
6283
                                }
6284
6285
                                if (!empty($content)) {
6286
                                    $content = self::parseContent(
6287
                                        $content,
6288
                                        $stats,
6289
                                        $objExercise,
6290
                                        $exercise_stat_info,
6291
                                        $studentId
6292
                                    );
6293
                                    foreach ($emailList as $email) {
6294
                                        if (empty($email)) {
6295
                                            continue;
6296
                                        }
6297
                                        api_mail_html(
6298
                                            null,
6299
                                            $email,
6300
                                            $subject,
6301
                                            $content,
6302
                                            null,
6303
                                            null,
6304
                                            [],
6305
                                            $attachments
6306
                                        );
6307
                                    }
6308
                                }
6309
6310
                                if (isset($attempt['post_actions'])) {
6311
                                    foreach ($attempt['post_actions'] as $action => $params) {
6312
                                        switch ($action) {
6313
                                            case 'subscribe_student_to_courses':
6314
                                                foreach ($params as $code) {
6315
                                                    $courseInfo = api_get_course_info($code);
6316
                                                    CourseManager::subscribeUser(
6317
                                                        $currentUserId,
6318
                                                        $courseInfo['real_id']
6319
                                                    );
6320
                                                    break;
6321
                                                }
6322
                                                break;
6323
                                        }
6324
                                    }
6325
                                }
6326
                            }
6327
                        }
6328
                    }
6329
                }
6330
            }
6331
        }
6332
    }
6333
6334
    /**
6335
     * Delete an exercise attempt.
6336
     *
6337
     * Log the exe_id deleted with the exe_user_id related.
6338
     *
6339
     * @param int $exeId
6340
     */
6341
    public static function deleteExerciseAttempt($exeId)
6342
    {
6343
        $exeId = (int) $exeId;
6344
6345
        $trackExerciseInfo = self::get_exercise_track_exercise_info($exeId);
6346
6347
        if (empty($trackExerciseInfo)) {
6348
            return;
6349
        }
6350
6351
        $tblTrackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6352
        $tblTrackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
6353
6354
        Database::query("DELETE FROM $tblTrackAttempt WHERE exe_id = $exeId");
6355
        Database::query("DELETE FROM $tblTrackExercises WHERE exe_id = $exeId");
6356
6357
        Event::addEvent(
6358
            LOG_EXERCISE_ATTEMPT_DELETE,
6359
            LOG_EXERCISE_ATTEMPT,
6360
            $exeId,
6361
            api_get_utc_datetime()
6362
        );
6363
        Event::addEvent(
6364
            LOG_EXERCISE_ATTEMPT_DELETE,
6365
            LOG_EXERCISE_AND_USER_ID,
6366
            $exeId.'-'.$trackExerciseInfo['exe_user_id'],
6367
            api_get_utc_datetime()
6368
        );
6369
    }
6370
6371
    public static function scorePassed($score, $total)
6372
    {
6373
        $compareResult = bccomp($score, $total, 3);
6374
        $scorePassed = 1 === $compareResult || 0 === $compareResult;
6375
        if (false === $scorePassed) {
6376
            $epsilon = 0.00001;
6377
            if (abs($score - $total) < $epsilon) {
6378
                $scorePassed = true;
6379
            }
6380
        }
6381
6382
        return $scorePassed;
6383
    }
6384
6385
    /**
6386
     * Returns the HTML for a specific exercise attempt, ready for PDF generation.
6387
     */
6388
    public static function getAttemptPdfHtml(int $exeId, int $courseId, int $sessionId): string
6389
    {
6390
        $_GET = [
6391
            'id'           => $exeId,
6392
            'action'       => 'export',
6393
            'export_type'  => 'all_results',
6394
            'cid'          => $courseId,
6395
            'sid'          => $sessionId,
6396
            'gid'          => 0,
6397
            'gradebook'    => 0,
6398
            'origin'       => '',
6399
        ];
6400
        $_REQUEST = $_GET + $_REQUEST;
6401
6402
        ob_start();
6403
        include __DIR__ . '/../../exercise/exercise_show.php';
6404
        return ob_get_clean();
6405
    }
6406
6407
    /**
6408
     * Generates and saves a PDF for a single exercise attempt
6409
     */
6410
    public static function saveFileExerciseResultPdfDirect(
6411
        int    $exeId,
6412
        int    $courseId,
6413
        int    $sessionId,
6414
        string $exportFolderPath
6415
    ): void {
6416
        // 1) Retrieve the HTML for this attempt and convert it to PDF
6417
        $html = self::getAttemptPdfHtml($exeId, $courseId, $sessionId);
6418
6419
        // 2) Determine filename and path based on user information
6420
        $track   = self::get_exercise_track_exercise_info($exeId);
6421
        $userId  = $track['exe_user_id'] ?? 0;
6422
        $user    = api_get_user_info($userId);
6423
        $pdfName = api_replace_dangerous_char(
6424
            ($user['firstname'] ?? 'user') . '_' .
6425
            ($user['lastname']  ?? 'unknown') .
6426
            '_attempt' . $exeId . '.pdf'
6427
        );
6428
        $filePath = rtrim($exportFolderPath, '/') . '/' . $pdfName;
6429
6430
        // 3) Ensure the directory exists
6431
        $dir = dirname($filePath);
6432
        if (!is_dir($dir)) {
6433
            mkdir($dir, 0755, true);
6434
        }
6435
6436
        // 4) Use Chamilo's PDF class to generate and save the file
6437
        $params = [
6438
            'filename'    => $pdfName,
6439
            'course_code' => api_get_course_id(),
6440
        ];
6441
        $pdf = new PDF('A4', 'P', $params);
6442
        $pdf->html_to_pdf_with_template(
6443
            $html,
6444
            true,
6445
            false,
6446
            true,
6447
            [],
6448
            'F',
6449
            $filePath
6450
        );
6451
    }
6452
6453
    /**
6454
     * Exports all results of an exercise to a ZIP archive by generating PDFs on disk and then sending the ZIP to the browser.
6455
     */
6456
    public static function exportExerciseAllResultsZip(
6457
        int   $sessionId,
6458
        int   $courseId,
6459
        int   $exerciseId,
6460
        array $filterDates = [],
6461
        string $mainPath    = ''
6462
    ) {
6463
        // 1) Retrieve all attempt records for this exercise
6464
        $exerciseObj = new Exercise($courseId);
6465
        $results     = $exerciseObj->getExerciseAndResult($courseId, $sessionId, $exerciseId);
6466
        if (empty($results)) {
6467
            Display::addFlash(
6468
                Display::return_message(
6469
                    get_lang('No result found for export in this test.'),
6470
                    'warning',
6471
                    false
6472
                )
6473
            );
6474
            return false;
6475
        }
6476
6477
        // 2) Prepare a temporary folder for the PDFs
6478
        $exportName       = 'S' . $sessionId . '-C' . $courseId . '-T' . $exerciseId;
6479
        $baseDir          = api_get_path(SYS_ARCHIVE_PATH);
6480
        $exportFolderPath = $baseDir . 'pdfexport-' . $exportName;
6481
        if (is_dir($exportFolderPath)) {
6482
            rmdirr($exportFolderPath);
6483
        }
6484
        mkdir($exportFolderPath, 0755, true);
6485
6486
        // 3) Generate a PDF for each attempt
6487
        foreach ($results as $row) {
6488
            $exeId = (int) $row['exe_id'];
6489
            self::saveFileExerciseResultPdfDirect(
6490
                $exeId,
6491
                $courseId,
6492
                $sessionId,
6493
                $exportFolderPath
6494
            );
6495
        }
6496
6497
        // 4) Create the ZIP archive containing all generated PDFs
6498
        $zipFilePath = $baseDir . 'pdfexport-' . $exportName . '.zip';
6499
        $zip = new \ZipArchive();
6500
        if ($zip->open($zipFilePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
6501
            throw new \Exception('Failed to create ZIP file');
6502
        }
6503
        $files = new RecursiveIteratorIterator(
6504
            new RecursiveDirectoryIterator($exportFolderPath),
6505
            RecursiveIteratorIterator::LEAVES_ONLY
6506
        );
6507
        foreach ($files as $file) {
6508
            if (!$file->isDir()) {
6509
                $filePath     = $file->getRealPath();
6510
                $relativePath = substr($filePath, strlen($exportFolderPath) + 1);
6511
                $zip->addFile($filePath, $relativePath);
6512
            }
6513
        }
6514
        $zip->close();
6515
        rmdirr($exportFolderPath);
6516
6517
        // 5) Send the ZIP file to the browser or move it to mainPath
6518
        if (!empty($mainPath)) {
6519
            @rename($zipFilePath, $mainPath . '/pdfexport-' . $exportName . '.zip');
6520
        } else {
6521
            // close session and clear output buffers
6522
            session_write_close();
6523
            while (ob_get_level()) {
6524
                @ob_end_clean();
6525
            }
6526
6527
            // send download headers
6528
            header('Content-Description: File Transfer');
6529
            header('Content-Type: application/zip');
6530
            header('Content-Disposition: attachment; filename="pdfexport-' . $exportName . '.zip"');
6531
            header('Content-Transfer-Encoding: binary');
6532
            header('Expires: 0');
6533
            header('Cache-Control: must-revalidate');
6534
            header('Pragma: public');
6535
            header('Content-Length: ' . filesize($zipFilePath));
6536
6537
            // output the file and remove it
6538
            readfile($zipFilePath);
6539
            @unlink($zipFilePath);
6540
            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...
6541
        }
6542
    }
6543
6544
    /**
6545
     * Calculates the overall score for Combination-type questions.
6546
     */
6547
    public static function getUserQuestionScoreGlobal(
6548
        int   $answerType,
6549
        array $listCorrectAnswers,
6550
        int   $exeId,
6551
        int   $questionId,
6552
        float $questionWeighting,
6553
        array $choice = [],
6554
        int $nbrAnswers = 0
6555
    ): float
6556
    {
6557
        $nbrCorrect = 0;
6558
        $nbrOptions = 0;
6559
        $choice = is_array($choice) ? $choice : [];
6560
        switch ($answerType) {
6561
            case FILL_IN_BLANKS_COMBINATION:
6562
                if (!empty($listCorrectAnswers)) {
6563
                    if (!empty($listCorrectAnswers['student_score']) && is_array($listCorrectAnswers['student_score'])) {
6564
                        foreach ($listCorrectAnswers['student_score'] as $val) {
6565
                            if ((int) $val === 1) {
6566
                                $nbrCorrect++;
6567
                            }
6568
                        }
6569
                    }
6570
                    if (!empty($listCorrectAnswers['words_count'])) {
6571
                        $nbrOptions = (int) $listCorrectAnswers['words_count'];
6572
                    } elseif (!empty($listCorrectAnswers['words']) && is_array($listCorrectAnswers['words'])) {
6573
                        $nbrOptions = count($listCorrectAnswers['words']);
6574
                    }
6575
                }
6576
                break;
6577
6578
            case HOT_SPOT_COMBINATION:
6579
                if (!empty($listCorrectAnswers) && is_array($listCorrectAnswers) && is_array($choice)) {
6580
                    foreach ($listCorrectAnswers as $idx => $val) {
6581
                        if (isset($choice[$idx]) && (int) $choice[$idx] === 1) {
6582
                            $nbrCorrect++;
6583
                        }
6584
                    }
6585
                } else {
6586
                    $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
6587
                    $exeIdEsc = Database::escape_string($exeId);
6588
                    $qIdEsc   = Database::escape_string($questionId);
6589
                    $sql = "SELECT COUNT(hotspot_id) AS ct
6590
                        FROM $TBL_TRACK_HOTSPOT
6591
                        WHERE hotspot_exe_id = '$exeIdEsc'
6592
                          AND hotspot_question_id = '$qIdEsc'
6593
                          AND hotspot_correct = 1";
6594
                    $result = Database::query($sql);
6595
                    $nbrCorrect = (int) Database::result($result, 0, 0);
6596
                }
6597
                $nbrOptions = (int) $nbrAnswers;
6598
                break;
6599
6600
            case MATCHING_COMBINATION:
6601
            case MATCHING_DRAGGABLE_COMBINATION:
6602
                if (isset($listCorrectAnswers['form_values'])) {
6603
                    if (isset($listCorrectAnswers['form_values']['correct'])) {
6604
                        $nbrCorrect = count($listCorrectAnswers['form_values']['correct']);
6605
                        $nbrOptions = (int) $listCorrectAnswers['form_values']['count_options'];
6606
                    }
6607
                } else {
6608
                    if (isset($listCorrectAnswers['from_database'])) {
6609
                        if (isset($listCorrectAnswers['from_database']['correct'])) {
6610
                            $nbrCorrect = count($listCorrectAnswers['from_database']['correct']);
6611
                            $nbrOptions = (int) $listCorrectAnswers['from_database']['count_options'];
6612
                        }
6613
                    }
6614
                }
6615
                break;
6616
        }
6617
6618
        $questionScore = 0.0;
6619
        if ($nbrOptions > 0 && $nbrCorrect === $nbrOptions) {
6620
            $questionScore = (float) $questionWeighting;
6621
        }
6622
6623
        return $questionScore;
6624
    }
6625
}
6626