ExerciseLib::exerciseResultsInRanking()   B
last analyzed

Complexity

Conditions 11

Size

Total Lines 62
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

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