Passed
Pull Request — master (#6637)
by
unknown
14:37 queued 06:29
created

ExerciseLib::get_number_students_answer_count()   D

Complexity

Conditions 18

Size

Total Lines 97
Code Lines 65

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 18
eloc 65
nop 8
dl 0
loc 97
rs 4.8666
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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