Passed
Pull Request — master (#5551)
by
unknown
07:24
created

ExerciseLib::getOralFileAudio()   B

Complexity

Conditions 8

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 8
eloc 22
c 2
b 0
f 0
nop 3
dl 0
loc 39
rs 8.4444
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
6
use Chamilo\CoreBundle\Component\Utils\ActionIcon;
7
use Chamilo\CoreBundle\Entity\Asset;
8
use Chamilo\CoreBundle\Entity\GradebookCategory;
9
use Chamilo\CoreBundle\Entity\TrackEExercise;
10
use Chamilo\CoreBundle\Framework\Container;
11
use Chamilo\CourseBundle\Entity\CQuiz;
12
use ChamiloSession as Session;
13
14
/**
15
 * Class ExerciseLib
16
 * shows a question and its answers.
17
 *
18
 * @author Olivier Brouckaert <[email protected]>
19
 * @author Hubert Borderiou 2011-10-21
20
 * @author ivantcholakov2009-07-20
21
 * @author Julio Montoya
22
 */
23
class ExerciseLib
24
{
25
    /**
26
     * Shows a question.
27
     *
28
     * @param Exercise $exercise
29
     * @param int      $questionId     $questionId question id
30
     * @param bool     $only_questions if true only show the questions, no exercise title
31
     * @param bool     $origin         i.e = learnpath
32
     * @param string   $current_item   current item from the list of questions
33
     * @param bool     $show_title
34
     * @param bool     $freeze
35
     * @param array    $user_choice
36
     * @param bool     $show_comment
37
     * @param bool     $show_answers
38
     *
39
     * @throws \Exception
40
     *
41
     * @return bool|int
42
     */
43
    public static function showQuestion(
44
        $exercise,
45
        $questionId,
46
        $only_questions = false,
47
        $origin = false,
48
        $current_item = '',
49
        $show_title = true,
50
        $freeze = false,
51
        $user_choice = [],
52
        $show_comment = false,
53
        $show_answers = false,
54
        $show_icon = false
55
    ) {
56
        $course_id = $exercise->course_id;
57
        $exerciseId = $exercise->iId;
58
59
        if (empty($course_id)) {
60
            return '';
61
        }
62
        $course = $exercise->course;
63
64
        // Change false to true in the following line to enable answer hinting
65
        $debug_mark_answer = $show_answers;
66
        // Reads question information
67
        if (!$objQuestionTmp = Question::read($questionId, $course)) {
68
            // Question not found
69
            return false;
70
        }
71
72
        if (EXERCISE_FEEDBACK_TYPE_END != $exercise->getFeedbackType()) {
73
            $show_comment = false;
74
        }
75
76
        $answerType = $objQuestionTmp->selectType();
77
        $s = '';
78
        if (HOT_SPOT != $answerType &&
79
            HOT_SPOT_DELINEATION != $answerType &&
80
            ANNOTATION != $answerType
81
        ) {
82
            // Question is not a hotspot
83
            if (!$only_questions) {
84
                $questionDescription = $objQuestionTmp->selectDescription();
85
                if ($show_title) {
86
                    if ($exercise->display_category_name) {
87
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->id);
88
                    }
89
                    $titleToDisplay = $objQuestionTmp->getTitleToDisplay($exercise, $current_item);
90
                    if (READING_COMPREHENSION == $answerType) {
91
                        // In READING_COMPREHENSION, the title of the question
92
                        // contains the question itself, which can only be
93
                        // shown at the end of the given time, so hide for now
94
                        $titleToDisplay = Display::div(
95
                            $current_item.'. '.get_lang('Reading comprehension'),
96
                            ['class' => 'question_title']
97
                        );
98
                    }
99
                    echo $titleToDisplay;
100
                }
101
102
                if (!empty($questionDescription) && READING_COMPREHENSION != $answerType) {
103
                    echo Display::div(
104
                        $questionDescription,
105
                        ['class' => 'question_description wysiwyg']
106
                    );
107
                }
108
            }
109
110
            if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION]) && $freeze) {
111
                return '';
112
            }
113
114
            echo '<div class="question_options type-'.$answerType.'">';
115
            // construction of the Answer object (also gets all answers details)
116
            $objAnswerTmp = new Answer($questionId, $course_id, $exercise);
117
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
118
            $quizQuestionOptions = Question::readQuestionOption($questionId, $course_id);
119
120
            // For "matching" type here, we need something a little bit special
121
            // because the match between the suggestions and the answers cannot be
122
            // done easily (suggestions and answers are in the same table), so we
123
            // have to go through answers first (elems with "correct" value to 0).
124
            $select_items = [];
125
            //This will contain the number of answers on the left side. We call them
126
            // suggestions here, for the sake of comprehensions, while the ones
127
            // on the right side are called answers
128
            $num_suggestions = 0;
129
            switch ($answerType) {
130
                case MATCHING:
131
                case DRAGGABLE:
132
                case MATCHING_DRAGGABLE:
133
                    if (DRAGGABLE == $answerType) {
134
                        $isVertical = 'v' === $objQuestionTmp->extra;
135
                        $s .= '<div><p class="small">'
136
                            .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.')
137
                            .'</p><ul class="exercise-draggable-answer '.($isVertical ? '' : 'list-inline')
138
                            .'" id="question-'.$questionId.'" data-question="'.$questionId.'">';
139
                    } else {
140
                        $s .= '<div id="drag'.$questionId.'_question" class="drag_question">
141
                               <table class="table table-hover table-striped data_table">';
142
                    }
143
144
                    // Iterate through answers.
145
                    $x = 1;
146
                    // Mark letters for each answer.
147
                    $letter = 'A';
148
                    $answer_matching = [];
149
                    $cpt1 = [];
150
                    for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
151
                        $answerCorrect = $objAnswerTmp->isCorrect($answerId);
152
                        $numAnswer = $objAnswerTmp->selectAutoId($answerId);
153
                        if (0 == $answerCorrect) {
154
                            // options (A, B, C, ...) that will be put into the list-box
155
                            // have the "correct" field set to 0 because they are answer
156
                            $cpt1[$x] = $letter;
157
                            $answer_matching[$x] = $objAnswerTmp->selectAnswerByAutoId($numAnswer);
158
                            $x++;
159
                            $letter++;
160
                        }
161
                    }
162
163
                    $i = 1;
164
                    $select_items[0]['id'] = 0;
165
                    $select_items[0]['letter'] = '--';
166
                    $select_items[0]['answer'] = '';
167
                    foreach ($answer_matching as $id => $value) {
168
                        $select_items[$i]['id'] = $value['iid'];
169
                        $select_items[$i]['letter'] = $cpt1[$id];
170
                        $select_items[$i]['answer'] = $value['answer'];
171
                        $i++;
172
                    }
173
174
                    $user_choice_array_position = [];
175
                    if (!empty($user_choice)) {
176
                        foreach ($user_choice as $item) {
177
                            $user_choice_array_position[$item['position']] = $item['answer'];
178
                        }
179
                    }
180
                    $num_suggestions = ($nbrAnswers - $x) + 1;
181
                    break;
182
                case FREE_ANSWER:
183
                    $fck_content = isset($user_choice[0]) && !empty($user_choice[0]['answer']) ? $user_choice[0]['answer'] : null;
184
                    $form = new FormValidator('free_choice_'.$questionId);
185
                    $config = [
186
                        'ToolbarSet' => 'TestFreeAnswer',
187
                    ];
188
                    $form->addHtmlEditor(
189
                        'choice['.$questionId.']',
190
                        null,
191
                        false,
192
                        false,
193
                        $config
194
                    );
195
                    $form->setDefaults(["choice[".$questionId."]" => $fck_content]);
196
                    $s .= $form->returnForm();
197
                    break;
198
                case ORAL_EXPRESSION:
199
                    // Add nanog
200
                    if ('true' === api_get_setting('enable_record_audio')) {
201
                        //@todo pass this as a parameter
202
                        global $exercise_stat_info;
203
                        if (!empty($exercise_stat_info)) {
204
                            echo $objQuestionTmp->returnRecorder((int) $exercise_stat_info['exe_id']);
205
                            $generatedFile = self::getOralFileAudio($exercise_stat_info['exe_id'], $questionId);
206
                            if (!empty($generatedFile)) {
207
                                echo $generatedFile;
208
                            }
209
                        }
210
                    }
211
212
                    $form = new FormValidator('free_choice_'.$questionId);
213
                    $config = ['ToolbarSet' => 'TestFreeAnswer'];
214
215
                    $form->addHtml('<div id="'.'hide_description_'.$questionId.'_options" style="display: none;">');
216
                    $form->addHtmlEditor(
217
                        "choice[$questionId]",
218
                        null,
219
                        false,
220
                        false,
221
                        $config
222
                    );
223
                    $form->addHtml('</div>');
224
                    $s .= $form->returnForm();
225
                    break;
226
            }
227
228
            // Now navigate through the possible answers, using the max number of
229
            // answers for the question as a limiter
230
            $lines_count = 1; // a counter for matching-type answers
231
            if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
232
                MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
233
            ) {
234
                $header = Display::tag('th', get_lang('Options'));
235
                foreach ($objQuestionTmp->options as $item) {
236
                    if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
237
                        if (in_array($item, $objQuestionTmp->options)) {
238
                            $header .= Display::tag('th', get_lang($item));
239
                        } else {
240
                            $header .= Display::tag('th', $item);
241
                        }
242
                    } else {
243
                        $header .= Display::tag('th', $item);
244
                    }
245
                }
246
                if ($show_comment) {
247
                    $header .= Display::tag('th', get_lang('Feedback'));
248
                }
249
                $s .= '<table class="table table-hover table-striped">';
250
                $s .= Display::tag(
251
                    'tr',
252
                    $header,
253
                    ['style' => 'text-align:left;']
254
                );
255
            } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
256
                $header = Display::tag('th', get_lang('Options'), ['width' => '50%']);
257
                echo "
258
                <script>
259
                    function RadioValidator(question_id, answer_id)
260
                    {
261
                        var ShowAlert = '';
262
                        var typeRadioB = '';
263
                        var AllFormElements = window.document.getElementById('exercise_form').elements;
264
265
                        for (i = 0; i < AllFormElements.length; i++) {
266
                            if (AllFormElements[i].type == 'radio') {
267
                                var ThisRadio = AllFormElements[i].name;
268
                                var ThisChecked = 'No';
269
                                var AllRadioOptions = document.getElementsByName(ThisRadio);
270
271
                                for (x = 0; x < AllRadioOptions.length; x++) {
272
                                     if (AllRadioOptions[x].checked && ThisChecked == 'No') {
273
                                         ThisChecked = 'Yes';
274
                                         break;
275
                                     }
276
                                }
277
278
                                var AlreadySearched = ShowAlert.indexOf(ThisRadio);
279
                                if (ThisChecked == 'No' && AlreadySearched == -1) {
280
                                    ShowAlert = ShowAlert + ThisRadio;
281
                                }
282
                            }
283
                        }
284
                        if (ShowAlert != '') {
285
286
                        } else {
287
                            $('.question-validate-btn').removeAttr('disabled');
288
                        }
289
                    }
290
291
                    function handleRadioRow(event, question_id, answer_id) {
292
                        var t = event.target;
293
                        if (t && t.tagName == 'INPUT')
294
                            return;
295
                        while (t && t.tagName != 'TD') {
296
                            t = t.parentElement;
297
                        }
298
                        var r = t.getElementsByTagName('INPUT')[0];
299
                        r.click();
300
                        RadioValidator(question_id, answer_id);
301
                    }
302
303
                    $(function() {
304
                        var ShowAlert = '';
305
                        var typeRadioB = '';
306
                        var question_id = $('input[name=question_id]').val();
307
                        var AllFormElements = window.document.getElementById('exercise_form').elements;
308
309
                        for (i = 0; i < AllFormElements.length; i++) {
310
                            if (AllFormElements[i].type == 'radio') {
311
                                var ThisRadio = AllFormElements[i].name;
312
                                var ThisChecked = 'No';
313
                                var AllRadioOptions = document.getElementsByName(ThisRadio);
314
315
                                for (x = 0; x < AllRadioOptions.length; x++) {
316
                                    if (AllRadioOptions[x].checked && ThisChecked == 'No') {
317
                                        ThisChecked = \"Yes\";
318
                                        break;
319
                                    }
320
                                }
321
322
                                var AlreadySearched = ShowAlert.indexOf(ThisRadio);
323
                                if (ThisChecked == 'No' && AlreadySearched == -1) {
324
                                    ShowAlert = ShowAlert + ThisRadio;
325
                                }
326
                            }
327
                        }
328
329
                        if (ShowAlert != '') {
330
                             $('.question-validate-btn').attr('disabled', 'disabled');
331
                        } else {
332
                            $('.question-validate-btn').removeAttr('disabled');
333
                        }
334
                    });
335
                </script>";
336
337
                foreach ($objQuestionTmp->optionsTitle as $item) {
338
                    if (in_array($item, $objQuestionTmp->optionsTitle)) {
339
                        $properties = [];
340
                        if ('Answers' === $item) {
341
                            $properties['colspan'] = 2;
342
                            $properties['style'] = 'background-color: #F56B2A; color: #ffffff;';
343
                        } elseif ('DegreeOfCertaintyThatMyAnswerIsCorrect' === $item) {
344
                            $properties['colspan'] = 6;
345
                            $properties['style'] = 'background-color: #330066; color: #ffffff;';
346
                        }
347
                        $header .= Display::tag('th', get_lang($item), $properties);
348
                    } else {
349
                        $header .= Display::tag('th', $item);
350
                    }
351
                }
352
353
                if ($show_comment) {
354
                    $header .= Display::tag('th', get_lang('Feedback'));
355
                }
356
357
                $s .= '<table class="table table-hover table-striped data_table">';
358
                $s .= Display::tag('tr', $header, ['style' => 'text-align:left;']);
359
360
                // ajout de la 2eme ligne d'entête pour true/falss et les pourcentages de certitude
361
                $header1 = Display::tag('th', '&nbsp;');
362
                $cpt1 = 0;
363
                foreach ($objQuestionTmp->options as $item) {
364
                    $colorBorder1 = ($cpt1 == (count($objQuestionTmp->options) - 1))
365
                        ? '' : 'border-right: solid #FFFFFF 1px;';
366
                    if ('True' === $item || 'False' === $item) {
367
                        $header1 .= Display::tag(
368
                            'th',
369
                            get_lang($item),
370
                            ['style' => 'background-color: #F7C9B4; color: black;'.$colorBorder1]
371
                        );
372
                    } else {
373
                        $header1 .= Display::tag(
374
                            'th',
375
                            $item,
376
                            ['style' => 'background-color: #e6e6ff; color: black;padding:5px; '.$colorBorder1]
377
                        );
378
                    }
379
                    $cpt1++;
380
                }
381
                if ($show_comment) {
382
                    $header1 .= Display::tag('th', '&nbsp;');
383
                }
384
385
                $s .= Display::tag('tr', $header1);
386
387
                // add explanation
388
                $header2 = Display::tag('th', '&nbsp;');
389
                $descriptionList = [
390
                    get_lang('I don\'t know the answer and I\'ve picked at random'),
391
                    get_lang('I am very unsure'),
392
                    get_lang('I am unsure'),
393
                    get_lang('I am pretty sure'),
394
                    get_lang('I am almost 100% sure'),
395
                    get_lang('I am totally sure'),
396
                ];
397
                $counter2 = 0;
398
                foreach ($objQuestionTmp->options as $item) {
399
                    if ('True' === $item || 'False' === $item) {
400
                        $header2 .= Display::tag('td',
401
                            '&nbsp;',
402
                            ['style' => 'background-color: #F7E1D7; color: black;border-right: solid #FFFFFF 1px;']);
403
                    } else {
404
                        $color_border2 = ($counter2 == (count($objQuestionTmp->options) - 1)) ?
405
                            '' : 'border-right: solid #FFFFFF 1px;font-size:11px;';
406
                        $header2 .= Display::tag(
407
                            'td',
408
                            nl2br($descriptionList[$counter2]),
409
                            ['style' => 'background-color: #EFEFFC; color: black; width: 110px; text-align:center;
410
                                vertical-align: top; padding:5px; '.$color_border2]);
411
                        $counter2++;
412
                    }
413
                }
414
                if ($show_comment) {
415
                    $header2 .= Display::tag('th', '&nbsp;');
416
                }
417
                $s .= Display::tag('tr', $header2);
418
            }
419
420
            if ($show_comment) {
421
                if (in_array(
422
                    $answerType,
423
                    [
424
                        MULTIPLE_ANSWER,
425
                        MULTIPLE_ANSWER_COMBINATION,
426
                        UNIQUE_ANSWER,
427
                        UNIQUE_ANSWER_IMAGE,
428
                        UNIQUE_ANSWER_NO_OPTION,
429
                        GLOBAL_MULTIPLE_ANSWER,
430
                    ]
431
                )) {
432
                    $header = Display::tag('th', get_lang('Options'));
433
                    if (EXERCISE_FEEDBACK_TYPE_END == $exercise->getFeedbackType()) {
434
                        $header .= Display::tag('th', get_lang('Feedback'));
435
                    }
436
                    $s .= '<table class="table table-hover table-striped">';
437
                    $s .= Display::tag(
438
                        'tr',
439
                        $header,
440
                        ['style' => 'text-align:left;']
441
                    );
442
                }
443
            }
444
445
            $matching_correct_answer = 0;
446
            $userChoiceList = [];
447
            if (!empty($user_choice)) {
448
                foreach ($user_choice as $item) {
449
                    $userChoiceList[] = $item['answer'];
450
                }
451
            }
452
453
            $hidingClass = '';
454
            if (READING_COMPREHENSION == $answerType) {
455
                /** @var ReadingComprehension */
456
                $objQuestionTmp->setExerciseType($exercise->selectType());
457
                $objQuestionTmp->processText($objQuestionTmp->selectDescription());
458
                $hidingClass = 'hide-reading-answers';
459
                $s .= Display::div(
460
                    $objQuestionTmp->selectTitle(),
461
                    ['class' => 'question_title '.$hidingClass]
462
                );
463
            }
464
465
            for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
466
                $answer = $objAnswerTmp->selectAnswer($answerId);
467
                $answerCorrect = $objAnswerTmp->isCorrect($answerId);
468
                $numAnswer = $objAnswerTmp->selectAutoId($answerId);
469
                $comment = $objAnswerTmp->selectComment($answerId);
470
                $attributes = [];
471
472
                switch ($answerType) {
473
                    case UNIQUE_ANSWER:
474
                    case UNIQUE_ANSWER_NO_OPTION:
475
                    case UNIQUE_ANSWER_IMAGE:
476
                    case READING_COMPREHENSION:
477
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
478
                        if (isset($user_choice[0]['answer']) && $user_choice[0]['answer'] == $numAnswer) {
479
                            $attributes = [
480
                                'id' => $input_id,
481
                                'checked' => 1,
482
                                'selected' => 1,
483
                            ];
484
                        } else {
485
                            $attributes = ['id' => $input_id];
486
                        }
487
488
                        if ($debug_mark_answer) {
489
                            if ($answerCorrect) {
490
                                $attributes['checked'] = 1;
491
                                $attributes['selected'] = 1;
492
                            }
493
                        }
494
495
                        if ($show_comment) {
496
                            $s .= '<tr><td>';
497
                        }
498
499
                        if (UNIQUE_ANSWER_IMAGE == $answerType) {
500
                            if ($show_comment) {
501
                                if (empty($comment)) {
502
                                    $s .= '<div id="answer'.$questionId.$numAnswer.'"
503
                                            class="exercise-unique-answer-image text-center">';
504
                                } else {
505
                                    $s .= '<div id="answer'.$questionId.$numAnswer.'"
506
                                            class="exercise-unique-answer-image col-xs-6 col-sm-12 text-center">';
507
                                }
508
                            } else {
509
                                $s .= '<div id="answer'.$questionId.$numAnswer.'"
510
                                        class="exercise-unique-answer-image col-xs-6 col-md-3 text-center">';
511
                            }
512
                        }
513
514
                        if (UNIQUE_ANSWER_IMAGE != $answerType) {
515
                            $userStatus = STUDENT;
516
                            // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
517
                            // see BT#18242
518
                            if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
519
                                $userStatus = COURSEMANAGERLOWSECURITY;
520
                            }
521
                            $answer = Security::remove_XSS($answer, $userStatus);
522
                        }
523
                        $s .= Display::input(
524
                            'hidden',
525
                            'choice2['.$questionId.']',
526
                            '0'
527
                        );
528
529
                        $answer_input = null;
530
                        $attributes['class'] = 'checkradios';
531
                        if (UNIQUE_ANSWER_IMAGE == $answerType) {
532
                            $attributes['class'] = '';
533
                            $attributes['style'] = 'display: none;';
534
                            $answer = '<div class="thumbnail">'.$answer.'</div>';
535
                        }
536
537
                        $answer_input .= '<label class="radio '.$hidingClass.'">';
538
                        $answer_input .= Display::input(
539
                            'radio',
540
                            'choice['.$questionId.']',
541
                            $numAnswer,
542
                            $attributes
543
                        );
544
                        $answer_input .= $answer;
545
                        $answer_input .= '</label>';
546
547
                        if (UNIQUE_ANSWER_IMAGE == $answerType) {
548
                            $answer_input .= "</div>";
549
                        }
550
551
                        if ($show_comment) {
552
                            $s .= $answer_input;
553
                            $s .= '</td>';
554
                            $s .= '<td>';
555
                            $s .= $comment;
556
                            $s .= '</td>';
557
                            $s .= '</tr>';
558
                        } else {
559
                            $s .= $answer_input;
560
                        }
561
                        break;
562
                    case MULTIPLE_ANSWER:
563
                    case MULTIPLE_ANSWER_TRUE_FALSE:
564
                    case GLOBAL_MULTIPLE_ANSWER:
565
                    case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
566
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
567
                        $userStatus = STUDENT;
568
                        // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
569
                        // see BT#18242
570
                        if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
571
                            $userStatus = COURSEMANAGERLOWSECURITY;
572
                        }
573
                        $answer = Security::remove_XSS($answer, $userStatus);
574
575
                        if (in_array($numAnswer, $userChoiceList)) {
576
                            $attributes = [
577
                                'id' => $input_id,
578
                                'checked' => 1,
579
                                'selected' => 1,
580
                            ];
581
                        } else {
582
                            $attributes = ['id' => $input_id];
583
                        }
584
585
                        if ($debug_mark_answer) {
586
                            if ($answerCorrect) {
587
                                $attributes['checked'] = 1;
588
                                $attributes['selected'] = 1;
589
                            }
590
                        }
591
592
                        if (MULTIPLE_ANSWER == $answerType || GLOBAL_MULTIPLE_ANSWER == $answerType) {
593
                            $s .= '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
594
                            $attributes['class'] = 'checkradios';
595
                            $answer_input = '<label class="checkbox">';
596
                            $answer_input .= Display::input(
597
                                'checkbox',
598
                                'choice['.$questionId.']['.$numAnswer.']',
599
                                $numAnswer,
600
                                $attributes
601
                            );
602
                            $answer_input .= $answer;
603
                            $answer_input .= '</label>';
604
605
                            if ($show_comment) {
606
                                $s .= '<tr><td>';
607
                                $s .= $answer_input;
608
                                $s .= '</td>';
609
                                $s .= '<td>';
610
                                $s .= $comment;
611
                                $s .= '</td>';
612
                                $s .= '</tr>';
613
                            } else {
614
                                $s .= $answer_input;
615
                            }
616
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
617
                            $myChoice = [];
618
                            if (!empty($userChoiceList)) {
619
                                foreach ($userChoiceList as $item) {
620
                                    $item = explode(':', $item);
621
                                    if (!empty($item)) {
622
                                        $myChoice[$item[0]] = isset($item[1]) ? $item[1] : '';
623
                                    }
624
                                }
625
                            }
626
627
                            $s .= '<tr>';
628
                            $s .= Display::tag('td', $answer);
629
630
                            if (!empty($quizQuestionOptions)) {
631
                                $j = 1;
632
                                foreach ($quizQuestionOptions as $id => $item) {
633
                                    if (isset($myChoice[$numAnswer]) && $item['iid'] == $myChoice[$numAnswer]) {
634
                                        $attributes = [
635
                                            'checked' => 1,
636
                                            'selected' => 1,
637
                                        ];
638
                                    } else {
639
                                        $attributes = [];
640
                                    }
641
642
                                    if ($debug_mark_answer) {
643
                                        if ($j == $answerCorrect) {
644
                                            $attributes['checked'] = 1;
645
                                            $attributes['selected'] = 1;
646
                                        }
647
                                    }
648
                                    $s .= Display::tag(
649
                                        'td',
650
                                        Display::input(
651
                                            'radio',
652
                                            'choice['.$questionId.']['.$numAnswer.']',
653
                                            $item['iid'],
654
                                            $attributes
655
                                        ),
656
                                        ['style' => '']
657
                                    );
658
                                    $j++;
659
                                }
660
                            }
661
662
                            if ($show_comment) {
663
                                $s .= '<td>';
664
                                $s .= $comment;
665
                                $s .= '</td>';
666
                            }
667
                            $s .= '</tr>';
668
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
669
                            $myChoice = [];
670
                            if (!empty($userChoiceList)) {
671
                                foreach ($userChoiceList as $item) {
672
                                    $item = explode(':', $item);
673
                                    $myChoice[$item[0]] = $item[1];
674
                                }
675
                            }
676
                            $myChoiceDegreeCertainty = [];
677
                            if (!empty($userChoiceList)) {
678
                                foreach ($userChoiceList as $item) {
679
                                    $item = explode(':', $item);
680
                                    $myChoiceDegreeCertainty[$item[0]] = $item[2];
681
                                }
682
                            }
683
                            $s .= '<tr>';
684
                            $s .= Display::tag('td', $answer);
685
686
                            if (!empty($quizQuestionOptions)) {
687
                                $j = 1;
688
                                foreach ($quizQuestionOptions as $id => $item) {
689
                                    if (isset($myChoice[$numAnswer]) && $id == $myChoice[$numAnswer]) {
690
                                        $attributes = ['checked' => 1, 'selected' => 1];
691
                                    } else {
692
                                        $attributes = [];
693
                                    }
694
                                    $attributes['onChange'] = 'RadioValidator('.$questionId.', '.$numAnswer.')';
695
696
                                    // radio button selection
697
                                    if (isset($myChoiceDegreeCertainty[$numAnswer]) &&
698
                                        $id == $myChoiceDegreeCertainty[$numAnswer]
699
                                    ) {
700
                                        $attributes1 = ['checked' => 1, 'selected' => 1];
701
                                    } else {
702
                                        $attributes1 = [];
703
                                    }
704
705
                                    $attributes1['onChange'] = 'RadioValidator('.$questionId.', '.$numAnswer.')';
706
707
                                    if ($debug_mark_answer) {
708
                                        if ($j == $answerCorrect) {
709
                                            $attributes['checked'] = 1;
710
                                            $attributes['selected'] = 1;
711
                                        }
712
                                    }
713
714
                                    if ('True' == $item['name'] || 'False' == $item['name']) {
715
                                        $s .= Display::tag('td',
716
                                            Display::input('radio',
717
                                                'choice['.$questionId.']['.$numAnswer.']',
718
                                                $id,
719
                                                $attributes
720
                                            ),
721
                                            ['style' => 'text-align:center; background-color:#F7E1D7;',
722
                                                'onclick' => 'handleRadioRow(event, '.
723
                                                    $questionId.', '.
724
                                                    $numAnswer.')',
725
                                            ]
726
                                        );
727
                                    } else {
728
                                        $s .= Display::tag('td',
729
                                            Display::input('radio',
730
                                                'choiceDegreeCertainty['.$questionId.']['.$numAnswer.']',
731
                                                $id,
732
                                                $attributes1
733
                                            ),
734
                                            ['style' => 'text-align:center; background-color:#EFEFFC;',
735
                                                'onclick' => 'handleRadioRow(event, '.
736
                                                    $questionId.', '.
737
                                                    $numAnswer.')',
738
                                            ]
739
                                        );
740
                                    }
741
                                    $j++;
742
                                }
743
                            }
744
745
                            if ($show_comment) {
746
                                $s .= '<td>';
747
                                $s .= $comment;
748
                                $s .= '</td>';
749
                            }
750
                            $s .= '</tr>';
751
                        }
752
                        break;
753
                    case MULTIPLE_ANSWER_COMBINATION:
754
                        // multiple answers
755
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
756
757
                        if (in_array($numAnswer, $userChoiceList)) {
758
                            $attributes = [
759
                                'id' => $input_id,
760
                                'checked' => 1,
761
                                'selected' => 1,
762
                            ];
763
                        } else {
764
                            $attributes = ['id' => $input_id];
765
                        }
766
767
                        if ($debug_mark_answer) {
768
                            if ($answerCorrect) {
769
                                $attributes['checked'] = 1;
770
                                $attributes['selected'] = 1;
771
                            }
772
                        }
773
774
                        $userStatus = STUDENT;
775
                        // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
776
                        // see BT#18242
777
                        if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
778
                            $userStatus = COURSEMANAGERLOWSECURITY;
779
                        }
780
                        $answer = Security::remove_XSS($answer, $userStatus);
781
                        $answer_input = '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
782
                        $answer_input .= '<label class="checkbox">';
783
                        $answer_input .= Display::input(
784
                            'checkbox',
785
                            'choice['.$questionId.']['.$numAnswer.']',
786
                            1,
787
                            $attributes
788
                        );
789
                        $answer_input .= $answer;
790
                        $answer_input .= '</label>';
791
792
                        if ($show_comment) {
793
                            $s .= '<tr>';
794
                            $s .= '<td>';
795
                            $s .= $answer_input;
796
                            $s .= '</td>';
797
                            $s .= '<td>';
798
                            $s .= $comment;
799
                            $s .= '</td>';
800
                            $s .= '</tr>';
801
                        } else {
802
                            $s .= $answer_input;
803
                        }
804
                        break;
805
                    case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
806
                        $s .= '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
807
                        $myChoice = [];
808
                        if (!empty($userChoiceList)) {
809
                            foreach ($userChoiceList as $item) {
810
                                $item = explode(':', $item);
811
                                if (isset($item[1]) && isset($item[0])) {
812
                                    $myChoice[$item[0]] = $item[1];
813
                                }
814
                            }
815
                        }
816
                        $userStatus = STUDENT;
817
                        // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
818
                        // see BT#18242
819
                        if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
820
                            $userStatus = COURSEMANAGERLOWSECURITY;
821
                        }
822
                        $answer = Security::remove_XSS($answer, $userStatus);
823
                        $s .= '<tr>';
824
                        $s .= Display::tag('td', $answer);
825
                        foreach ($objQuestionTmp->options as $key => $item) {
826
                            if (isset($myChoice[$numAnswer]) && $key == $myChoice[$numAnswer]) {
827
                                $attributes = [
828
                                    'checked' => 1,
829
                                    'selected' => 1,
830
                                ];
831
                            } else {
832
                                $attributes = [];
833
                            }
834
835
                            if ($debug_mark_answer) {
836
                                if ($key == $answerCorrect) {
837
                                    $attributes['checked'] = 1;
838
                                    $attributes['selected'] = 1;
839
                                }
840
                            }
841
                            $s .= Display::tag(
842
                                'td',
843
                                Display::input(
844
                                    'radio',
845
                                    'choice['.$questionId.']['.$numAnswer.']',
846
                                    $key,
847
                                    $attributes
848
                                )
849
                            );
850
                        }
851
852
                        if ($show_comment) {
853
                            $s .= '<td>';
854
                            $s .= $comment;
855
                            $s .= '</td>';
856
                        }
857
                        $s .= '</tr>';
858
                        break;
859
                    case FILL_IN_BLANKS:
860
                        // display the question, with field empty, for student to fill it,
861
                        // or filled to display the answer in the Question preview of the exercise/admin.php page
862
                        $displayForStudent = true;
863
                        $listAnswerInfo = FillBlanks::getAnswerInfo($answer);
864
                        // Correct answers
865
                        $correctAnswerList = $listAnswerInfo['words'];
866
                        // Student's answer
867
                        $studentAnswerList = [];
868
                        if (isset($user_choice[0]['answer'])) {
869
                            $arrayStudentAnswer = FillBlanks::getAnswerInfo(
870
                                $user_choice[0]['answer'],
871
                                true
872
                            );
873
                            $studentAnswerList = $arrayStudentAnswer['student_answer'];
874
                        }
875
876
                        // If the question must be shown with the answer (in page exercise/admin.php)
877
                        // for teacher preview set the student-answer to the correct answer
878
                        if ($debug_mark_answer) {
879
                            $studentAnswerList = $correctAnswerList;
880
                            $displayForStudent = false;
881
                        }
882
883
                        if (!empty($correctAnswerList) && !empty($studentAnswerList)) {
884
                            $answer = '';
885
                            for ($i = 0; $i < count($listAnswerInfo['common_words']) - 1; $i++) {
886
                                // display the common word
887
                                $answer .= $listAnswerInfo['common_words'][$i];
888
                                // display the blank word
889
                                $correctItem = $listAnswerInfo['words'][$i];
890
                                if (isset($studentAnswerList[$i])) {
891
                                    // If student already started this test and answered this question,
892
                                    // fill the blank with his previous answers
893
                                    // may be "" if student viewed the question, but did not fill the blanks
894
                                    $correctItem = $studentAnswerList[$i];
895
                                }
896
                                $attributes['style'] = 'width:'.$listAnswerInfo['input_size'][$i].'px';
897
                                $answer .= FillBlanks::getFillTheBlankHtml(
898
                                    $current_item,
899
                                    $questionId,
900
                                    $correctItem,
901
                                    $attributes,
902
                                    $answer,
903
                                    $listAnswerInfo,
904
                                    $displayForStudent,
905
                                    $i
906
                                );
907
                            }
908
                            // display the last common word
909
                            $answer .= $listAnswerInfo['common_words'][$i];
910
                        } else {
911
                            // display empty [input] with the right width for student to fill it
912
                            $answer = '';
913
                            for ($i = 0; $i < count($listAnswerInfo['common_words']) - 1; $i++) {
914
                                // display the common words
915
                                $answer .= $listAnswerInfo['common_words'][$i];
916
                                // display the blank word
917
                                $attributes['style'] = 'width:'.$listAnswerInfo['input_size'][$i].'px';
918
                                $answer .= FillBlanks::getFillTheBlankHtml(
919
                                    $current_item,
920
                                    $questionId,
921
                                    '',
922
                                    $attributes,
923
                                    $answer,
924
                                    $listAnswerInfo,
925
                                    $displayForStudent,
926
                                    $i
927
                                );
928
                            }
929
                            // display the last common word
930
                            $answer .= $listAnswerInfo['common_words'][$i];
931
                        }
932
                        $s .= $answer;
933
                        break;
934
                    case CALCULATED_ANSWER:
935
                        /*
936
                         * In the CALCULATED_ANSWER test
937
                         * you mustn't have [ and ] in the textarea
938
                         * you mustn't have @@ in the textarea
939
                         * the text to find mustn't be empty or contains only spaces
940
                         * the text to find mustn't contains HTML tags
941
                         * the text to find mustn't contains char "
942
                         */
943
                        if (null !== $origin) {
944
                            global $exe_id;
945
                            $exe_id = (int) $exe_id;
946
                            $trackAttempts = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
947
                            $sql = "SELECT answer FROM $trackAttempts
948
                                    WHERE exe_id = $exe_id AND question_id= $questionId";
949
                            $rsLastAttempt = Database::query($sql);
950
                            $rowLastAttempt = Database::fetch_array($rsLastAttempt);
951
952
                            $answer = null;
953
                            if (isset($rowLastAttempt['answer'])) {
954
                                $answer = $rowLastAttempt['answer'];
955
                            }
956
957
                            if (empty($answer)) {
958
                                $_SESSION['calculatedAnswerId'][$questionId] = mt_rand(
959
                                    1,
960
                                    $nbrAnswers
961
                                );
962
                                $answer = $objAnswerTmp->selectAnswer(
963
                                    $_SESSION['calculatedAnswerId'][$questionId]
964
                                );
965
                            }
966
                        }
967
968
                        [$answer] = explode('@@', $answer);
969
                        // $correctAnswerList array of array with correct anwsers 0=> [0=>[\p] 1=>[plop]]
970
                        api_preg_match_all(
971
                            '/\[[^]]+\]/',
972
                            $answer,
973
                            $correctAnswerList
974
                        );
975
976
                        // get student answer to display it if student go back
977
                        // to previous calculated answer question in a test
978
                        if (isset($user_choice[0]['answer'])) {
979
                            api_preg_match_all(
980
                                '/\[[^]]+\]/',
981
                                $answer,
982
                                $studentAnswerList
983
                            );
984
                            $studentAnswerListToClean = $studentAnswerList[0];
985
                            $studentAnswerList = [];
986
987
                            $maxStudents = count($studentAnswerListToClean);
988
                            for ($i = 0; $i < $maxStudents; $i++) {
989
                                $answerCorrected = $studentAnswerListToClean[$i];
990
                                $answerCorrected = api_preg_replace(
991
                                    '| / <font color="green"><b>.*$|',
992
                                    '',
993
                                    $answerCorrected
994
                                );
995
                                $answerCorrected = api_preg_replace(
996
                                    '/^\[/',
997
                                    '',
998
                                    $answerCorrected
999
                                );
1000
                                $answerCorrected = api_preg_replace(
1001
                                    '|^<font color="red"><s>|',
1002
                                    '',
1003
                                    $answerCorrected
1004
                                );
1005
                                $answerCorrected = api_preg_replace(
1006
                                    '|</s></font>$|',
1007
                                    '',
1008
                                    $answerCorrected
1009
                                );
1010
                                $answerCorrected = '['.$answerCorrected.']';
1011
                                $studentAnswerList[] = $answerCorrected;
1012
                            }
1013
                        }
1014
1015
                        // If display preview of answer in test view for exemple,
1016
                        // set the student answer to the correct answers
1017
                        if ($debug_mark_answer) {
1018
                            // contain the rights answers surronded with brackets
1019
                            $studentAnswerList = $correctAnswerList[0];
1020
                        }
1021
1022
                        /*
1023
                        Split the response by bracket
1024
                        tabComments is an array with text surrounding the text to find
1025
                        we add a space before and after the answerQuestion to be sure to
1026
                        have a block of text before and after [xxx] patterns
1027
                        so we have n text to find ([xxx]) and n+1 block of texts before,
1028
                        between and after the text to find
1029
                        */
1030
                        $tabComments = api_preg_split(
1031
                            '/\[[^]]+\]/',
1032
                            ' '.$answer.' '
1033
                        );
1034
                        if (!empty($correctAnswerList) && !empty($studentAnswerList)) {
1035
                            $answer = '';
1036
                            $i = 0;
1037
                            foreach ($studentAnswerList as $studentItem) {
1038
                                // Remove surronding brackets
1039
                                $studentResponse = api_substr(
1040
                                    $studentItem,
1041
                                    1,
1042
                                    api_strlen($studentItem) - 2
1043
                                );
1044
                                $size = strlen($studentItem);
1045
                                $attributes['class'] = self::detectInputAppropriateClass($size);
1046
                                $answer .= $tabComments[$i].
1047
                                    Display::input(
1048
                                        'text',
1049
                                        "choice[$questionId][]",
1050
                                        $studentResponse,
1051
                                        $attributes
1052
                                    );
1053
                                $i++;
1054
                            }
1055
                            $answer .= $tabComments[$i];
1056
                        } else {
1057
                            // display exercise with empty input fields
1058
                            // every [xxx] are replaced with an empty input field
1059
                            foreach ($correctAnswerList[0] as $item) {
1060
                                $size = strlen($item);
1061
                                $attributes['class'] = self::detectInputAppropriateClass($size);
1062
                                if (EXERCISE_FEEDBACK_TYPE_POPUP == $exercise->getFeedbackType()) {
1063
                                    $attributes['id'] = "question_$questionId";
1064
                                    $attributes['class'] .= ' checkCalculatedQuestionOnEnter ';
1065
                                }
1066
1067
                                $answer = str_replace(
1068
                                    $item,
1069
                                    Display::input(
1070
                                        'text',
1071
                                        "choice[$questionId][]",
1072
                                        '',
1073
                                        $attributes
1074
                                    ),
1075
                                    $answer
1076
                                );
1077
                            }
1078
                        }
1079
                        if (null !== $origin) {
1080
                            $s = $answer;
1081
                            break;
1082
                        } else {
1083
                            $s .= $answer;
1084
                        }
1085
                        break;
1086
                    case MATCHING:
1087
                        // matching type, showing suggestions and answers
1088
                        // TODO: replace $answerId by $numAnswer
1089
                        if (0 != $answerCorrect) {
1090
                            // only show elements to be answered (not the contents of
1091
                            // the select boxes, who are correct = 0)
1092
                            $s .= '<tr><td width="45%" valign="top">';
1093
                            $parsed_answer = $answer;
1094
                            // Left part questions
1095
                            $s .= '<p class="indent">'.$lines_count.'.&nbsp;'.$parsed_answer.'</p></td>';
1096
                            // Middle part (matches selects)
1097
                            // Id of select is # question + # of option
1098
                            $s .= '<td width="10%" valign="top" align="center">
1099
                                <div class="select-matching">
1100
                                <select
1101
                                    class="form-control"
1102
                                    id="choice_id_'.$current_item.'_'.$lines_count.'"
1103
                                    name="choice['.$questionId.']['.$numAnswer.']">';
1104
1105
                            // fills the list-box
1106
                            foreach ($select_items as $key => $val) {
1107
                                // set $debug_mark_answer to true at function start to
1108
                                // show the correct answer with a suffix '-x'
1109
                                $selected = '';
1110
                                if ($debug_mark_answer) {
1111
                                    if ($val['id'] == $answerCorrect) {
1112
                                        $selected = 'selected="selected"';
1113
                                    }
1114
                                }
1115
                                //$user_choice_array_position
1116
                                if (isset($user_choice_array_position[$numAnswer]) &&
1117
                                    $val['id'] == $user_choice_array_position[$numAnswer]
1118
                                ) {
1119
                                    $selected = 'selected="selected"';
1120
                                }
1121
                                $s .= '<option value="'.$val['id'].'" '.$selected.'>'.$val['letter'].'</option>';
1122
                            }
1123
1124
                            $s .= '</select></div></td><td width="5%" class="separate">&nbsp;</td>';
1125
                            $s .= '<td width="40%" valign="top" >';
1126
                            if (isset($select_items[$lines_count])) {
1127
                                $s .= '<div class="text-right">
1128
                                        <p class="indent">'.
1129
                                    $select_items[$lines_count]['letter'].'.&nbsp; '.
1130
                                    $select_items[$lines_count]['answer'].'
1131
                                        </p>
1132
                                        </div>';
1133
                            } else {
1134
                                $s .= '&nbsp;';
1135
                            }
1136
                            $s .= '</td>';
1137
                            $s .= '</tr>';
1138
                            $lines_count++;
1139
                            // If the left side of the "matching" has been completely
1140
                            // shown but the right side still has values to show...
1141
                            if (($lines_count - 1) == $num_suggestions) {
1142
                                // if it remains answers to shown at the right side
1143
                                while (isset($select_items[$lines_count])) {
1144
                                    $s .= '<tr>
1145
                                      <td colspan="2"></td>
1146
                                      <td valign="top">';
1147
                                    $s .= '<b>'.$select_items[$lines_count]['letter'].'.</b> '.
1148
                                        $select_items[$lines_count]['answer'];
1149
                                    $s .= "</td>
1150
                                </tr>";
1151
                                    $lines_count++;
1152
                                }
1153
                            }
1154
                            $matching_correct_answer++;
1155
                        }
1156
                        break;
1157
                    case DRAGGABLE:
1158
                        if ($answerCorrect) {
1159
                            $windowId = $questionId.'_'.$lines_count;
1160
                            $s .= '<li class="touch-items" id="'.$windowId.'">';
1161
                            $s .= Display::div(
1162
                                $answer,
1163
                                [
1164
                                    'id' => "window_$windowId",
1165
                                    'class' => "window{$questionId}_question_draggable exercise-draggable-answer-option",
1166
                                ]
1167
                            );
1168
1169
                            $draggableSelectOptions = [];
1170
                            $selectedValue = 0;
1171
                            $selectedIndex = 0;
1172
                            if ($user_choice) {
1173
                                foreach ($user_choice as $userChoiceKey => $chosen) {
1174
                                    $userChoiceKey++;
1175
                                    if ($lines_count != $userChoiceKey) {
1176
                                        continue;
1177
                                    }
1178
                                    /*if ($answerCorrect != $chosen['answer']) {
1179
                                        continue;
1180
                                    }*/
1181
                                    $selectedValue = $chosen['answer'];
1182
                                }
1183
                            }
1184
                            foreach ($select_items as $key => $select_item) {
1185
                                $draggableSelectOptions[$select_item['id']] = $select_item['letter'];
1186
                            }
1187
1188
                            foreach ($draggableSelectOptions as $value => $text) {
1189
                                if ($value == $selectedValue) {
1190
                                    break;
1191
                                }
1192
                                $selectedIndex++;
1193
                            }
1194
1195
                            $s .= Display::select(
1196
                                "choice[$questionId][$numAnswer]",
1197
                                $draggableSelectOptions,
1198
                                $selectedValue,
1199
                                [
1200
                                    'id' => "window_{$windowId}_select",
1201
                                    'class' => 'select_option hidden',
1202
                                ],
1203
                                false
1204
                            );
1205
1206
                            if ($selectedValue && $selectedIndex) {
1207
                                $s .= "
1208
                                    <script>
1209
                                        $(function() {
1210
                                            DraggableAnswer.deleteItem(
1211
                                                $('#{$questionId}_$lines_count'),
1212
                                                $('#drop_{$questionId}_{$selectedIndex}')
1213
                                            );
1214
                                        });
1215
                                    </script>
1216
                                ";
1217
                            }
1218
1219
                            if (isset($select_items[$lines_count])) {
1220
                                $s .= Display::div(
1221
                                    Display::tag(
1222
                                        'b',
1223
                                        $select_items[$lines_count]['letter']
1224
                                    ).$select_items[$lines_count]['answer'],
1225
                                    [
1226
                                        'id' => "window_{$windowId}_answer",
1227
                                        'class' => 'hidden',
1228
                                    ]
1229
                                );
1230
                            } else {
1231
                                $s .= '&nbsp;';
1232
                            }
1233
1234
                            $lines_count++;
1235
                            if (($lines_count - 1) == $num_suggestions) {
1236
                                while (isset($select_items[$lines_count])) {
1237
                                    $s .= Display::tag('b', $select_items[$lines_count]['letter']);
1238
                                    $s .= $select_items[$lines_count]['answer'];
1239
                                    $lines_count++;
1240
                                }
1241
                            }
1242
1243
                            $matching_correct_answer++;
1244
                            $s .= '</li>';
1245
                        }
1246
                        break;
1247
                    case MATCHING_DRAGGABLE:
1248
                        if (1 == $answerId) {
1249
                            echo $objAnswerTmp->getJs();
1250
                        }
1251
                        if (0 != $answerCorrect) {
1252
                            $windowId = "{$questionId}_{$lines_count}";
1253
                            $s .= <<<HTML
1254
                            <tr>
1255
                                <td width="45%">
1256
                                    <div id="window_{$windowId}"
1257
                                        class="window window_left_question window{$questionId}_question">
1258
                                        <strong>$lines_count.</strong>
1259
                                        $answer
1260
                                    </div>
1261
                                </td>
1262
                                <td width="10%">
1263
HTML;
1264
1265
                            $draggableSelectOptions = [];
1266
                            $selectedValue = 0;
1267
                            $selectedIndex = 0;
1268
1269
                            if ($user_choice) {
1270
                                foreach ($user_choice as $chosen) {
1271
                                    if ($numAnswer == $chosen['position']) {
1272
                                        $selectedValue = $chosen['answer'];
1273
                                        break;
1274
                                    }
1275
                                }
1276
                            }
1277
1278
                            foreach ($select_items as $key => $selectItem) {
1279
                                $draggableSelectOptions[$selectItem['id']] = $selectItem['letter'];
1280
                            }
1281
1282
                            foreach ($draggableSelectOptions as $value => $text) {
1283
                                if ($value == $selectedValue) {
1284
                                    break;
1285
                                }
1286
                                $selectedIndex++;
1287
                            }
1288
1289
                            $s .= Display::select(
1290
                                "choice[$questionId][$numAnswer]",
1291
                                $draggableSelectOptions,
1292
                                $selectedValue,
1293
                                [
1294
                                    'id' => "window_{$windowId}_select",
1295
                                    'class' => 'hidden',
1296
                                ],
1297
                                false
1298
                            );
1299
1300
                            if (!empty($answerCorrect) && !empty($selectedValue)) {
1301
                                // Show connect if is not freeze (question preview)
1302
                                if (!$freeze) {
1303
                                    $s .= "
1304
                                        <script>
1305
                                            $(function() {
1306
                                                MatchingDraggable.instances['$questionId'].connect({
1307
                                                    source: 'window_$windowId',
1308
                                                    target: 'window_{$questionId}_{$selectedIndex}_answer',
1309
                                                    endpoint: ['Dot', {radius: 12}],
1310
                                                    anchors: ['RightMiddle', 'LeftMiddle'],
1311
                                                    paintStyle: {stroke: '#8A8888', strokeWidth: 8},
1312
                                                    connector: [
1313
                                                        MatchingDraggable.connectorType,
1314
                                                        {curvines: MatchingDraggable.curviness}
1315
                                                    ]
1316
                                                });
1317
                                            });
1318
                                        </script>
1319
                                    ";
1320
                                }
1321
                            }
1322
1323
                            $s .= '</td><td width="45%">';
1324
                            if (isset($select_items[$lines_count])) {
1325
                                $s .= <<<HTML
1326
                                <div id="window_{$windowId}_answer" class="window window_right_question">
1327
                                    <strong>{$select_items[$lines_count]['letter']}.</strong>
1328
                                    {$select_items[$lines_count]['answer']}
1329
                                </div>
1330
HTML;
1331
                            } else {
1332
                                $s .= '&nbsp;';
1333
                            }
1334
1335
                            $s .= '</td></tr>';
1336
                            $lines_count++;
1337
                            if (($lines_count - 1) == $num_suggestions) {
1338
                                while (isset($select_items[$lines_count])) {
1339
                                    $s .= <<<HTML
1340
                                    <tr>
1341
                                        <td colspan="2"></td>
1342
                                        <td>
1343
                                            <strong>{$select_items[$lines_count]['letter']}</strong>
1344
                                            {$select_items[$lines_count]['answer']}
1345
                                        </td>
1346
                                    </tr>
1347
HTML;
1348
                                    $lines_count++;
1349
                                }
1350
                            }
1351
                            $matching_correct_answer++;
1352
                        }
1353
                        break;
1354
                }
1355
            }
1356
1357
            if ($show_comment) {
1358
                $s .= '</table>';
1359
            } elseif (in_array(
1360
                $answerType,
1361
                [
1362
                    MATCHING,
1363
                    MATCHING_DRAGGABLE,
1364
                    UNIQUE_ANSWER_NO_OPTION,
1365
                    MULTIPLE_ANSWER_TRUE_FALSE,
1366
                    MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE,
1367
                    MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY,
1368
                ]
1369
            )) {
1370
                $s .= '</table>';
1371
            }
1372
1373
            if (DRAGGABLE == $answerType) {
1374
                $isVertical = 'v' == $objQuestionTmp->extra;
1375
                $s .= "</ul></div>";
1376
                $counterAnswer = 1;
1377
                $s .= '<div class="question-answer__items question-answer__items--'.($isVertical ? 'vertical' : 'horizontal').'">';
1378
                for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
1379
                    $answerCorrect = $objAnswerTmp->isCorrect($answerId);
1380
                    $windowId = $questionId.'_'.$counterAnswer;
1381
                    if ($answerCorrect) {
1382
                        $s .= '<div class="droppable-item">
1383
                            <span class="number">'.$counterAnswer.'</span>
1384
                            <div id="drop_'.$windowId.'" class="droppable"></div>
1385
                            </div>';
1386
                        $counterAnswer++;
1387
                    }
1388
                }
1389
1390
                $s .= '</div>';
1391
//                $s .= '</div>';
1392
            }
1393
1394
            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
1395
                $s .= '</div>'; //drag_question
1396
            }
1397
1398
            $s .= '</div>'; //question_options row
1399
1400
            // destruction of the Answer object
1401
            unset($objAnswerTmp);
1402
            // destruction of the Question object
1403
            unset($objQuestionTmp);
1404
            if ('export' == $origin) {
1405
                return $s;
1406
            }
1407
            echo $s;
1408
        } elseif (HOT_SPOT == $answerType || HOT_SPOT_DELINEATION == $answerType) {
1409
            global $exe_id;
1410
            $questionDescription = $objQuestionTmp->selectDescription();
1411
            // Get the answers, make a list
1412
            $objAnswerTmp = new Answer($questionId, $course_id);
1413
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
1414
1415
            // get answers of hotpost
1416
            $answers_hotspot = [];
1417
            for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
1418
                $answers = $objAnswerTmp->selectAnswerByAutoId(
1419
                    $objAnswerTmp->selectAutoId($answerId)
1420
                );
1421
                $answers_hotspot[$answers['iid']] = $objAnswerTmp->selectAnswer(
1422
                    $answerId
1423
                );
1424
            }
1425
1426
            $answerList = '';
1427
            $hotspotColor = 0;
1428
            if (HOT_SPOT_DELINEATION != $answerType) {
1429
                $answerList = '
1430
                    <div class="well well-sm">
1431
                        <h5 class="page-header">'.get_lang('Image zones').'</h5>
1432
                        <ol>
1433
                ';
1434
1435
                if (!empty($answers_hotspot)) {
1436
                    Session::write("hotspot_ordered$questionId", array_keys($answers_hotspot));
1437
                    foreach ($answers_hotspot as $value) {
1438
                        $answerList .= '<li>';
1439
                        if ($freeze) {
1440
                            $answerList .= '<span class="hotspot-color-'.$hotspotColor
1441
                                .' fa fa-square" aria-hidden="true"></span>'.PHP_EOL;
1442
                        }
1443
                        $answerList .= $value;
1444
                        $answerList .= '</li>';
1445
                        $hotspotColor++;
1446
                    }
1447
                }
1448
1449
                $answerList .= '
1450
                        </ol>
1451
                    </div>
1452
                ';
1453
            }
1454
            if ($freeze) {
1455
                $relPath = api_get_path(WEB_CODE_PATH);
1456
                echo "
1457
                        <div class=\"row\">
1458
                            <div class=\"col-sm-9\">
1459
                                <div id=\"hotspot-preview-$questionId\"></div>
1460
                            </div>
1461
                            <div class=\"col-sm-3\">
1462
                                $answerList
1463
                            </div>
1464
                        </div>
1465
                        <script>
1466
                            new ".(HOT_SPOT == $answerType ? "HotspotQuestion" : "DelineationQuestion")."({
1467
                                questionId: $questionId,
1468
                                exerciseId: $exerciseId,
1469
                                exeId: 0,
1470
                                selector: '#hotspot-preview-$questionId',
1471
                                for: 'preview',
1472
                                relPath: '$relPath'
1473
                            });
1474
                        </script>
1475
                    ";
1476
1477
                return;
1478
            }
1479
1480
            if (!$only_questions) {
1481
                if ($show_title) {
1482
                    if ($exercise->display_category_name) {
1483
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->id);
1484
                    }
1485
                    echo $objQuestionTmp->getTitleToDisplay($exercise, $current_item);
1486
                }
1487
1488
                //@todo I need to the get the feedback type
1489
                echo <<<HOTSPOT
1490
                    <input type="hidden" name="hidden_hotspot_id" value="$questionId" />
1491
                    <div class="exercise_questions">
1492
                        $questionDescription
1493
                        <div class="row">
1494
HOTSPOT;
1495
            }
1496
1497
            $relPath = api_get_path(WEB_CODE_PATH);
1498
            $s .= "<div class=\"col-sm-8 col-md-9\">
1499
                   <div class=\"hotspot-image\"></div>
1500
                    <script>
1501
                        $(function() {
1502
                            new ".(HOT_SPOT_DELINEATION == $answerType ? 'DelineationQuestion' : 'HotspotQuestion')."({
1503
                                questionId: $questionId,
1504
                                exerciseId: $exerciseId,
1505
                                exeId: 0,
1506
                                selector: '#question_div_' + $questionId + ' .hotspot-image',
1507
                                for: 'user',
1508
                                relPath: '$relPath'
1509
                            });
1510
                        });
1511
                    </script>
1512
                </div>
1513
                <div class=\"col-sm-4 col-md-3\">
1514
                    $answerList
1515
                </div>
1516
            ";
1517
1518
            echo <<<HOTSPOT
1519
                            $s
1520
                        </div>
1521
                    </div>
1522
HOTSPOT;
1523
        } elseif (ANNOTATION == $answerType) {
1524
            global $exe_id;
1525
            $relPath = api_get_path(WEB_CODE_PATH);
1526
            if (api_is_platform_admin() || api_is_course_admin()) {
1527
                $questionRepo = Container::getQuestionRepository();
1528
                $questionEntity = $questionRepo->find($questionId);
1529
                if ($freeze) {
1530
                    echo Display::img(
1531
                        $questionRepo->getHotSpotImageUrl($questionEntity),
1532
                        $objQuestionTmp->selectTitle(),
1533
                        ['width' => '600px']
1534
                    );
1535
1536
                    return 0;
1537
                }
1538
            }
1539
1540
            if (!$only_questions) {
1541
                if ($show_title) {
1542
                    if ($exercise->display_category_name) {
1543
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->id);
1544
                    }
1545
                    echo $objQuestionTmp->getTitleToDisplay($exercise, $current_item);
1546
                }
1547
1548
                echo '
1549
                    <input type="hidden" name="hidden_hotspot_id" value="'.$questionId.'" />
1550
                    <div class="exercise_questions">
1551
                        '.$objQuestionTmp->selectDescription().'
1552
                        <div class="row">
1553
                            <div class="col-sm-8 col-md-9">
1554
                                <div id="annotation-canvas-'.$questionId.'" class="annotation-canvas center-block">
1555
                                </div>
1556
                                <script>
1557
                                    AnnotationQuestion({
1558
                                        questionId: '.$questionId.',
1559
                                        exerciseId: '.$exerciseId.',
1560
                                        relPath: \''.$relPath.'\',
1561
                                        courseId: '.$course_id.',
1562
                                    });
1563
                                </script>
1564
                            </div>
1565
                            <div class="col-sm-4 col-md-3">
1566
                                <div class="well well-sm" id="annotation-toolbar-'.$questionId.'">
1567
                                    <div class="btn-toolbar">
1568
                                        <div class="btn-group" data-toggle="buttons">
1569
                                            <label class="btn btn--plain active"
1570
                                                aria-label="'.get_lang('Add annotation path').'">
1571
                                                <input
1572
                                                    type="radio" value="0"
1573
                                                    name="'.$questionId.'-options" autocomplete="off" checked>
1574
                                                <span class="fas fa-pencil-alt" aria-hidden="true"></span>
1575
                                            </label>
1576
                                            <label class="btn btn--plain"
1577
                                                aria-label="'.get_lang('Add annotation text').'">
1578
                                                <input
1579
                                                    type="radio" value="1"
1580
                                                    name="'.$questionId.'-options" autocomplete="off">
1581
                                                <span class="fa fa-font fa-fw" aria-hidden="true"></span>
1582
                                            </label>
1583
                                        </div>
1584
                                    </div>
1585
                                    <ul class="list-unstyled"></ul>
1586
                                </div>
1587
                            </div>
1588
                        </div>
1589
                    </div>
1590
                ';
1591
            }
1592
            $objAnswerTmp = new Answer($questionId);
1593
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
1594
            unset($objAnswerTmp, $objQuestionTmp);
1595
        }
1596
1597
        return $nbrAnswers;
1598
    }
1599
1600
    /**
1601
     * Get an HTML string with the list of exercises where the given question
1602
     * is being used.
1603
     *
1604
     * @param int $questionId    The iid of the question being observed
1605
     * @param int $excludeTestId If defined, exclude this (current) test from the list of results
1606
     *
1607
     * @return string An HTML string containing a div and a table
1608
     */
1609
    public static function showTestsWhereQuestionIsUsed(int $questionId, int $excludeTestId = 0)
1610
    {
1611
        $questionId = (int) $questionId;
1612
        $excludeTestId = (int) $excludeTestId;
1613
1614
        $sql = "SELECT qz.title quiz_title,
1615
                        c.title course_title,
1616
                        s.name session_name,
1617
                        qz.iid as quiz_id,
1618
                        qz.c_id,
1619
                        qz.session_id
1620
                FROM c_quiz qz,
1621
                    c_quiz_rel_question qq,
1622
                    course c,
1623
                    session s
1624
                WHERE qz.c_id = c.id AND
1625
                    (qz.session_id = s.id OR qz.session_id = 0) AND
1626
                    qq.quiz_id = qz.iid AND ";
1627
        if (!empty($excludeTestId)) {
1628
            $sql .= " qz.iid != $excludeTestId AND ";
1629
        }
1630
        $sql .= "     qq.question_id = $questionId
1631
                GROUP BY qq.iid";
1632
1633
        $result = [];
1634
        $html = "";
1635
1636
        $sqlResult = Database::query($sql);
1637
1638
        if (0 != Database::num_rows($sqlResult)) {
1639
            while ($row = Database::fetch_assoc($sqlResult)) {
1640
                $tmp = [];
1641
                $tmp[0] = $row['course_title'];
1642
                $tmp[1] = $row['session_name'];
1643
                $tmp[2] = $row['quiz_title'];
1644
                // Send do other test with r=1 to reset current test session variables
1645
                $urlToQuiz = api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_cidreq().'&exerciseId='.$row['quiz_id'].'&r=1';
1646
                $tmp[3] = '<a href="'.$urlToQuiz.'">'.Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit')).'</a>';
1647
                if (0 == (int) $row['session_id']) {
1648
                    $tmp[1] = '-';
1649
                }
1650
1651
                $result[] = $tmp;
1652
            }
1653
1654
            $headers = [
1655
                get_lang('Course'),
1656
                get_lang('Session'),
1657
                get_lang('Quiz'),
1658
                get_lang('LinkToTestEdition'),
1659
            ];
1660
1661
            $title = Display::div(
1662
                get_lang('QuestionAlsoUsedInTheFollowingTests'),
1663
                [
1664
                    'class' => 'section-title',
1665
                    'style' => 'margin-top: 25px; border-bottom: none',
1666
                ]
1667
            );
1668
1669
            $html = $title.Display::table($headers, $result);
1670
        }
1671
1672
        echo $html;
1673
    }
1674
1675
    /**
1676
     * @param int $exeId
1677
     *
1678
     * @return array
1679
     */
1680
    public static function get_exercise_track_exercise_info($exeId)
1681
    {
1682
        $quizTable = Database::get_course_table(TABLE_QUIZ_TEST);
1683
        $trackExerciseTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1684
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
1685
        $exeId = (int) $exeId;
1686
        $result = [];
1687
        if (!empty($exeId)) {
1688
            $sql = " SELECT q.*, tee.*
1689
                FROM $quizTable as q
1690
                INNER JOIN $trackExerciseTable as tee
1691
                ON q.iid = tee.exe_exo_id
1692
                WHERE
1693
                    tee.exe_id = $exeId";
1694
1695
            $sqlResult = Database::query($sql);
1696
            if (Database::num_rows($sqlResult)) {
1697
                $result = Database::fetch_assoc($sqlResult);
1698
                $result['duration_formatted'] = '';
1699
                if (!empty($result['exe_duration'])) {
1700
                    $time = api_format_time($result['exe_duration'], 'js');
1701
                    $result['duration_formatted'] = $time;
1702
                }
1703
            }
1704
        }
1705
1706
        return $result;
1707
    }
1708
1709
    /**
1710
     * Validates the time control key.
1711
     *
1712
     * @param int $lp_id
1713
     * @param int $lp_item_id
1714
     *
1715
     * @return bool
1716
     */
1717
    public static function exercise_time_control_is_valid(Exercise $exercise, $lp_id = 0, $lp_item_id = 0)
1718
    {
1719
        $exercise_id = $exercise->getId();
1720
        $expiredTime = $exercise->expired_time;
1721
1722
        if (!empty($expiredTime)) {
1723
            $current_expired_time_key = self::get_time_control_key(
1724
                $exercise_id,
1725
                $lp_id,
1726
                $lp_item_id
1727
            );
1728
            if (isset($_SESSION['expired_time'][$current_expired_time_key])) {
1729
                $current_time = time();
1730
                $expired_time = api_strtotime(
1731
                    $_SESSION['expired_time'][$current_expired_time_key],
1732
                    'UTC'
1733
                );
1734
                $total_time_allowed = $expired_time + 30;
1735
                if ($total_time_allowed < $current_time) {
1736
                    return false;
1737
                }
1738
1739
                return true;
1740
            }
1741
1742
            return false;
1743
        }
1744
1745
        return true;
1746
    }
1747
1748
    /**
1749
     * Deletes the time control token.
1750
     *
1751
     * @param int $exercise_id
1752
     * @param int $lp_id
1753
     * @param int $lp_item_id
1754
     */
1755
    public static function exercise_time_control_delete(
1756
        $exercise_id,
1757
        $lp_id = 0,
1758
        $lp_item_id = 0
1759
    ) {
1760
        $current_expired_time_key = self::get_time_control_key(
1761
            $exercise_id,
1762
            $lp_id,
1763
            $lp_item_id
1764
        );
1765
        unset($_SESSION['expired_time'][$current_expired_time_key]);
1766
    }
1767
1768
    /**
1769
     * Generates the time control key.
1770
     *
1771
     * @param int $exercise_id
1772
     * @param int $lp_id
1773
     * @param int $lp_item_id
1774
     *
1775
     * @return string
1776
     */
1777
    public static function get_time_control_key(
1778
        $exercise_id,
1779
        $lp_id = 0,
1780
        $lp_item_id = 0
1781
    ) {
1782
        $exercise_id = (int) $exercise_id;
1783
        $lp_id = (int) $lp_id;
1784
        $lp_item_id = (int) $lp_item_id;
1785
1786
        return
1787
            api_get_course_int_id().'_'.
1788
            api_get_session_id().'_'.
1789
            $exercise_id.'_'.
1790
            api_get_user_id().'_'.
1791
            $lp_id.'_'.
1792
            $lp_item_id;
1793
    }
1794
1795
    /**
1796
     * Get session time control.
1797
     *
1798
     * @param int $exercise_id
1799
     * @param int $lp_id
1800
     * @param int $lp_item_id
1801
     *
1802
     * @return int
1803
     */
1804
    public static function get_session_time_control_key(
1805
        $exercise_id,
1806
        $lp_id = 0,
1807
        $lp_item_id = 0
1808
    ) {
1809
        $return_value = 0;
1810
        $time_control_key = self::get_time_control_key(
1811
            $exercise_id,
1812
            $lp_id,
1813
            $lp_item_id
1814
        );
1815
        if (isset($_SESSION['expired_time']) && isset($_SESSION['expired_time'][$time_control_key])) {
1816
            $return_value = $_SESSION['expired_time'][$time_control_key];
1817
        }
1818
1819
        return $return_value;
1820
    }
1821
1822
    /**
1823
     * Gets count of exam results.
1824
     *
1825
     * @param int   $exerciseId
1826
     * @param array $conditions
1827
     * @param int   $courseId
1828
     * @param bool  $showSession
1829
     *
1830
     * @return array
1831
     */
1832
    public static function get_count_exam_results($exerciseId, $conditions, $courseId, $showSession = false)
1833
    {
1834
        $count = self::get_exam_results_data(
1835
            null,
1836
            null,
1837
            null,
1838
            null,
1839
            $exerciseId,
1840
            $conditions,
1841
            true,
1842
            $courseId,
1843
            $showSession
1844
        );
1845
1846
        return $count;
1847
    }
1848
1849
    /**
1850
     * Gets the exam'data results.
1851
     *
1852
     * @todo this function should be moved in a library  + no global calls
1853
     *
1854
     * @param int    $from
1855
     * @param int    $number_of_items
1856
     * @param int    $column
1857
     * @param string $direction
1858
     * @param int    $exercise_id
1859
     * @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...
1860
     * @param bool   $get_count
1861
     * @param int    $courseId
1862
     * @param bool   $showSessionField
1863
     * @param bool   $showExerciseCategories
1864
     * @param array  $userExtraFieldsToAdd
1865
     * @param bool   $useCommaAsDecimalPoint
1866
     * @param bool   $roundValues
1867
     * @param bool   $getOnyIds
1868
     *
1869
     * @return array
1870
     */
1871
    public static function get_exam_results_data(
1872
        $from,
1873
        $number_of_items,
1874
        $column,
1875
        $direction,
1876
        $exercise_id,
1877
        $extra_where_conditions = null,
1878
        $get_count = false,
1879
        $courseId = null,
1880
        $showSessionField = false,
1881
        $showExerciseCategories = false,
1882
        $userExtraFieldsToAdd = [],
1883
        $useCommaAsDecimalPoint = false,
1884
        $roundValues = false,
1885
        $getOnyIds = false
1886
    ) {
1887
        //@todo replace all this globals
1888
        global $filter;
1889
        $courseId = (int) $courseId;
1890
        $course = api_get_course_entity($courseId);
1891
        if (null === $course) {
1892
            return [];
1893
        }
1894
1895
        $sessionId = api_get_session_id();
1896
        $exercise_id = (int) $exercise_id;
1897
1898
        $is_allowedToEdit =
1899
            api_is_allowed_to_edit(null, true) ||
1900
            api_is_allowed_to_edit(true) ||
1901
            api_is_drh() ||
1902
            api_is_student_boss() ||
1903
            api_is_session_admin();
1904
        $TBL_USER = Database::get_main_table(TABLE_MAIN_USER);
1905
        $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
1906
        $TBL_GROUP_REL_USER = Database::get_course_table(TABLE_GROUP_USER);
1907
        $TBL_GROUP = Database::get_course_table(TABLE_GROUP);
1908
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1909
        $tblTrackAttemptQualify = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY);
1910
1911
        $session_id_and = '';
1912
        $sessionCondition = '';
1913
        if (!$showSessionField) {
1914
            $session_id_and = api_get_session_condition($sessionId, true, false, 'te.session_id');
1915
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'ttte.session_id');
1916
        }
1917
1918
        $exercise_where = '';
1919
        if (!empty($exercise_id)) {
1920
            $exercise_where .= ' AND te.exe_exo_id = '.$exercise_id.'  ';
1921
        }
1922
1923
        // sql for chamilo-type tests for teacher / tutor view
1924
        $sql_inner_join_tbl_track_exercices = "
1925
        (
1926
            SELECT DISTINCT ttte.*, if(tr.exe_id,1, 0) as revised
1927
            FROM $TBL_TRACK_EXERCISES ttte
1928
            LEFT JOIN $tblTrackAttemptQualify tr
1929
            ON (ttte.exe_id = tr.exe_id) AND tr.author > 0
1930
            WHERE
1931
                c_id = $courseId AND
1932
                exe_exo_id = $exercise_id
1933
                $sessionCondition
1934
        )";
1935
1936
        if ($is_allowedToEdit) {
1937
            //@todo fix to work with COURSE_RELATION_TYPE_RRHH in both queries
1938
            // Hack in order to filter groups
1939
            $sql_inner_join_tbl_user = '';
1940
            if (strpos($extra_where_conditions, 'group_id')) {
1941
                $sql_inner_join_tbl_user = "
1942
                (
1943
                    SELECT
1944
                        u.id as user_id,
1945
                        firstname,
1946
                        lastname,
1947
                        official_code,
1948
                        email,
1949
                        username,
1950
                        g.name as group_name,
1951
                        g.id as group_id
1952
                    FROM $TBL_USER u
1953
                    INNER JOIN $TBL_GROUP_REL_USER gru
1954
                    ON (gru.user_id = u.id AND gru.c_id= $courseId )
1955
                    INNER JOIN $TBL_GROUP g
1956
                    ON (gru.group_id = g.id AND g.c_id= $courseId )
1957
                    WHERE u.active <> ".USER_SOFT_DELETED."
1958
                )";
1959
            }
1960
1961
            if (strpos($extra_where_conditions, 'group_all')) {
1962
                $extra_where_conditions = str_replace(
1963
                    "AND (  group_id = 'group_all'  )",
1964
                    '',
1965
                    $extra_where_conditions
1966
                );
1967
                $extra_where_conditions = str_replace(
1968
                    "AND group_id = 'group_all'",
1969
                    '',
1970
                    $extra_where_conditions
1971
                );
1972
                $extra_where_conditions = str_replace(
1973
                    "group_id = 'group_all' AND",
1974
                    '',
1975
                    $extra_where_conditions
1976
                );
1977
1978
                $sql_inner_join_tbl_user = "
1979
                (
1980
                    SELECT
1981
                        u.id as user_id,
1982
                        firstname,
1983
                        lastname,
1984
                        official_code,
1985
                        email,
1986
                        username,
1987
                        '' as group_name,
1988
                        '' as group_id
1989
                    FROM $TBL_USER u
1990
                    WHERE u.active <> ".USER_SOFT_DELETED."
1991
                )";
1992
                $sql_inner_join_tbl_user = null;
1993
            }
1994
1995
            if (strpos($extra_where_conditions, 'group_none')) {
1996
                $extra_where_conditions = str_replace(
1997
                    "AND (  group_id = 'group_none'  )",
1998
                    "AND (  group_id is null  )",
1999
                    $extra_where_conditions
2000
                );
2001
                $extra_where_conditions = str_replace(
2002
                    "AND group_id = 'group_none'",
2003
                    "AND (  group_id is null  )",
2004
                    $extra_where_conditions
2005
                );
2006
                $sql_inner_join_tbl_user = "
2007
            (
2008
                SELECT
2009
                    u.id as user_id,
2010
                    firstname,
2011
                    lastname,
2012
                    official_code,
2013
                    email,
2014
                    username,
2015
                    g.name as group_name,
2016
                    g.iid as group_id
2017
                FROM $TBL_USER u
2018
                LEFT OUTER JOIN $TBL_GROUP_REL_USER gru
2019
                ON (gru.user_id = u.id AND gru.c_id= $courseId )
2020
                LEFT OUTER JOIN $TBL_GROUP g
2021
                ON (gru.group_id = g.id AND g.c_id = $courseId )
2022
                WHERE u.active <> ".USER_SOFT_DELETED."
2023
            )";
2024
            }
2025
2026
            // All
2027
            $is_empty_sql_inner_join_tbl_user = false;
2028
            if (empty($sql_inner_join_tbl_user)) {
2029
                $is_empty_sql_inner_join_tbl_user = true;
2030
                $sql_inner_join_tbl_user = "
2031
            (
2032
                SELECT u.id as user_id, firstname, lastname, email, username, ' ' as group_name, '' as group_id, official_code
2033
                FROM $TBL_USER u
2034
                WHERE u.active <> ".USER_SOFT_DELETED." AND u.status NOT IN(".api_get_users_status_ignored_in_reports('string').")
2035
            )";
2036
            }
2037
2038
            $sqlFromOption = " , $TBL_GROUP_REL_USER AS gru ";
2039
            $sqlWhereOption = "  AND gru.c_id = $courseId AND gru.user_id = user.id ";
2040
            $first_and_last_name = api_is_western_name_order() ? "firstname, lastname" : "lastname, firstname";
2041
2042
            if ($get_count) {
2043
                $sql_select = 'SELECT count(te.exe_id) ';
2044
            } else {
2045
                $sql_select = "SELECT DISTINCT
2046
                    user.user_id,
2047
                    $first_and_last_name,
2048
                    official_code,
2049
                    ce.title,
2050
                    username,
2051
                    te.score,
2052
                    te.max_score,
2053
                    te.exe_date,
2054
                    te.exe_id,
2055
                    te.session_id,
2056
                    email as exemail,
2057
                    te.start_date,
2058
                    ce.expired_time,
2059
                    steps_counter,
2060
                    exe_user_id,
2061
                    te.exe_duration,
2062
                    te.status as completion_status,
2063
                    propagate_neg,
2064
                    revised,
2065
                    group_name,
2066
                    group_id,
2067
                    orig_lp_id,
2068
                    te.user_ip";
2069
            }
2070
2071
            $sql = " $sql_select
2072
                FROM $TBL_EXERCISES AS ce
2073
                INNER JOIN $sql_inner_join_tbl_track_exercices AS te
2074
                ON (te.exe_exo_id = ce.iid)
2075
                INNER JOIN $sql_inner_join_tbl_user AS user
2076
                ON (user.user_id = exe_user_id)
2077
                WHERE
2078
                    te.c_id = $courseId $session_id_and AND
2079
                    ce.active <> -1
2080
                    $exercise_where
2081
                    $extra_where_conditions
2082
                ";
2083
        }
2084
2085
        if (empty($sql)) {
2086
            return false;
2087
        }
2088
2089
        if ($get_count) {
2090
            $resx = Database::query($sql);
2091
            $rowx = Database::fetch_row($resx, 'ASSOC');
2092
2093
            return $rowx[0];
2094
        }
2095
2096
        $teacher_list = CourseManager::get_teacher_list_from_course_code($course->getCode());
2097
        $teacher_id_list = [];
2098
        if (!empty($teacher_list)) {
2099
            foreach ($teacher_list as $teacher) {
2100
                $teacher_id_list[] = $teacher['user_id'];
2101
            }
2102
        }
2103
2104
        $scoreDisplay = new ScoreDisplay();
2105
        $decimalSeparator = '.';
2106
        $thousandSeparator = ',';
2107
2108
        if ($useCommaAsDecimalPoint) {
2109
            $decimalSeparator = ',';
2110
            $thousandSeparator = '';
2111
        }
2112
2113
        $listInfo = [];
2114
        $column = !empty($column) ? Database::escape_string($column) : null;
2115
        $from = (int) $from;
2116
        $number_of_items = (int) $number_of_items;
2117
        $direction = !in_array(strtolower(trim($direction)), ['asc', 'desc']) ? 'asc' : $direction;
2118
2119
        if (!empty($column)) {
2120
            $sql .= " ORDER BY `$column` $direction ";
2121
        }
2122
2123
        if (!$getOnyIds) {
2124
            $sql .= " LIMIT $from, $number_of_items";
2125
        }
2126
2127
        $results = [];
2128
        $resx = Database::query($sql);
2129
        while ($rowx = Database::fetch_assoc($resx)) {
2130
            $results[] = $rowx;
2131
        }
2132
2133
        $group_list = GroupManager::get_group_list(null, $course);
2134
        $clean_group_list = [];
2135
        if (!empty($group_list)) {
2136
            foreach ($group_list as $group) {
2137
                $clean_group_list[$group['iid']] = $group['name'];
2138
            }
2139
        }
2140
2141
        $lp_list_obj = new LearnpathList(api_get_user_id());
2142
        $lp_list = $lp_list_obj->get_flat_list();
2143
        $oldIds = array_column($lp_list, 'lp_old_id', 'iid');
2144
2145
        if (is_array($results)) {
2146
            $users_array_id = [];
2147
            $from_gradebook = false;
2148
            if (isset($_GET['gradebook']) && 'view' === $_GET['gradebook']) {
2149
                $from_gradebook = true;
2150
            }
2151
            $sizeof = count($results);
2152
            $locked = api_resource_is_locked_by_gradebook(
2153
                $exercise_id,
2154
                LINK_EXERCISE
2155
            );
2156
2157
            $timeNow = strtotime(api_get_utc_datetime());
2158
            // Looping results
2159
            for ($i = 0; $i < $sizeof; $i++) {
2160
                $revised = $results[$i]['revised'];
2161
                if ('incomplete' === $results[$i]['completion_status']) {
2162
                    // If the exercise was incomplete, we need to determine
2163
                    // if it is still into the time allowed, or if its
2164
                    // allowed time has expired and it can be closed
2165
                    // (it's "unclosed")
2166
                    $minutes = $results[$i]['expired_time'];
2167
                    if (0 == $minutes) {
2168
                        // There's no time limit, so obviously the attempt
2169
                        // can still be "ongoing", but the teacher should
2170
                        // be able to choose to close it, so mark it as
2171
                        // "unclosed" instead of "ongoing"
2172
                        $revised = 2;
2173
                    } else {
2174
                        $allowedSeconds = $minutes * 60;
2175
                        $timeAttemptStarted = strtotime($results[$i]['start_date']);
2176
                        $secondsSinceStart = $timeNow - $timeAttemptStarted;
2177
                        if ($secondsSinceStart > $allowedSeconds) {
2178
                            $revised = 2; // mark as "unclosed"
2179
                        } else {
2180
                            $revised = 3; // mark as "ongoing"
2181
                        }
2182
                    }
2183
                }
2184
2185
                if ($from_gradebook && ($is_allowedToEdit)) {
2186
                    if (in_array(
2187
                        $results[$i]['username'].$results[$i]['firstname'].$results[$i]['lastname'],
2188
                        $users_array_id
2189
                    )) {
2190
                        continue;
2191
                    }
2192
                    $users_array_id[] = $results[$i]['username'].$results[$i]['firstname'].$results[$i]['lastname'];
2193
                }
2194
2195
                $lp_obj = isset($results[$i]['orig_lp_id']) && isset($lp_list[$results[$i]['orig_lp_id']]) ? $lp_list[$results[$i]['orig_lp_id']] : null;
2196
                if (empty($lp_obj)) {
2197
                    // Try to get the old id (id instead of iid)
2198
                    $lpNewId = isset($results[$i]['orig_lp_id']) && isset($oldIds[$results[$i]['orig_lp_id']]) ? $oldIds[$results[$i]['orig_lp_id']] : null;
2199
                    if ($lpNewId) {
2200
                        $lp_obj = isset($lp_list[$lpNewId]) ? $lp_list[$lpNewId] : null;
2201
                    }
2202
                }
2203
                $lp_name = null;
2204
                if ($lp_obj) {
2205
                    $url = api_get_path(WEB_CODE_PATH).
2206
                        'lp/lp_controller.php?'.api_get_cidreq().'&action=view&lp_id='.$results[$i]['orig_lp_id'];
2207
                    $lp_name = Display::url(
2208
                        $lp_obj['lp_name'],
2209
                        $url,
2210
                        ['target' => '_blank']
2211
                    );
2212
                }
2213
2214
                // Add all groups by user
2215
                $group_name_list = '';
2216
                if ($is_empty_sql_inner_join_tbl_user) {
2217
                    $group_list = GroupManager::get_group_ids(
2218
                        api_get_course_int_id(),
2219
                        $results[$i]['user_id']
2220
                    );
2221
2222
                    foreach ($group_list as $id) {
2223
                        if (isset($clean_group_list[$id])) {
2224
                            $group_name_list .= $clean_group_list[$id].'<br/>';
2225
                        }
2226
                    }
2227
                    $results[$i]['group_name'] = $group_name_list;
2228
                }
2229
2230
                $results[$i]['exe_duration'] = !empty($results[$i]['exe_duration']) ? round($results[$i]['exe_duration'] / 60) : 0;
2231
                $id = $results[$i]['exe_id'];
2232
                $dt = api_convert_and_format_date($results[$i]['max_score']);
2233
2234
                // we filter the results if we have the permission to
2235
                $result_disabled = 0;
2236
                if (isset($results[$i]['results_disabled'])) {
2237
                    $result_disabled = (int) $results[$i]['results_disabled'];
2238
                }
2239
                if (0 == $result_disabled) {
2240
                    $my_res = $results[$i]['score'];
2241
                    $my_total = $results[$i]['max_score'];
2242
                    $results[$i]['start_date'] = api_get_local_time($results[$i]['start_date']);
2243
                    $results[$i]['exe_date'] = api_get_local_time($results[$i]['exe_date']);
2244
2245
                    if (!$results[$i]['propagate_neg'] && $my_res < 0) {
2246
                        $my_res = 0;
2247
                    }
2248
2249
                    $score = self::show_score(
2250
                        $my_res,
2251
                        $my_total,
2252
                        true,
2253
                        true,
2254
                        false,
2255
                        false,
2256
                        $decimalSeparator,
2257
                        $thousandSeparator,
2258
                        $roundValues
2259
                    );
2260
2261
                    $actions = '<div class="pull-right">';
2262
                    if ($is_allowedToEdit) {
2263
                        if (isset($teacher_id_list)) {
2264
                            if (in_array(
2265
                                $results[$i]['exe_user_id'],
2266
                                $teacher_id_list
2267
                            )) {
2268
                                $actions .= Display::getMdiIcon('human-male-board', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Trainer'));
2269
                            }
2270
                        }
2271
                        $revisedLabel = '';
2272
                        switch ($revised) {
2273
                            case 0:
2274
                                $actions .= "<a href='exercise_show.php?".api_get_cidreq()."&action=qualify&id=$id'>".
2275
                                    Display::getMdiIcon(ActionIcon::GRADE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Grade activity')
2276
                                    );
2277
                                $actions .= '</a>';
2278
                                $revisedLabel = Display::label(
2279
                                    get_lang('Not validated'),
2280
                                    'info'
2281
                                );
2282
                                break;
2283
                            case 1:
2284
                                $actions .= "<a href='exercise_show.php?".api_get_cidreq()."&action=edit&id=$id'>".
2285
                                    Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit'));
2286
                                $actions .= '</a>';
2287
                                $revisedLabel = Display::label(
2288
                                    get_lang('Validated'),
2289
                                    'success'
2290
                                );
2291
                                break;
2292
                            case 2: //finished but not marked as such
2293
                                $actions .= '<a href="exercise_report.php?'
2294
                                    .api_get_cidreq()
2295
                                    .'&exerciseId='
2296
                                    .$exercise_id
2297
                                    .'&a=close&id='
2298
                                    .$id
2299
                                    .'">'.
2300
                                    Display::getMdiIcon(ActionIcon::LOCK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Mark attempt as closed'));
2301
                                $actions .= '</a>';
2302
                                $revisedLabel = Display::label(
2303
                                    get_lang('Unclosed'),
2304
                                    'warning'
2305
                                );
2306
                                break;
2307
                            case 3: //still ongoing
2308
                                $actions .= Display::getMdiIcon('clock', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Attempt still going on. Please wait.'));
2309
                                $actions .= '';
2310
                                $revisedLabel = Display::label(
2311
                                    get_lang('Ongoing'),
2312
                                    'danger'
2313
                                );
2314
                                break;
2315
                        }
2316
2317
                        if (2 == $filter) {
2318
                            $actions .= ' <a href="exercise_history.php?'.api_get_cidreq().'&exe_id='.$id.'">'.
2319
                                Display::getMdiIcon('clipboard-text-clock', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('View changes history')
2320
                                ).'</a>';
2321
                        }
2322
2323
                        // Admin can always delete the attempt
2324
                        if ((false == $locked || api_is_platform_admin()) && !api_is_student_boss()) {
2325
                            $ip = Tracking::get_ip_from_user_event(
2326
                                $results[$i]['exe_user_id'],
2327
                                api_get_utc_datetime(),
2328
                                false
2329
                            );
2330
                            $actions .= '<a href="http://www.whatsmyip.org/ip-geo-location/?ip='.$ip.'" target="_blank">'
2331
                                .Display::getMdiIcon('information', 'ch-tool-icon', null, ICON_SIZE_SMALL, $ip)
2332
                                .'</a>';
2333
2334
                            $recalculateUrl = api_get_path(WEB_CODE_PATH).'exercise/recalculate.php?'.
2335
                                api_get_cidreq().'&'.
2336
                                http_build_query([
2337
                                    'id' => $id,
2338
                                    'exercise' => $exercise_id,
2339
                                    'user' => $results[$i]['exe_user_id'],
2340
                                ]);
2341
                            $actions .= Display::url(
2342
                                Display::getMdiIcon(ActionIcon::REFRESH, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Recalculate results')),
2343
                                $recalculateUrl,
2344
                                [
2345
                                    'data-exercise' => $exercise_id,
2346
                                    'data-user' => $results[$i]['exe_user_id'],
2347
                                    'data-id' => $id,
2348
                                    'class' => 'exercise-recalculate',
2349
                                ]
2350
                            );
2351
2352
                            $filterByUser = isset($_GET['filter_by_user']) ? (int) $_GET['filter_by_user'] : 0;
2353
                            $delete_link = '<a
2354
                                href="exercise_report.php?'.api_get_cidreq().'&filter_by_user='.$filterByUser.'&filter='.$filter.'&exerciseId='.$exercise_id.'&delete=delete&did='.$id.'"
2355
                                onclick=
2356
                                "javascript:if(!confirm(\''.sprintf(addslashes(get_lang('Delete attempt?')), $results[$i]['username'], $dt).'\')) return false;"
2357
                                >';
2358
                            $delete_link .= Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, addslashes(get_lang('Delete'))).'</a>';
2359
2360
                            if (api_is_drh() && !api_is_platform_admin()) {
2361
                                $delete_link = null;
2362
                            }
2363
                            if (api_is_session_admin()) {
2364
                                $delete_link = '';
2365
                            }
2366
                            if (3 == $revised) {
2367
                                $delete_link = null;
2368
                            }
2369
                            $actions .= $delete_link;
2370
                        }
2371
                    } else {
2372
                        $attempt_url = api_get_path(WEB_CODE_PATH).'exercise/result.php?'.api_get_cidreq().'&id='.$results[$i]['exe_id'].'&sid='.$sessionId;
2373
                        $attempt_link = Display::url(
2374
                            get_lang('Show'),
2375
                            $attempt_url,
2376
                            [
2377
                                'class' => 'ajax btn btn--plain',
2378
                                'data-title' => get_lang('Show'),
2379
                            ]
2380
                        );
2381
                        $actions .= $attempt_link;
2382
                    }
2383
                    $actions .= '</div>';
2384
2385
                    if (!empty($userExtraFieldsToAdd)) {
2386
                        foreach ($userExtraFieldsToAdd as $variable) {
2387
                            $extraFieldValue = new ExtraFieldValue('user');
2388
                            $values = $extraFieldValue->get_values_by_handler_and_field_variable(
2389
                                $results[$i]['user_id'],
2390
                                $variable
2391
                            );
2392
                            if (isset($values['value'])) {
2393
                                $results[$i][$variable] = $values['value'];
2394
                            }
2395
                        }
2396
                    }
2397
2398
                    $exeId = $results[$i]['exe_id'];
2399
                    $results[$i]['id'] = $exeId;
2400
                    $category_list = [];
2401
                    if ($is_allowedToEdit) {
2402
                        $sessionName = '';
2403
                        $sessionStartAccessDate = '';
2404
                        if (!empty($results[$i]['session_id'])) {
2405
                            $sessionInfo = api_get_session_info($results[$i]['session_id']);
2406
                            if (!empty($sessionInfo)) {
2407
                                $sessionName = $sessionInfo['name'];
2408
                                $sessionStartAccessDate = api_get_local_time($sessionInfo['access_start_date']);
2409
                            }
2410
                        }
2411
2412
                        $objExercise = new Exercise($courseId);
2413
                        if ($showExerciseCategories) {
2414
                            // Getting attempt info
2415
                            $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
2416
                            if (!empty($exercise_stat_info['data_tracking'])) {
2417
                                $question_list = explode(',', $exercise_stat_info['data_tracking']);
2418
                                if (!empty($question_list)) {
2419
                                    foreach ($question_list as $questionId) {
2420
                                        $objQuestionTmp = Question::read($questionId, $objExercise->course);
2421
                                        // We're inside *one* question. Go through each possible answer for this question
2422
                                        $result = $objExercise->manage_answer(
2423
                                            $exeId,
2424
                                            $questionId,
2425
                                            null,
2426
                                            'exercise_result',
2427
                                            false,
2428
                                            false,
2429
                                            true,
2430
                                            false,
2431
                                            $objExercise->selectPropagateNeg(),
2432
                                            null,
2433
                                            true
2434
                                        );
2435
2436
                                        $my_total_score = $result['score'];
2437
                                        $my_total_weight = $result['weight'];
2438
2439
                                        // Category report
2440
                                        $category_was_added_for_this_test = false;
2441
                                        if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
2442
                                            if (!isset($category_list[$objQuestionTmp->category]['score'])) {
2443
                                                $category_list[$objQuestionTmp->category]['score'] = 0;
2444
                                            }
2445
                                            if (!isset($category_list[$objQuestionTmp->category]['total'])) {
2446
                                                $category_list[$objQuestionTmp->category]['total'] = 0;
2447
                                            }
2448
                                            $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
2449
                                            $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
2450
                                            $category_was_added_for_this_test = true;
2451
                                        }
2452
2453
                                        if (isset($objQuestionTmp->category_list) &&
2454
                                            !empty($objQuestionTmp->category_list)
2455
                                        ) {
2456
                                            foreach ($objQuestionTmp->category_list as $category_id) {
2457
                                                $category_list[$category_id]['score'] += $my_total_score;
2458
                                                $category_list[$category_id]['total'] += $my_total_weight;
2459
                                                $category_was_added_for_this_test = true;
2460
                                            }
2461
                                        }
2462
2463
                                        // No category for this question!
2464
                                        if (false == $category_was_added_for_this_test) {
2465
                                            if (!isset($category_list['none']['score'])) {
2466
                                                $category_list['none']['score'] = 0;
2467
                                            }
2468
                                            if (!isset($category_list['none']['total'])) {
2469
                                                $category_list['none']['total'] = 0;
2470
                                            }
2471
2472
                                            $category_list['none']['score'] += $my_total_score;
2473
                                            $category_list['none']['total'] += $my_total_weight;
2474
                                        }
2475
                                    }
2476
                                }
2477
                            }
2478
                        }
2479
2480
                        foreach ($category_list as $categoryId => $result) {
2481
                            $scoreToDisplay = self::show_score(
2482
                                $result['score'],
2483
                                $result['total'],
2484
                                true,
2485
                                true,
2486
                                false,
2487
                                false,
2488
                                $decimalSeparator,
2489
                                $thousandSeparator,
2490
                                $roundValues
2491
                            );
2492
                            $results[$i]['category_'.$categoryId] = $scoreToDisplay;
2493
                            $results[$i]['category_'.$categoryId.'_score_percentage'] = self::show_score(
2494
                                $result['score'],
2495
                                $result['total'],
2496
                                true,
2497
                                true,
2498
                                true, // $show_only_percentage = false
2499
                                true, // hide % sign
2500
                                $decimalSeparator,
2501
                                $thousandSeparator,
2502
                                $roundValues
2503
                            );
2504
                            $results[$i]['category_'.$categoryId.'_only_score'] = $result['score'];
2505
                            $results[$i]['category_'.$categoryId.'_total'] = $result['total'];
2506
                        }
2507
                        $results[$i]['session'] = $sessionName;
2508
                        $results[$i]['session_access_start_date'] = $sessionStartAccessDate;
2509
                        $results[$i]['status'] = $revisedLabel;
2510
                        $results[$i]['score'] = $score;
2511
                        $results[$i]['score_percentage'] = self::show_score(
2512
                            $my_res,
2513
                            $my_total,
2514
                            true,
2515
                            true,
2516
                            true,
2517
                            true,
2518
                            $decimalSeparator,
2519
                            $thousandSeparator,
2520
                            $roundValues
2521
                        );
2522
2523
                        if ($roundValues) {
2524
                            $whole = floor($my_res); // 1
2525
                            $fraction = $my_res - $whole; // .25
2526
                            if ($fraction >= 0.5) {
2527
                                $onlyScore = ceil($my_res);
2528
                            } else {
2529
                                $onlyScore = round($my_res);
2530
                            }
2531
                        } else {
2532
                            $onlyScore = $scoreDisplay->format_score(
2533
                                $my_res,
2534
                                false,
2535
                                $decimalSeparator,
2536
                                $thousandSeparator
2537
                            );
2538
                        }
2539
2540
                        $results[$i]['only_score'] = $onlyScore;
2541
2542
                        if ($roundValues) {
2543
                            $whole = floor($my_total); // 1
2544
                            $fraction = $my_total - $whole; // .25
2545
                            if ($fraction >= 0.5) {
2546
                                $onlyTotal = ceil($my_total);
2547
                            } else {
2548
                                $onlyTotal = round($my_total);
2549
                            }
2550
                        } else {
2551
                            $onlyTotal = $scoreDisplay->format_score(
2552
                                $my_total,
2553
                                false,
2554
                                $decimalSeparator,
2555
                                $thousandSeparator
2556
                            );
2557
                        }
2558
                        $results[$i]['total'] = $onlyTotal;
2559
                        $results[$i]['lp'] = $lp_name;
2560
                        $results[$i]['actions'] = $actions;
2561
                        $listInfo[] = $results[$i];
2562
                    } else {
2563
                        $results[$i]['status'] = $revisedLabel;
2564
                        $results[$i]['score'] = $score;
2565
                        $results[$i]['actions'] = $actions;
2566
                        $listInfo[] = $results[$i];
2567
                    }
2568
                }
2569
            }
2570
        }
2571
2572
        return $listInfo;
2573
    }
2574
2575
    /**
2576
     * @param $score
2577
     * @param $weight
2578
     *
2579
     * @return array
2580
     */
2581
    public static function convertScoreToPlatformSetting($score, $weight)
2582
    {
2583
        $maxNote = api_get_setting('exercise_max_score');
2584
        $minNote = api_get_setting('exercise_min_score');
2585
2586
        if ('' != $maxNote && '' != $minNote) {
2587
            if (!empty($weight) && (float) $weight !== (float) 0) {
2588
                $score = $minNote + ($maxNote - $minNote) * $score / $weight;
2589
            } else {
2590
                $score = $minNote;
2591
            }
2592
            $weight = $maxNote;
2593
        }
2594
2595
        return ['score' => $score, 'weight' => $weight];
2596
    }
2597
2598
    /**
2599
     * Converts the score with the exercise_max_note and exercise_min_score
2600
     * the platform settings + formats the results using the float_format function.
2601
     *
2602
     * @param float  $score
2603
     * @param float  $weight
2604
     * @param bool   $show_percentage       show percentage or not
2605
     * @param bool   $use_platform_settings use or not the platform settings
2606
     * @param bool   $show_only_percentage
2607
     * @param bool   $hidePercentageSign    hide "%" sign
2608
     * @param string $decimalSeparator
2609
     * @param string $thousandSeparator
2610
     * @param bool   $roundValues           This option rounds the float values into a int using ceil()
2611
     * @param bool   $removeEmptyDecimals
2612
     *
2613
     * @return string an html with the score modified
2614
     */
2615
    public static function show_score(
2616
        $score,
2617
        $weight,
2618
        $show_percentage = true,
2619
        $use_platform_settings = true,
2620
        $show_only_percentage = false,
2621
        $hidePercentageSign = false,
2622
        $decimalSeparator = '.',
2623
        $thousandSeparator = ',',
2624
        $roundValues = false,
2625
        $removeEmptyDecimals = false
2626
    ) {
2627
        if (is_null($score) && is_null($weight)) {
2628
            return '-';
2629
        }
2630
2631
        $decimalSeparator = empty($decimalSeparator) ? '.' : $decimalSeparator;
2632
        $thousandSeparator = empty($thousandSeparator) ? ',' : $thousandSeparator;
2633
2634
        if ($use_platform_settings) {
2635
            $result = self::convertScoreToPlatformSetting($score, $weight);
2636
            $score = $result['score'];
2637
            $weight = $result['weight'];
2638
        }
2639
2640
        $percentage = (100 * $score) / (0 != $weight ? $weight : 1);
2641
        // Formats values
2642
        $percentage = float_format($percentage, 1);
2643
        $score = float_format($score, 1);
2644
        $weight = float_format($weight, 1);
2645
2646
        if ($roundValues) {
2647
            $whole = floor($percentage); // 1
2648
            $fraction = $percentage - $whole; // .25
2649
2650
            // Formats values
2651
            if ($fraction >= 0.5) {
2652
                $percentage = ceil($percentage);
2653
            } else {
2654
                $percentage = round($percentage);
2655
            }
2656
2657
            $whole = floor($score); // 1
2658
            $fraction = $score - $whole; // .25
2659
            if ($fraction >= 0.5) {
2660
                $score = ceil($score);
2661
            } else {
2662
                $score = round($score);
2663
            }
2664
2665
            $whole = floor($weight); // 1
2666
            $fraction = $weight - $whole; // .25
2667
            if ($fraction >= 0.5) {
2668
                $weight = ceil($weight);
2669
            } else {
2670
                $weight = round($weight);
2671
            }
2672
        } else {
2673
            // Formats values
2674
            $percentage = float_format($percentage, 1, $decimalSeparator, $thousandSeparator);
2675
            $score = float_format($score, 1, $decimalSeparator, $thousandSeparator);
2676
            $weight = float_format($weight, 1, $decimalSeparator, $thousandSeparator);
2677
        }
2678
2679
        if ($show_percentage) {
2680
            $percentageSign = ' %';
2681
            if ($hidePercentageSign) {
2682
                $percentageSign = '';
2683
            }
2684
            $html = $percentage."$percentageSign ($score / $weight)";
2685
            if ($show_only_percentage) {
2686
                $html = $percentage.$percentageSign;
2687
            }
2688
        } else {
2689
            if ($removeEmptyDecimals) {
2690
                if (ScoreDisplay::hasEmptyDecimals($weight)) {
2691
                    $weight = round($weight);
2692
                }
2693
            }
2694
            $html = $score.' / '.$weight;
2695
        }
2696
2697
        // Over write score
2698
        $scoreBasedInModel = self::convertScoreToModel($percentage);
2699
        if (!empty($scoreBasedInModel)) {
2700
            $html = $scoreBasedInModel;
2701
        }
2702
2703
        // Ignore other formats and use the configuration['exercise_score_format'] value
2704
        // But also keep the round values settings.
2705
        $format = (int) api_get_setting('exercise.exercise_score_format');
2706
        if (!empty($format)) {
2707
            $html = ScoreDisplay::instance()->display_score([$score, $weight], $format);
2708
        }
2709
2710
        return Display::span($html, ['class' => 'score_exercise']);
2711
    }
2712
2713
    /**
2714
     * @param array $model
2715
     * @param float $percentage
2716
     *
2717
     * @return string
2718
     */
2719
    public static function getModelStyle($model, $percentage)
2720
    {
2721
        return '<span class="'.$model['css_class'].'">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>';
2722
    }
2723
2724
    /**
2725
     * @param float $percentage value between 0 and 100
2726
     *
2727
     * @return string
2728
     */
2729
    public static function convertScoreToModel($percentage)
2730
    {
2731
        $model = self::getCourseScoreModel();
2732
        if (!empty($model)) {
2733
            $scoreWithGrade = [];
2734
            foreach ($model['score_list'] as $item) {
2735
                if ($percentage >= $item['min'] && $percentage <= $item['max']) {
2736
                    $scoreWithGrade = $item;
2737
                    break;
2738
                }
2739
            }
2740
2741
            if (!empty($scoreWithGrade)) {
2742
                return self::getModelStyle($scoreWithGrade, $percentage);
2743
            }
2744
        }
2745
2746
        return '';
2747
    }
2748
2749
    /**
2750
     * @return array
2751
     */
2752
    public static function getCourseScoreModel()
2753
    {
2754
        $modelList = self::getScoreModels();
2755
        if (empty($modelList)) {
2756
            return [];
2757
        }
2758
2759
        $courseInfo = api_get_course_info();
2760
        if (!empty($courseInfo)) {
2761
            $scoreModelId = api_get_course_setting('score_model_id');
2762
            if (-1 != $scoreModelId) {
2763
                $modelIdList = array_column($modelList['models'], 'id');
2764
                if (in_array($scoreModelId, $modelIdList)) {
2765
                    foreach ($modelList['models'] as $item) {
2766
                        if ($item['id'] == $scoreModelId) {
2767
                            return $item;
2768
                        }
2769
                    }
2770
                }
2771
            }
2772
        }
2773
2774
        return [];
2775
    }
2776
2777
    /**
2778
     * @return array
2779
     */
2780
    public static function getScoreModels()
2781
    {
2782
        return api_get_setting('exercise.score_grade_model', true);
2783
    }
2784
2785
    /**
2786
     * @param float  $score
2787
     * @param float  $weight
2788
     * @param string $passPercentage
2789
     *
2790
     * @return bool
2791
     */
2792
    public static function isSuccessExerciseResult($score, $weight, $passPercentage)
2793
    {
2794
        $percentage = float_format(
2795
            ($score / (0 != $weight ? $weight : 1)) * 100,
2796
            1
2797
        );
2798
        if (isset($passPercentage) && !empty($passPercentage)) {
2799
            if ($percentage >= $passPercentage) {
2800
                return true;
2801
            }
2802
        }
2803
2804
        return false;
2805
    }
2806
2807
    /**
2808
     * @param string $name
2809
     * @param $weight
2810
     * @param $selected
2811
     *
2812
     * @return bool
2813
     */
2814
    public static function addScoreModelInput(
2815
        FormValidator $form,
2816
        $name,
2817
        $weight,
2818
        $selected
2819
    ) {
2820
        $model = self::getCourseScoreModel();
2821
        if (empty($model)) {
2822
            return false;
2823
        }
2824
2825
        /** @var HTML_QuickForm_select $element */
2826
        $element = $form->createElement(
2827
            'select',
2828
            $name,
2829
            get_lang('Score'),
2830
            [],
2831
            ['class' => 'exercise_mark_select']
2832
        );
2833
2834
        foreach ($model['score_list'] as $item) {
2835
            $i = api_number_format($item['score_to_qualify'] / 100 * $weight, 2);
2836
            $label = self::getModelStyle($item, $i);
2837
            $attributes = [
2838
                'class' => $item['css_class'],
2839
            ];
2840
            if ($selected == $i) {
2841
                $attributes['selected'] = 'selected';
2842
            }
2843
            $element->addOption($label, $i, $attributes);
2844
        }
2845
        $form->addElement($element);
2846
    }
2847
2848
    /**
2849
     * @return string
2850
     */
2851
    public static function getJsCode()
2852
    {
2853
        // Filling the scores with the right colors.
2854
        $models = self::getCourseScoreModel();
2855
        $cssListToString = '';
2856
        if (!empty($models)) {
2857
            $cssList = array_column($models['score_list'], 'css_class');
2858
            $cssListToString = implode(' ', $cssList);
2859
        }
2860
2861
        if (empty($cssListToString)) {
2862
            return '';
2863
        }
2864
        $js = <<<EOT
2865
2866
        function updateSelect(element) {
2867
            var spanTag = element.parent().find('span.filter-option');
2868
            var value = element.val();
2869
            var selectId = element.attr('id');
2870
            var optionClass = $('#' + selectId + ' option[value="'+value+'"]').attr('class');
2871
            spanTag.removeClass('$cssListToString');
2872
            spanTag.addClass(optionClass);
2873
        }
2874
2875
        $(function() {
2876
            // Loading values
2877
            $('.exercise_mark_select').on('loaded.bs.select', function() {
2878
                updateSelect($(this));
2879
            });
2880
            // On change
2881
            $('.exercise_mark_select').on('changed.bs.select', function() {
2882
                updateSelect($(this));
2883
            });
2884
        });
2885
EOT;
2886
2887
        return $js;
2888
    }
2889
2890
    /**
2891
     * @param float  $score
2892
     * @param float  $weight
2893
     * @param string $pass_percentage
2894
     *
2895
     * @return string
2896
     */
2897
    public static function showSuccessMessage($score, $weight, $pass_percentage)
2898
    {
2899
        $res = '';
2900
        if (self::isPassPercentageEnabled($pass_percentage)) {
2901
            $isSuccess = self::isSuccessExerciseResult(
2902
                $score,
2903
                $weight,
2904
                $pass_percentage
2905
            );
2906
2907
            if ($isSuccess) {
2908
                $html = get_lang('Congratulations you passed the test!');
2909
                $icon = Display::getMdiIcon('check-circle', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Correct'));
2910
            } else {
2911
                $html = get_lang('You didn\'t reach the minimum score');
2912
                $icon = Display::getMdiIcon('alert', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Wrong'));
2913
            }
2914
            $html = Display::tag('h4', $html);
2915
            $html .= Display::tag(
2916
                'h5',
2917
                $icon,
2918
                ['style' => 'width:40px; padding:2px 10px 0px 0px']
2919
            );
2920
            $res = $html;
2921
        }
2922
2923
        return $res;
2924
    }
2925
2926
    /**
2927
     * Return true if pass_pourcentage activated (we use the pass pourcentage feature
2928
     * return false if pass_percentage = 0 (we don't use the pass pourcentage feature.
2929
     *
2930
     * @param $value
2931
     *
2932
     * @return bool
2933
     *              In this version, pass_percentage and show_success_message are disabled if
2934
     *              pass_percentage is set to 0
2935
     */
2936
    public static function isPassPercentageEnabled($value)
2937
    {
2938
        return $value > 0;
2939
    }
2940
2941
    /**
2942
     * Converts a numeric value in a percentage example 0.66666 to 66.67 %.
2943
     *
2944
     * @param $value
2945
     *
2946
     * @return float Converted number
2947
     */
2948
    public static function convert_to_percentage($value)
2949
    {
2950
        $return = '-';
2951
        if ('' != $value) {
2952
            $return = float_format($value * 100, 1).' %';
2953
        }
2954
2955
        return $return;
2956
    }
2957
2958
    /**
2959
     * Getting all active exercises from a course from a session
2960
     * (if a session_id is provided we will show all the exercises in the course +
2961
     * all exercises in the session).
2962
     *
2963
     * @param array  $course_info
2964
     * @param int    $session_id
2965
     * @param bool   $check_publication_dates
2966
     * @param string $search                  Search exercise name
2967
     * @param bool   $search_all_sessions     Search exercises in all sessions
2968
     * @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...
2969
     *                  1 = only active exercises,
2970
     *                  2 = all exercises
2971
     *                  3 = active <> -1
2972
     *
2973
     * @return CQuiz[]
2974
     */
2975
    public static function get_all_exercises(
2976
        $course_info = null,
2977
        $session_id = 0,
2978
        $check_publication_dates = false,
2979
        $search = '',
2980
        $search_all_sessions = false,
2981
        $active = 2
2982
    ) {
2983
        $course_id = api_get_course_int_id();
2984
        if (!empty($course_info) && !empty($course_info['real_id'])) {
2985
            $course_id = $course_info['real_id'];
2986
        }
2987
2988
        if (-1 == $session_id) {
2989
            $session_id = 0;
2990
        }
2991
        $course = api_get_course_entity($course_id);
2992
        $session = api_get_session_entity($session_id);
2993
2994
        if (null === $course) {
2995
            return [];
2996
        }
2997
2998
        $repo = Container::getQuizRepository();
2999
3000
        return $repo->findAllByCourse($course, $session, (string) $search, $active);
3001
3002
        // Show courses by active status
3003
        /*if (true == $search_all_sessions) {
3004
            $conditions = [
3005
                'where' => [
3006
                    $active_sql.' c_id = ? '.$needle_where.$timeConditions => [
3007
                        $course_id,
3008
                        $needle,
3009
                    ],
3010
                ],
3011
                'order' => 'title',
3012
            ];
3013
        } else {
3014
            if (empty($session_id)) {
3015
                $conditions = [
3016
                    'where' => [
3017
                        $active_sql.' (session_id = 0 OR session_id IS NULL) AND c_id = ? '.$needle_where.$timeConditions => [
3018
                            $course_id,
3019
                            $needle,
3020
                        ],
3021
                    ],
3022
                    'order' => 'title',
3023
                ];
3024
            } else {
3025
                $conditions = [
3026
                    'where' => [
3027
                        $active_sql.' (session_id = 0 OR session_id IS NULL OR session_id = ? ) AND c_id = ? '.$needle_where.$timeConditions => [
3028
                            $session_id,
3029
                            $course_id,
3030
                            $needle,
3031
                        ],
3032
                    ],
3033
                    'order' => 'title',
3034
                ];
3035
            }
3036
        }
3037
3038
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
3039
3040
        return Database::select('*', $table, $conditions);*/
3041
    }
3042
3043
    /**
3044
     * Getting all exercises (active only or all)
3045
     * from a course from a session
3046
     * (if a session_id is provided we will show all the exercises in the
3047
     * course + all exercises in the session).
3048
     */
3049
    public static function get_all_exercises_for_course_id(
3050
        int $courseId,
3051
        int $sessionId = 0,
3052
        bool $onlyActiveExercises = true
3053
    ): array {
3054
3055
        if (!($courseId > 0)) {
3056
            return [];
3057
        }
3058
3059
        $course = api_get_course_entity($courseId);
3060
        $session = api_get_session_entity($sessionId);
3061
3062
        $repo = Container::getQuizRepository();
3063
3064
        $qb = $repo->getResourcesByCourse($course, $session);
3065
3066
        if ($onlyActiveExercises) {
3067
            $qb->andWhere('resource.active = 1');
3068
        } else {
3069
            $qb->andWhere('resource.active IN (1, 0)');
3070
        }
3071
3072
        $qb->orderBy('resource.title', 'ASC');
3073
3074
        $exercises = $qb->getQuery()->getResult();
3075
3076
        $exerciseList = [];
3077
        foreach ($exercises as $exercise) {
3078
            $exerciseList[] = [
3079
                'iid' => $exercise->getIid(),
3080
                'title' => $exercise->getTitle(),
3081
            ];
3082
        }
3083
3084
        return $exerciseList;
3085
    }
3086
3087
    /**
3088
     * Gets the position of the score based in a given score (result/weight)
3089
     * and the exe_id based in the user list
3090
     * (NO Exercises in LPs ).
3091
     *
3092
     * @param float  $my_score      user score to be compared *attention*
3093
     *                              $my_score = score/weight and not just the score
3094
     * @param int    $my_exe_id     exe id of the exercise
3095
     *                              (this is necessary because if 2 students have the same score the one
3096
     *                              with the minor exe_id will have a best position, just to be fair and FIFO)
3097
     * @param int    $exercise_id
3098
     * @param string $course_code
3099
     * @param int    $session_id
3100
     * @param array  $user_list
3101
     * @param bool   $return_string
3102
     *
3103
     * @return int the position of the user between his friends in a course
3104
     *             (or course within a session)
3105
     */
3106
    public static function get_exercise_result_ranking(
3107
        $my_score,
3108
        $my_exe_id,
3109
        $exercise_id,
3110
        $course_code,
3111
        $session_id = 0,
3112
        $user_list = [],
3113
        $return_string = true
3114
    ) {
3115
        //No score given we return
3116
        if (is_null($my_score)) {
3117
            return '-';
3118
        }
3119
        if (empty($user_list)) {
3120
            return '-';
3121
        }
3122
3123
        $best_attempts = [];
3124
        foreach ($user_list as $user_data) {
3125
            $user_id = $user_data['user_id'];
3126
            $best_attempts[$user_id] = self::get_best_attempt_by_user(
3127
                $user_id,
3128
                $exercise_id,
3129
                $course_code,
3130
                $session_id
3131
            );
3132
        }
3133
3134
        if (empty($best_attempts)) {
3135
            return 1;
3136
        } else {
3137
            $position = 1;
3138
            $my_ranking = [];
3139
            foreach ($best_attempts as $user_id => $result) {
3140
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3141
                    $my_ranking[$user_id] = $result['score'] / $result['max_score'];
3142
                } else {
3143
                    $my_ranking[$user_id] = 0;
3144
                }
3145
            }
3146
            //if (!empty($my_ranking)) {
3147
            asort($my_ranking);
3148
            $position = count($my_ranking);
3149
            if (!empty($my_ranking)) {
3150
                foreach ($my_ranking as $user_id => $ranking) {
3151
                    if ($my_score >= $ranking) {
3152
                        if ($my_score == $ranking && isset($best_attempts[$user_id]['exe_id'])) {
3153
                            $exe_id = $best_attempts[$user_id]['exe_id'];
3154
                            if ($my_exe_id < $exe_id) {
3155
                                $position--;
3156
                            }
3157
                        } else {
3158
                            $position--;
3159
                        }
3160
                    }
3161
                }
3162
            }
3163
            //}
3164
            $return_value = [
3165
                'position' => $position,
3166
                'count' => count($my_ranking),
3167
            ];
3168
3169
            if ($return_string) {
3170
                if (!empty($position) && !empty($my_ranking)) {
3171
                    $return_value = $position.'/'.count($my_ranking);
3172
                } else {
3173
                    $return_value = '-';
3174
                }
3175
            }
3176
3177
            return $return_value;
3178
        }
3179
    }
3180
3181
    /**
3182
     * Gets the position of the score based in a given score (result/weight) and the exe_id based in all attempts
3183
     * (NO Exercises in LPs ) old functionality by attempt.
3184
     *
3185
     * @param   float   user score to be compared attention => score/weight
3186
     * @param   int     exe id of the exercise
3187
     * (this is necessary because if 2 students have the same score the one
3188
     * with the minor exe_id will have a best position, just to be fair and FIFO)
3189
     * @param   int     exercise id
3190
     * @param   string  course code
3191
     * @param   int     session id
3192
     * @param bool $return_string
3193
     *
3194
     * @return int the position of the user between his friends in a course (or course within a session)
3195
     */
3196
    public static function get_exercise_result_ranking_by_attempt(
3197
        $my_score,
3198
        $my_exe_id,
3199
        $exercise_id,
3200
        $courseId,
3201
        $session_id = 0,
3202
        $return_string = true
3203
    ) {
3204
        if (empty($session_id)) {
3205
            $session_id = 0;
3206
        }
3207
        if (is_null($my_score)) {
3208
            return '-';
3209
        }
3210
        $user_results = Event::get_all_exercise_results(
3211
            $exercise_id,
3212
            $courseId,
3213
            $session_id,
3214
            false
3215
        );
3216
        $position_data = [];
3217
        if (empty($user_results)) {
3218
            return 1;
3219
        } else {
3220
            $position = 1;
3221
            $my_ranking = [];
3222
            foreach ($user_results as $result) {
3223
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3224
                    $my_ranking[$result['exe_id']] = $result['score'] / $result['max_score'];
3225
                } else {
3226
                    $my_ranking[$result['exe_id']] = 0;
3227
                }
3228
            }
3229
            asort($my_ranking);
3230
            $position = count($my_ranking);
3231
            if (!empty($my_ranking)) {
3232
                foreach ($my_ranking as $exe_id => $ranking) {
3233
                    if ($my_score >= $ranking) {
3234
                        if ($my_score == $ranking) {
3235
                            if ($my_exe_id < $exe_id) {
3236
                                $position--;
3237
                            }
3238
                        } else {
3239
                            $position--;
3240
                        }
3241
                    }
3242
                }
3243
            }
3244
            $return_value = [
3245
                'position' => $position,
3246
                'count' => count($my_ranking),
3247
            ];
3248
3249
            if ($return_string) {
3250
                if (!empty($position) && !empty($my_ranking)) {
3251
                    return $position.'/'.count($my_ranking);
3252
                }
3253
            }
3254
3255
            return $return_value;
3256
        }
3257
    }
3258
3259
    /**
3260
     * Get the best attempt in a exercise (NO Exercises in LPs ).
3261
     *
3262
     * @param int $exercise_id
3263
     * @param int $courseId
3264
     * @param int $session_id
3265
     *
3266
     * @return array
3267
     */
3268
    public static function get_best_attempt_in_course($exercise_id, $courseId, $session_id)
3269
    {
3270
        $user_results = Event::get_all_exercise_results(
3271
            $exercise_id,
3272
            $courseId,
3273
            $session_id,
3274
            false
3275
        );
3276
3277
        $best_score_data = [];
3278
        $best_score = 0;
3279
        if (!empty($user_results)) {
3280
            foreach ($user_results as $result) {
3281
                if (!empty($result['max_score']) &&
3282
                    0 != intval($result['max_score'])
3283
                ) {
3284
                    $score = $result['score'] / $result['max_score'];
3285
                    if ($score >= $best_score) {
3286
                        $best_score = $score;
3287
                        $best_score_data = $result;
3288
                    }
3289
                }
3290
            }
3291
        }
3292
3293
        return $best_score_data;
3294
    }
3295
3296
    /**
3297
     * Get the best score in a exercise (NO Exercises in LPs ).
3298
     *
3299
     * @param int $user_id
3300
     * @param int $exercise_id
3301
     * @param int $courseId
3302
     * @param int $session_id
3303
     *
3304
     * @return array
3305
     */
3306
    public static function get_best_attempt_by_user(
3307
        $user_id,
3308
        $exercise_id,
3309
        $courseId,
3310
        $session_id
3311
    ) {
3312
        $user_results = Event::get_all_exercise_results(
3313
            $exercise_id,
3314
            $courseId,
3315
            $session_id,
3316
            false,
3317
            $user_id
3318
        );
3319
        $best_score_data = [];
3320
        $best_score = 0;
3321
        if (!empty($user_results)) {
3322
            foreach ($user_results as $result) {
3323
                if (!empty($result['max_score']) && 0 != (float) $result['max_score']) {
3324
                    $score = $result['score'] / $result['max_score'];
3325
                    if ($score >= $best_score) {
3326
                        $best_score = $score;
3327
                        $best_score_data = $result;
3328
                    }
3329
                }
3330
            }
3331
        }
3332
3333
        return $best_score_data;
3334
    }
3335
3336
    /**
3337
     * Get average score (NO Exercises in LPs ).
3338
     *
3339
     * @param    int    exercise id
3340
     * @param int $courseId
3341
     * @param    int    session id
3342
     *
3343
     * @return float Average score
3344
     */
3345
    public static function get_average_score($exercise_id, $courseId, $session_id)
3346
    {
3347
        $user_results = Event::get_all_exercise_results(
3348
            $exercise_id,
3349
            $courseId,
3350
            $session_id
3351
        );
3352
        $avg_score = 0;
3353
        if (!empty($user_results)) {
3354
            foreach ($user_results as $result) {
3355
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3356
                    $score = $result['score'] / $result['max_score'];
3357
                    $avg_score += $score;
3358
                }
3359
            }
3360
            $avg_score = float_format($avg_score / count($user_results), 1);
3361
        }
3362
3363
        return $avg_score;
3364
    }
3365
3366
    /**
3367
     * Get average score by score (NO Exercises in LPs ).
3368
     *
3369
     * @param int $courseId
3370
     * @param    int    session id
3371
     *
3372
     * @return float Average score
3373
     */
3374
    public static function get_average_score_by_course($courseId, $session_id)
3375
    {
3376
        $user_results = Event::get_all_exercise_results_by_course(
3377
            $courseId,
3378
            $session_id,
3379
            false
3380
        );
3381
        $avg_score = 0;
3382
        if (!empty($user_results)) {
3383
            foreach ($user_results as $result) {
3384
                if (!empty($result['max_score']) && 0 != intval(
3385
                        $result['max_score']
3386
                    )
3387
                ) {
3388
                    $score = $result['score'] / $result['max_score'];
3389
                    $avg_score += $score;
3390
                }
3391
            }
3392
            // We assume that all max_score
3393
            $avg_score = $avg_score / count($user_results);
3394
        }
3395
3396
        return $avg_score;
3397
    }
3398
3399
    /**
3400
     * @param int $user_id
3401
     * @param int $courseId
3402
     * @param int $session_id
3403
     *
3404
     * @return float|int
3405
     */
3406
    public static function get_average_score_by_course_by_user(
3407
        $user_id,
3408
        $courseId,
3409
        $session_id
3410
    ) {
3411
        $user_results = Event::get_all_exercise_results_by_user(
3412
            $user_id,
3413
            $courseId,
3414
            $session_id
3415
        );
3416
        $avg_score = 0;
3417
        if (!empty($user_results)) {
3418
            foreach ($user_results as $result) {
3419
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3420
                    $score = $result['score'] / $result['max_score'];
3421
                    $avg_score += $score;
3422
                }
3423
            }
3424
            // We assume that all max_score
3425
            $avg_score = ($avg_score / count($user_results));
3426
        }
3427
3428
        return $avg_score;
3429
    }
3430
3431
    /**
3432
     * Get average score by score (NO Exercises in LPs ).
3433
     *
3434
     * @param int $exercise_id
3435
     * @param int $courseId
3436
     * @param int $session_id
3437
     * @param int $user_count
3438
     *
3439
     * @return float Best average score
3440
     */
3441
    public static function get_best_average_score_by_exercise(
3442
        $exercise_id,
3443
        $courseId,
3444
        $session_id,
3445
        $user_count
3446
    ) {
3447
        $user_results = Event::get_best_exercise_results_by_user(
3448
            $exercise_id,
3449
            $courseId,
3450
            $session_id
3451
        );
3452
        $avg_score = 0;
3453
        if (!empty($user_results)) {
3454
            foreach ($user_results as $result) {
3455
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3456
                    $score = $result['score'] / $result['max_score'];
3457
                    $avg_score += $score;
3458
                }
3459
            }
3460
            // We asumme that all max_score
3461
            if (!empty($user_count)) {
3462
                $avg_score = float_format($avg_score / $user_count, 1) * 100;
3463
            } else {
3464
                $avg_score = 0;
3465
            }
3466
        }
3467
3468
        return $avg_score;
3469
    }
3470
3471
    /**
3472
     * Get average score by score (NO Exercises in LPs ).
3473
     *
3474
     * @param int $exercise_id
3475
     * @param int $courseId
3476
     * @param int $session_id
3477
     *
3478
     * @return float Best average score
3479
     */
3480
    public static function getBestScoreByExercise(
3481
        $exercise_id,
3482
        $courseId,
3483
        $session_id
3484
    ) {
3485
        $user_results = Event::get_best_exercise_results_by_user(
3486
            $exercise_id,
3487
            $courseId,
3488
            $session_id
3489
        );
3490
        $avg_score = 0;
3491
        if (!empty($user_results)) {
3492
            foreach ($user_results as $result) {
3493
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3494
                    $score = $result['score'] / $result['max_score'];
3495
                    $avg_score += $score;
3496
                }
3497
            }
3498
        }
3499
3500
        return $avg_score;
3501
    }
3502
3503
    /**
3504
     * Get student results (only in completed exercises) stats by question.
3505
     *
3506
     * @throws \Doctrine\DBAL\Exception
3507
     */
3508
    public static function getStudentStatsByQuestion(
3509
        int $questionId,
3510
        int $exerciseId,
3511
        string $courseCode,
3512
        int $sessionId,
3513
        bool $onlyStudent = false
3514
    ): array
3515
    {
3516
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3517
        $trackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3518
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3519
3520
        $questionId = (int) $questionId;
3521
        $exerciseId = (int) $exerciseId;
3522
        $courseCode = Database::escape_string($courseCode);
3523
        $sessionId = (int) $sessionId;
3524
        $courseId = api_get_course_int_id($courseCode);
3525
3526
        $sql = "SELECT MAX(marks) as max, MIN(marks) as min, AVG(marks) as average
3527
                FROM $trackExercises e ";
3528
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
3529
        if ($onlyStudent) {
3530
            $courseCondition = '';
3531
            if (empty($sessionId)) {
3532
                $courseCondition = "
3533
                INNER JOIN $courseUser c
3534
                ON (
3535
                    e.exe_user_id = c.user_id AND
3536
                    e.c_id = c.c_id AND
3537
                    c.status = ".STUDENT." AND
3538
                    relation_type <> 2
3539
                )";
3540
            } else {
3541
                $sessionRelCourse = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3542
                $courseCondition = "
3543
            INNER JOIN $sessionRelCourse sc
3544
            ON (
3545
                        e.exe_user_id = sc.user_id AND
3546
                        e.c_id = sc.c_id AND
3547
                        e.session_id = sc.session_id AND
3548
                        sc.status = ".SessionEntity::STUDENT."
3549
                )";
3550
            }
3551
            $sql .= $courseCondition;
3552
        }
3553
        $sql .= "
3554
    		INNER JOIN $trackAttempt a
3555
    		ON (
3556
    		    a.exe_id = e.exe_id
3557
            )
3558
    		WHERE
3559
    		    exe_exo_id 	= $exerciseId AND
3560
                e.c_id = $courseId AND
3561
                question_id = $questionId AND
3562
                e.status = ''
3563
                $sessionCondition
3564
            LIMIT 1";
3565
        $result = Database::query($sql);
3566
        $return = [];
3567
        if ($result) {
3568
            $return = Database::fetch_assoc($result);
3569
        }
3570
3571
        return $return;
3572
    }
3573
3574
    /**
3575
     * Get the correct answer count for a fill blanks question.
3576
     *
3577
     * @param int $question_id
3578
     * @param int $exercise_id
3579
     *
3580
     * @return array
3581
     */
3582
    public static function getNumberStudentsFillBlanksAnswerCount(
3583
        $question_id,
3584
        $exercise_id
3585
    ) {
3586
        $listStudentsId = [];
3587
        $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
3588
            api_get_course_id(),
3589
            true
3590
        );
3591
        foreach ($listAllStudentInfo as $i => $listStudentInfo) {
3592
            $listStudentsId[] = $listStudentInfo['user_id'];
3593
        }
3594
3595
        $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3596
            $exercise_id,
3597
            $question_id,
3598
            $listStudentsId,
3599
            '1970-01-01',
3600
            '3000-01-01'
3601
        );
3602
3603
        $arrayCount = [];
3604
3605
        foreach ($listFillTheBlankResult as $resultCount) {
3606
            foreach ($resultCount as $index => $count) {
3607
                //this is only for declare the array index per answer
3608
                $arrayCount[$index] = 0;
3609
            }
3610
        }
3611
3612
        foreach ($listFillTheBlankResult as $resultCount) {
3613
            foreach ($resultCount as $index => $count) {
3614
                $count = (0 === $count) ? 1 : 0;
3615
                $arrayCount[$index] += $count;
3616
            }
3617
        }
3618
3619
        return $arrayCount;
3620
    }
3621
3622
    /**
3623
     * Get the number of questions with answers.
3624
     *
3625
     * @param int    $question_id
3626
     * @param int    $exercise_id
3627
     * @param string $course_code
3628
     * @param int    $session_id
3629
     * @param string $questionType
3630
     *
3631
     * @return int
3632
     */
3633
    public static function get_number_students_question_with_answer_count(
3634
        $question_id,
3635
        $exercise_id,
3636
        $course_code,
3637
        $session_id,
3638
        $questionType = ''
3639
    ) {
3640
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3641
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3642
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3643
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
3644
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3645
3646
        $question_id = intval($question_id);
3647
        $exercise_id = intval($exercise_id);
3648
        $courseId = api_get_course_int_id($course_code);
3649
        $session_id = intval($session_id);
3650
3651
        if (FILL_IN_BLANKS == $questionType) {
3652
            $listStudentsId = [];
3653
            $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
3654
                api_get_course_id(),
3655
                true
3656
            );
3657
            foreach ($listAllStudentInfo as $i => $listStudentInfo) {
3658
                $listStudentsId[] = $listStudentInfo['user_id'];
3659
            }
3660
3661
            $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3662
                $exercise_id,
3663
                $question_id,
3664
                $listStudentsId,
3665
                '1970-01-01',
3666
                '3000-01-01'
3667
            );
3668
3669
            return FillBlanks::getNbResultFillBlankAll($listFillTheBlankResult);
3670
        }
3671
3672
        if (empty($session_id)) {
3673
            $courseCondition = "
3674
            INNER JOIN $courseUser cu
3675
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
3676
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3677
        } else {
3678
            $courseCondition = "
3679
            INNER JOIN $courseUserSession cu
3680
            ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
3681
            $courseConditionWhere = " AND cu.status = ".SessionEntity::STUDENT;
3682
        }
3683
3684
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
3685
        $sql = "SELECT DISTINCT exe_user_id
3686
                FROM $track_exercises e
3687
                INNER JOIN $track_attempt a
3688
                ON (
3689
                    a.exe_id = e.exe_id
3690
                )
3691
                INNER JOIN $courseTable c
3692
                ON (c.id = e.c_id)
3693
                $courseCondition
3694
                WHERE
3695
                    exe_exo_id = $exercise_id AND
3696
                    e.c_id = $courseId AND
3697
                    question_id = $question_id AND
3698
                    answer <> '0' AND
3699
                    e.status = ''
3700
                    $courseConditionWhere
3701
                    $sessionCondition
3702
            ";
3703
        $result = Database::query($sql);
3704
        $return = 0;
3705
        if ($result) {
3706
            $return = Database::num_rows($result);
3707
        }
3708
3709
        return $return;
3710
    }
3711
3712
    /**
3713
     * Get number of answers to hotspot questions.
3714
     */
3715
    public static function getNumberStudentsAnswerHotspotCount(
3716
        int    $answerId,
3717
        int    $questionId,
3718
        int    $exerciseId,
3719
        string $courseCode,
3720
        int $sessionId
3721
    ): int
3722
    {
3723
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3724
        $trackHotspot = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
3725
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3726
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
3727
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3728
3729
        $questionId = (int) $questionId;
3730
        $answerId = (int) $answerId;
3731
        $exerciseId = (int) $exerciseId;
3732
        $courseId = api_get_course_int_id($courseCode);
3733
        $sessionId = (int) $sessionId;
3734
3735
        if (empty($sessionId)) {
3736
            $courseCondition = "
3737
            INNER JOIN $courseUser cu
3738
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
3739
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3740
        } else {
3741
            $courseCondition = "
3742
            INNER JOIN $courseUserSession cu
3743
            ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
3744
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
3745
        }
3746
3747
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
3748
        $sql = "SELECT DISTINCT exe_user_id
3749
                FROM $trackExercises e
3750
                INNER JOIN $trackHotspot a
3751
                ON (a.hotspot_exe_id = e.exe_id)
3752
                INNER JOIN $courseTable c
3753
                ON (a.c_id = c.id)
3754
                $courseCondition
3755
                WHERE
3756
                    exe_exo_id              = $exerciseId AND
3757
                    a.c_id 	= $courseId AND
3758
                    hotspot_answer_id       = $answerId AND
3759
                    hotspot_question_id     = $questionId AND
3760
                    hotspot_correct         =  1 AND
3761
                    e.status                = ''
3762
                    $courseConditionWhere
3763
                    $sessionCondition
3764
            ";
3765
        $result = Database::query($sql);
3766
        $return = 0;
3767
        if ($result) {
3768
            $return = Database::num_rows($result);
3769
        }
3770
3771
        return $return;
3772
    }
3773
3774
    /**
3775
     * @param int    $answer_id
3776
     * @param int    $question_id
3777
     * @param int    $exercise_id
3778
     * @param string $course_code
3779
     * @param int    $session_id
3780
     * @param string $question_type
3781
     * @param string $correct_answer
3782
     * @param string $current_answer
3783
     *
3784
     * @return int
3785
     */
3786
    public static function get_number_students_answer_count(
3787
        $answer_id,
3788
        $question_id,
3789
        $exercise_id,
3790
        $course_code,
3791
        $session_id,
3792
        $question_type = null,
3793
        $correct_answer = null,
3794
        $current_answer = null
3795
    ) {
3796
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3797
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3798
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
3799
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3800
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3801
3802
        $question_id = (int) $question_id;
3803
        $answer_id = (int) $answer_id;
3804
        $exercise_id = (int) $exercise_id;
3805
        $courseId = api_get_course_int_id($course_code);
3806
        $session_id = (int) $session_id;
3807
3808
        switch ($question_type) {
3809
            case FILL_IN_BLANKS:
3810
                $answer_condition = '';
3811
                $select_condition = ' e.exe_id, answer ';
3812
                break;
3813
            case MATCHING:
3814
            case MATCHING_DRAGGABLE:
3815
            default:
3816
                $answer_condition = " answer = $answer_id AND ";
3817
                $select_condition = ' DISTINCT exe_user_id ';
3818
        }
3819
3820
        if (empty($session_id)) {
3821
            $courseCondition = "
3822
            INNER JOIN $courseUser cu
3823
            ON cu.c_id = c.id AND cu.user_id = exe_user_id";
3824
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3825
        } else {
3826
            $courseCondition = "
3827
            INNER JOIN $courseUserSession cu
3828
            ON (cu.c_id = a.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
3829
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
3830
        }
3831
3832
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
3833
        $sql = "SELECT $select_condition
3834
                FROM $track_exercises e
3835
                INNER JOIN $track_attempt a
3836
                ON (
3837
                    a.exe_id = e.exe_id
3838
                )
3839
                INNER JOIN $courseTable c
3840
                ON c.id = e.c_id
3841
                $courseCondition
3842
                WHERE
3843
                    exe_exo_id = $exercise_id AND
3844
                    e.c_id = $courseId AND
3845
                    $answer_condition
3846
                    question_id = $question_id AND
3847
                    e.status = ''
3848
                    $courseConditionWhere
3849
                    $sessionCondition
3850
            ";
3851
        $result = Database::query($sql);
3852
        $return = 0;
3853
        if ($result) {
3854
            $good_answers = 0;
3855
            switch ($question_type) {
3856
                case FILL_IN_BLANKS:
3857
                    while ($row = Database::fetch_assoc($result)) {
3858
                        $fill_blank = self::check_fill_in_blanks(
3859
                            $correct_answer,
3860
                            $row['answer'],
3861
                            $current_answer
3862
                        );
3863
                        if (isset($fill_blank[$current_answer]) && 1 == $fill_blank[$current_answer]) {
3864
                            $good_answers++;
3865
                        }
3866
                    }
3867
3868
                    return $good_answers;
3869
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
3870
                case MATCHING:
3871
                case MATCHING_DRAGGABLE:
3872
                default:
3873
                    $return = Database::num_rows($result);
3874
            }
3875
        }
3876
3877
        return $return;
3878
    }
3879
3880
    /**
3881
     * Get the number of times an answer was selected.
3882
     */
3883
    public static function getCountOfAnswers(
3884
        int $answerId,
3885
        int $questionId,
3886
        int $exerciseId,
3887
        string $courseCode,
3888
        int $sessionId,
3889
        $questionType = null,
3890
    ): int
3891
    {
3892
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3893
        $trackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3894
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
3895
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3896
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3897
3898
        $answerId = (int) $answerId;
3899
        $questionId = (int) $questionId;
3900
        $exerciseId = (int) $exerciseId;
3901
        $courseId = api_get_course_int_id($courseCode);
3902
        $sessionId = (int) $sessionId;
3903
        $return = 0;
3904
3905
        $answerCondition = match ($questionType) {
3906
            FILL_IN_BLANKS => '',
3907
            default => " answer = $answerId AND ",
3908
        };
3909
3910
        if (empty($sessionId)) {
3911
            $courseCondition = "
3912
            INNER JOIN $courseUser cu
3913
            ON cu.c_id = c.id AND cu.user_id = exe_user_id";
3914
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3915
        } else {
3916
            $courseCondition = "
3917
            INNER JOIN $courseUserSession cu
3918
            ON (cu.c_id = a.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
3919
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
3920
        }
3921
3922
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
3923
        $sql = "SELECT count(a.answer) as total
3924
                FROM $trackExercises e
3925
                INNER JOIN $trackAttempt a
3926
                ON (
3927
                    a.exe_id = e.exe_id
3928
                )
3929
                INNER JOIN $courseTable c
3930
                ON c.id = e.c_id
3931
                $courseCondition
3932
                WHERE
3933
                    exe_exo_id = $exerciseId AND
3934
                    e.c_id = $courseId AND
3935
                    $answerCondition
3936
                    question_id = $questionId AND
3937
                    e.status = ''
3938
                    $courseConditionWhere
3939
                    $sessionCondition
3940
            ";
3941
        $result = Database::query($sql);
3942
        if ($result) {
3943
            $count = Database::fetch_array($result);
3944
            $return = (int) $count['total'];
3945
        }
3946
        return $return;
3947
    }
3948
3949
    /**
3950
     * @param array  $answer
3951
     * @param string $user_answer
3952
     *
3953
     * @return array
3954
     */
3955
    public static function check_fill_in_blanks($answer, $user_answer, $current_answer)
3956
    {
3957
        // the question is encoded like this
3958
        // [A] B [C] D [E] F::10,10,10@1
3959
        // number 1 before the "@" means that is a switchable fill in blank question
3960
        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
3961
        // means that is a normal fill blank question
3962
        // first we explode the "::"
3963
        $pre_array = explode('::', $answer);
3964
        // is switchable fill blank or not
3965
        $last = count($pre_array) - 1;
3966
        $is_set_switchable = explode('@', $pre_array[$last]);
3967
        $switchable_answer_set = false;
3968
        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
3969
            $switchable_answer_set = true;
3970
        }
3971
        $answer = '';
3972
        for ($k = 0; $k < $last; $k++) {
3973
            $answer .= $pre_array[$k];
3974
        }
3975
        // splits weightings that are joined with a comma
3976
        $answerWeighting = explode(',', $is_set_switchable[0]);
3977
3978
        // we save the answer because it will be modified
3979
        //$temp = $answer;
3980
        $temp = $answer;
3981
3982
        $answer = '';
3983
        $j = 0;
3984
        //initialise answer tags
3985
        $user_tags = $correct_tags = $real_text = [];
3986
        // the loop will stop at the end of the text
3987
        while (1) {
3988
            // quits the loop if there are no more blanks (detect '[')
3989
            if (false === ($pos = api_strpos($temp, '['))) {
3990
                // adds the end of the text
3991
                $answer = $temp;
3992
                $real_text[] = $answer;
3993
                break; //no more "blanks", quit the loop
3994
            }
3995
            // adds the piece of text that is before the blank
3996
            //and ends with '[' into a general storage array
3997
            $real_text[] = api_substr($temp, 0, $pos + 1);
3998
            $answer .= api_substr($temp, 0, $pos + 1);
3999
            //take the string remaining (after the last "[" we found)
4000
            $temp = api_substr($temp, $pos + 1);
4001
            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4002
            if (false === ($pos = api_strpos($temp, ']'))) {
4003
                // adds the end of the text
4004
                $answer .= $temp;
4005
                break;
4006
            }
4007
4008
            $str = $user_answer;
4009
4010
            preg_match_all('#\[([^[]*)\]#', $str, $arr);
4011
            $str = str_replace('\r\n', '', $str);
4012
            $choices = $arr[1];
4013
            $choice = [];
4014
            $check = false;
4015
            $i = 0;
4016
            foreach ($choices as $item) {
4017
                if ($current_answer === $item) {
4018
                    $check = true;
4019
                }
4020
                if ($check) {
4021
                    $choice[] = $item;
4022
                    $i++;
4023
                }
4024
                if (3 == $i) {
4025
                    break;
4026
                }
4027
            }
4028
            $tmp = api_strrpos($choice[$j], ' / ');
4029
4030
            if (false !== $tmp) {
4031
                $choice[$j] = api_substr($choice[$j], 0, $tmp);
4032
            }
4033
4034
            $choice[$j] = trim($choice[$j]);
4035
4036
            //Needed to let characters ' and " to work as part of an answer
4037
            $choice[$j] = stripslashes($choice[$j]);
4038
4039
            $user_tags[] = api_strtolower($choice[$j]);
4040
            //put the contents of the [] answer tag into correct_tags[]
4041
            $correct_tags[] = api_strtolower(api_substr($temp, 0, $pos));
4042
            $j++;
4043
            $temp = api_substr($temp, $pos + 1);
4044
        }
4045
4046
        $answer = '';
4047
        $real_correct_tags = $correct_tags;
4048
        $chosen_list = [];
4049
        $good_answer = [];
4050
4051
        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...
4052
            if (!$switchable_answer_set) {
4053
                //needed to parse ' and " characters
4054
                $user_tags[$i] = stripslashes($user_tags[$i]);
4055
                if ($correct_tags[$i] == $user_tags[$i]) {
4056
                    $good_answer[$correct_tags[$i]] = 1;
4057
                } elseif (!empty($user_tags[$i])) {
4058
                    $good_answer[$correct_tags[$i]] = 0;
4059
                } else {
4060
                    $good_answer[$correct_tags[$i]] = 0;
4061
                }
4062
            } else {
4063
                // switchable fill in the blanks
4064
                if (in_array($user_tags[$i], $correct_tags)) {
4065
                    $correct_tags = array_diff($correct_tags, $chosen_list);
4066
                    $good_answer[$correct_tags[$i]] = 1;
4067
                } elseif (!empty($user_tags[$i])) {
4068
                    $good_answer[$correct_tags[$i]] = 0;
4069
                } else {
4070
                    $good_answer[$correct_tags[$i]] = 0;
4071
                }
4072
            }
4073
            // adds the correct word, followed by ] to close the blank
4074
            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4075
            if (isset($real_text[$i + 1])) {
4076
                $answer .= $real_text[$i + 1];
4077
            }
4078
        }
4079
4080
        return $good_answer;
4081
    }
4082
4083
    /**
4084
     * Return an HTML select menu with the student groups.
4085
     *
4086
     * @param string $name     is the name and the id of the <select>
4087
     * @param string $default  default value for option
4088
     * @param string $onchange
4089
     *
4090
     * @return string the html code of the <select>
4091
     */
4092
    public static function displayGroupMenu($name, $default, $onchange = "")
4093
    {
4094
        // check the default value of option
4095
        $tabSelected = [$default => " selected='selected' "];
4096
        $res = "<select name='$name' id='$name' onchange='".$onchange."' >";
4097
        $res .= "<option value='-1'".$tabSelected["-1"].">-- ".get_lang('AllGroups')." --</option>";
4098
        $res .= "<option value='0'".$tabSelected["0"].">- ".get_lang('NotInAGroup')." -</option>";
4099
        $groups = GroupManager::get_group_list();
4100
        $currentCatId = 0;
4101
        $countGroups = count($groups);
4102
        for ($i = 0; $i < $countGroups; $i++) {
4103
            $category = GroupManager::get_category_from_group($groups[$i]['iid']);
4104
            if ($category['id'] != $currentCatId) {
4105
                $res .= "<option value='-1' disabled='disabled'>".$category['title']."</option>";
4106
                $currentCatId = $category['id'];
4107
            }
4108
            $res .= "<option ".$tabSelected[$groups[$i]['id']]."style='margin-left:40px' value='".
4109
                $groups[$i]["iid"]."'>".
4110
                $groups[$i]["name"].
4111
                "</option>";
4112
        }
4113
        $res .= "</select>";
4114
4115
        return $res;
4116
    }
4117
4118
    /**
4119
     * @param int $exe_id
4120
     */
4121
    public static function create_chat_exercise_session($exe_id)
4122
    {
4123
        if (!isset($_SESSION['current_exercises'])) {
4124
            $_SESSION['current_exercises'] = [];
4125
        }
4126
        $_SESSION['current_exercises'][$exe_id] = true;
4127
    }
4128
4129
    /**
4130
     * @param int $exe_id
4131
     */
4132
    public static function delete_chat_exercise_session($exe_id)
4133
    {
4134
        if (isset($_SESSION['current_exercises'])) {
4135
            $_SESSION['current_exercises'][$exe_id] = false;
4136
        }
4137
    }
4138
4139
    /**
4140
     * Display the exercise results.
4141
     *
4142
     * @param Exercise $objExercise
4143
     * @param int      $exeId
4144
     * @param bool     $save_user_result save users results (true) or just show the results (false)
4145
     * @param string   $remainingMessage
4146
     * @param bool     $allowSignature
4147
     * @param bool     $allowExportPdf
4148
     * @param bool     $isExport
4149
     */
4150
    public static function displayQuestionListByAttempt(
4151
        $objExercise,
4152
        $exeId,
4153
        $save_user_result = false,
4154
        $remainingMessage = '',
4155
        $allowSignature = false,
4156
        $allowExportPdf = false,
4157
        $isExport = false
4158
    ) {
4159
        $origin = api_get_origin();
4160
        $courseId = api_get_course_int_id();
4161
        $courseCode = api_get_course_id();
4162
        $sessionId = api_get_session_id();
4163
4164
        // Getting attempt info
4165
        $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
4166
4167
        // Getting question list
4168
        $question_list = [];
4169
        $studentInfo = [];
4170
        if (!empty($exercise_stat_info['data_tracking'])) {
4171
            $studentInfo = api_get_user_info($exercise_stat_info['exe_user_id']);
4172
            $question_list = explode(',', $exercise_stat_info['data_tracking']);
4173
        } else {
4174
            // Try getting the question list only if save result is off
4175
            if (false == $save_user_result) {
4176
                $question_list = $objExercise->get_validated_question_list();
4177
            }
4178
            if (in_array(
4179
                $objExercise->getFeedbackType(),
4180
                [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4181
            )) {
4182
                $question_list = $objExercise->get_validated_question_list();
4183
            }
4184
        }
4185
4186
        if ($objExercise->getResultAccess()) {
4187
            if (false === $objExercise->hasResultsAccess($exercise_stat_info)) {
4188
                echo Display::return_message(
4189
                    sprintf(get_lang('YouPassedTheLimitOfXMinutesToSeeTheResults'), $objExercise->getResultsAccess())
4190
                );
4191
4192
                return false;
4193
            }
4194
4195
            if (!empty($objExercise->getResultAccess())) {
4196
                $url = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$objExercise->id;
4197
                echo $objExercise->returnTimeLeftDiv();
4198
                echo $objExercise->showSimpleTimeControl(
4199
                    $objExercise->getResultAccessTimeDiff($exercise_stat_info),
4200
                    $url
4201
                );
4202
            }
4203
        }
4204
4205
        $counter = 1;
4206
        $total_score = $total_weight = 0;
4207
        $exerciseContent = null;
4208
4209
        // Hide results
4210
        $show_results = false;
4211
        $show_only_score = false;
4212
        if (in_array($objExercise->results_disabled,
4213
            [
4214
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4215
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
4216
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4217
            ]
4218
        )) {
4219
            $show_results = true;
4220
        }
4221
4222
        if (in_array(
4223
            $objExercise->results_disabled,
4224
            [
4225
                RESULT_DISABLE_SHOW_SCORE_ONLY,
4226
                RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES,
4227
                RESULT_DISABLE_RANKING,
4228
            ]
4229
        )
4230
        ) {
4231
            $show_only_score = true;
4232
        }
4233
4234
        // Not display expected answer, but score, and feedback
4235
        $show_all_but_expected_answer = false;
4236
        if (RESULT_DISABLE_SHOW_SCORE_ONLY == $objExercise->results_disabled &&
4237
            EXERCISE_FEEDBACK_TYPE_END == $objExercise->getFeedbackType()
4238
        ) {
4239
            $show_all_but_expected_answer = true;
4240
            $show_results = true;
4241
            $show_only_score = false;
4242
        }
4243
4244
        $showTotalScoreAndUserChoicesInLastAttempt = true;
4245
        $showTotalScore = true;
4246
        $showQuestionScore = true;
4247
        $attemptResult = [];
4248
4249
        if (in_array(
4250
            $objExercise->results_disabled,
4251
            [
4252
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
4253
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
4254
                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
4255
            ])
4256
        ) {
4257
            $show_only_score = true;
4258
            $show_results = true;
4259
            $numberAttempts = 0;
4260
            if ($objExercise->attempts > 0) {
4261
                $attempts = Event::getExerciseResultsByUser(
4262
                    api_get_user_id(),
4263
                    $objExercise->id,
4264
                    $courseId,
4265
                    $sessionId,
4266
                    $exercise_stat_info['orig_lp_id'],
4267
                    $exercise_stat_info['orig_lp_item_id'],
4268
                    'desc'
4269
                );
4270
                if ($attempts) {
4271
                    $numberAttempts = count($attempts);
4272
                }
4273
4274
                if ($save_user_result) {
4275
                    $numberAttempts++;
4276
                }
4277
4278
                $showTotalScore = false;
4279
                if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT == $objExercise->results_disabled) {
4280
                    $showTotalScore = true;
4281
                }
4282
                $showTotalScoreAndUserChoicesInLastAttempt = false;
4283
                if ($numberAttempts >= $objExercise->attempts) {
4284
                    $showTotalScore = true;
4285
                    $show_results = true;
4286
                    $show_only_score = false;
4287
                    $showTotalScoreAndUserChoicesInLastAttempt = true;
4288
                }
4289
4290
                if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $objExercise->results_disabled) {
4291
                    $showTotalScore = true;
4292
                    $show_results = true;
4293
                    $show_only_score = false;
4294
                    $showTotalScoreAndUserChoicesInLastAttempt = false;
4295
                    if ($numberAttempts >= $objExercise->attempts) {
4296
                        $showTotalScoreAndUserChoicesInLastAttempt = true;
4297
                    }
4298
4299
                    // Check if the current attempt is the last.
4300
                    if (false === $save_user_result && !empty($attempts)) {
4301
                        $showTotalScoreAndUserChoicesInLastAttempt = false;
4302
                        $position = 1;
4303
                        foreach ($attempts as $attempt) {
4304
                            if ($exeId == $attempt['exe_id']) {
4305
                                break;
4306
                            }
4307
                            $position++;
4308
                        }
4309
4310
                        if ($position == $objExercise->attempts) {
4311
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
4312
                        }
4313
                    }
4314
                }
4315
            }
4316
4317
            if (RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK ==
4318
                $objExercise->results_disabled
4319
            ) {
4320
                $show_only_score = false;
4321
                $show_results = true;
4322
                $show_all_but_expected_answer = false;
4323
                $showTotalScore = false;
4324
                $showQuestionScore = false;
4325
                if ($numberAttempts >= $objExercise->attempts) {
4326
                    $showTotalScore = true;
4327
                    $showQuestionScore = true;
4328
                }
4329
            }
4330
        }
4331
4332
        // When exporting to PDF hide feedback/comment/score show warning in hotspot.
4333
        if ($allowExportPdf && $isExport) {
4334
            $showTotalScore = false;
4335
            $showQuestionScore = false;
4336
            $objExercise->feedback_type = 2;
4337
            $objExercise->hideComment = true;
4338
            $objExercise->hideNoAnswer = true;
4339
            $objExercise->results_disabled = 0;
4340
            $objExercise->hideExpectedAnswer = true;
4341
            $show_results = true;
4342
        }
4343
4344
        if ('embeddable' !== $origin &&
4345
            !empty($exercise_stat_info['exe_user_id']) &&
4346
            !empty($studentInfo)
4347
        ) {
4348
            // Shows exercise header.
4349
            echo $objExercise->showExerciseResultHeader(
4350
                $studentInfo,
4351
                $exercise_stat_info,
4352
                $save_user_result,
4353
                $allowSignature,
4354
                $allowExportPdf
4355
            );
4356
        }
4357
4358
        $question_list_answers = [];
4359
        $category_list = [];
4360
        $loadChoiceFromSession = false;
4361
        $fromDatabase = true;
4362
        $exerciseResult = null;
4363
        $exerciseResultCoordinates = null;
4364
        $delineationResults = null;
4365
        if (true === $save_user_result && in_array(
4366
            $objExercise->getFeedbackType(),
4367
            [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4368
        )) {
4369
            $loadChoiceFromSession = true;
4370
            $fromDatabase = false;
4371
            $exerciseResult = Session::read('exerciseResult');
4372
            $exerciseResultCoordinates = Session::read('exerciseResultCoordinates');
4373
            $delineationResults = Session::read('hotspot_delineation_result');
4374
            $delineationResults = isset($delineationResults[$objExercise->id]) ? $delineationResults[$objExercise->id] : null;
4375
        }
4376
4377
        $countPendingQuestions = 0;
4378
        $result = [];
4379
        // Loop over all question to show results for each of them, one by one
4380
        if (!empty($question_list)) {
4381
            foreach ($question_list as $questionId) {
4382
                // Creates a temporary Question object
4383
                $objQuestionTmp = Question::read($questionId, $objExercise->course);
4384
                // This variable came from exercise_submit_modal.php
4385
                ob_start();
4386
                $choice = null;
4387
                $delineationChoice = null;
4388
                if ($loadChoiceFromSession) {
4389
                    $choice = isset($exerciseResult[$questionId]) ? $exerciseResult[$questionId] : null;
4390
                    $delineationChoice = isset($delineationResults[$questionId]) ? $delineationResults[$questionId] : null;
4391
                }
4392
4393
                // We're inside *one* question. Go through each possible answer for this question
4394
                $result = $objExercise->manage_answer(
4395
                    $exeId,
4396
                    $questionId,
4397
                    $choice,
4398
                    'exercise_result',
4399
                    $exerciseResultCoordinates,
4400
                    $save_user_result,
4401
                    $fromDatabase,
4402
                    $show_results,
4403
                    $objExercise->selectPropagateNeg(),
4404
                    $delineationChoice,
4405
                    $showTotalScoreAndUserChoicesInLastAttempt
4406
                );
4407
4408
                if (empty($result)) {
4409
                    continue;
4410
                }
4411
4412
                $total_score += $result['score'];
4413
                $total_weight += $result['weight'];
4414
4415
                $question_list_answers[] = [
4416
                    'question' => $result['open_question'],
4417
                    'answer' => $result['open_answer'],
4418
                    'answer_type' => $result['answer_type'],
4419
                    'generated_oral_file' => $result['generated_oral_file'],
4420
                ];
4421
4422
                $my_total_score = $result['score'];
4423
                $my_total_weight = $result['weight'];
4424
                $scorePassed = self::scorePassed($my_total_score, $my_total_weight);
4425
4426
                // Category report
4427
                $category_was_added_for_this_test = false;
4428
                if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
4429
                    if (!isset($category_list[$objQuestionTmp->category]['score'])) {
4430
                        $category_list[$objQuestionTmp->category]['score'] = 0;
4431
                    }
4432
                    if (!isset($category_list[$objQuestionTmp->category]['total'])) {
4433
                        $category_list[$objQuestionTmp->category]['total'] = 0;
4434
                    }
4435
                    if (!isset($category_list[$objQuestionTmp->category]['total_questions'])) {
4436
                        $category_list[$objQuestionTmp->category]['total_questions'] = 0;
4437
                    }
4438
                    if (!isset($category_list[$objQuestionTmp->category]['passed'])) {
4439
                        $category_list[$objQuestionTmp->category]['passed'] = 0;
4440
                    }
4441
                    if (!isset($category_list[$objQuestionTmp->category]['wrong'])) {
4442
                        $category_list[$objQuestionTmp->category]['wrong'] = 0;
4443
                    }
4444
                    if (!isset($category_list[$objQuestionTmp->category]['no_answer'])) {
4445
                        $category_list[$objQuestionTmp->category]['no_answer'] = 0;
4446
                    }
4447
4448
                    $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
4449
                    $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
4450
                    if ($scorePassed) {
4451
                        // Only count passed if score is not empty
4452
                        if (!empty($my_total_score)) {
4453
                            $category_list[$objQuestionTmp->category]['passed']++;
4454
                        }
4455
                    } else {
4456
                        if ($result['user_answered']) {
4457
                            $category_list[$objQuestionTmp->category]['wrong']++;
4458
                        } else {
4459
                            $category_list[$objQuestionTmp->category]['no_answer']++;
4460
                        }
4461
                    }
4462
4463
                    $category_list[$objQuestionTmp->category]['total_questions']++;
4464
                    $category_was_added_for_this_test = true;
4465
                }
4466
                if (isset($objQuestionTmp->category_list) && !empty($objQuestionTmp->category_list)) {
4467
                    foreach ($objQuestionTmp->category_list as $category_id) {
4468
                        $category_list[$category_id]['score'] += $my_total_score;
4469
                        $category_list[$category_id]['total'] += $my_total_weight;
4470
                        $category_was_added_for_this_test = true;
4471
                    }
4472
                }
4473
4474
                // No category for this question!
4475
                if (false == $category_was_added_for_this_test) {
4476
                    if (!isset($category_list['none']['score'])) {
4477
                        $category_list['none']['score'] = 0;
4478
                    }
4479
                    if (!isset($category_list['none']['total'])) {
4480
                        $category_list['none']['total'] = 0;
4481
                    }
4482
4483
                    $category_list['none']['score'] += $my_total_score;
4484
                    $category_list['none']['total'] += $my_total_weight;
4485
                }
4486
4487
                if (0 == $objExercise->selectPropagateNeg() && $my_total_score < 0) {
4488
                    $my_total_score = 0;
4489
                }
4490
4491
                $comnt = null;
4492
                if ($show_results) {
4493
                    $comnt = Event::get_comments($exeId, $questionId);
4494
                    $teacherAudio = self::getOralFeedbackAudio(
4495
                        $exeId,
4496
                        $questionId
4497
                    );
4498
4499
                    if (!empty($comnt) || $teacherAudio) {
4500
                        echo '<b>'.get_lang('Feedback').'</b>';
4501
                    }
4502
4503
                    if (!empty($comnt)) {
4504
                        echo self::getFeedbackText($comnt);
4505
                    }
4506
4507
                    if ($teacherAudio) {
4508
                        echo $teacherAudio;
4509
                    }
4510
                }
4511
4512
                $calculatedScore = [
4513
                    'result' => self::show_score(
4514
                        $my_total_score,
4515
                        $my_total_weight,
4516
                        false
4517
                    ),
4518
                    'pass' => $scorePassed,
4519
                    'score' => $my_total_score,
4520
                    'weight' => $my_total_weight,
4521
                    'comments' => $comnt,
4522
                    'user_answered' => $result['user_answered'],
4523
                ];
4524
4525
                $score = [];
4526
                if ($show_results) {
4527
                    $score = $calculatedScore;
4528
                }
4529
                if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION])) {
4530
                    $reviewScore = [
4531
                        'score' => $my_total_score,
4532
                        'comments' => Event::get_comments($exeId, $questionId),
4533
                    ];
4534
                    $check = $objQuestionTmp->isQuestionWaitingReview($reviewScore);
4535
                    if (false === $check) {
4536
                        $countPendingQuestions++;
4537
                    }
4538
                }
4539
4540
                $contents = ob_get_clean();
4541
                $questionContent = '';
4542
                if ($show_results) {
4543
                    $questionContent = '<div class="question-answer-result">';
4544
                    if (false === $showQuestionScore) {
4545
                        $score = [];
4546
                    }
4547
4548
                    // Shows question title an description
4549
                    $questionContent .= $objQuestionTmp->return_header(
4550
                        $objExercise,
4551
                        $counter,
4552
                        $score
4553
                    );
4554
                }
4555
                $counter++;
4556
                $questionContent .= $contents;
4557
                if ($show_results) {
4558
                    $questionContent .= '</div>';
4559
                }
4560
4561
                $calculatedScore['question_content'] = $questionContent;
4562
                $attemptResult[] = $calculatedScore;
4563
4564
                if ($objExercise->showExpectedChoice()) {
4565
                    $exerciseContent .= Display::panel($questionContent);
4566
                } else {
4567
                    // $show_all_but_expected_answer should not happen at
4568
                    // the same time as $show_results
4569
                    if ($show_results && !$show_only_score) {
4570
                        $exerciseContent .= Display::panel($questionContent);
4571
                    }
4572
                }
4573
            }
4574
        }
4575
4576
        // Display text when test is finished #4074 and for LP #4227
4577
        $endOfMessage = $objExercise->getFinishText($total_score, $total_weight);
4578
        if (!empty($endOfMessage)) {
4579
            echo Display::div(
4580
                $endOfMessage,
4581
                ['id' => 'quiz_end_message']
4582
            );
4583
        }
4584
4585
        $totalScoreText = null;
4586
        $certificateBlock = '';
4587
        if (($show_results || $show_only_score) && $showTotalScore) {
4588
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4589
                echo '<h1 style="text-align : center; margin : 20px 0;">'.get_lang('Your results').'</h1><br />';
4590
            }
4591
            $totalScoreText .= '<div class="question_row_score">';
4592
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4593
                $totalScoreText .= self::getQuestionDiagnosisRibbon(
4594
                    $objExercise,
4595
                    $total_score,
4596
                    $total_weight,
4597
                    true
4598
                );
4599
            } else {
4600
                $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
4601
                if ('true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)) {
4602
                    $formula = $pluginEvaluation->getFormulaForExercise($objExercise->getId());
4603
4604
                    if (!empty($formula)) {
4605
                        $total_score = $pluginEvaluation->getResultWithFormula($exeId, $formula);
4606
                        $total_weight = $pluginEvaluation->getMaxScore();
4607
                    }
4608
                }
4609
4610
                $totalScoreText .= self::getTotalScoreRibbon(
4611
                    $objExercise,
4612
                    $total_score,
4613
                    $total_weight,
4614
                    true,
4615
                    $countPendingQuestions
4616
                );
4617
            }
4618
            $totalScoreText .= '</div>';
4619
4620
            if (!empty($studentInfo)) {
4621
                $certificateBlock = self::generateAndShowCertificateBlock(
4622
                    $total_score,
4623
                    $total_weight,
4624
                    $objExercise,
4625
                    $studentInfo['id'],
4626
                    $courseId,
4627
                    $sessionId
4628
                );
4629
            }
4630
        }
4631
4632
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4633
            $chartMultiAnswer = MultipleAnswerTrueFalseDegreeCertainty::displayStudentsChartResults(
4634
                $exeId,
4635
                $objExercise
4636
            );
4637
            echo $chartMultiAnswer;
4638
        }
4639
4640
        if (!empty($category_list) &&
4641
            ($show_results || $show_only_score || RESULT_DISABLE_RADAR == $objExercise->results_disabled)
4642
        ) {
4643
            // Adding total
4644
            $category_list['total'] = [
4645
                'score' => $total_score,
4646
                'total' => $total_weight,
4647
            ];
4648
            echo TestCategory::get_stats_table_by_attempt($objExercise, $category_list);
4649
        }
4650
4651
        if ($show_all_but_expected_answer) {
4652
            $exerciseContent .= Display::return_message(get_lang('Note: This test has been setup to hide the expected answers.'));
4653
        }
4654
4655
        // Remove audio auto play from questions on results page - refs BT#7939
4656
        $exerciseContent = preg_replace(
4657
            ['/autoplay[\=\".+\"]+/', '/autostart[\=\".+\"]+/'],
4658
            '',
4659
            $exerciseContent
4660
        );
4661
4662
        echo $totalScoreText;
4663
        echo $certificateBlock;
4664
4665
        // Ofaj change BT#11784
4666
        if (('true' === api_get_setting('exercise.quiz_show_description_on_results_page')) &&
4667
            !empty($objExercise->description)
4668
        ) {
4669
            echo Display::div($objExercise->description, ['class' => 'exercise_description']);
4670
        }
4671
4672
        echo $exerciseContent;
4673
        if (!$show_only_score) {
4674
            echo $totalScoreText;
4675
        }
4676
4677
        if ($save_user_result) {
4678
            // Tracking of results
4679
            if ($exercise_stat_info) {
4680
                $learnpath_id = $exercise_stat_info['orig_lp_id'];
4681
                $learnpath_item_id = $exercise_stat_info['orig_lp_item_id'];
4682
                $learnpath_item_view_id = $exercise_stat_info['orig_lp_item_view_id'];
4683
4684
                if (api_is_allowed_to_session_edit()) {
4685
                    Event::updateEventExercise(
4686
                        $exercise_stat_info['exe_id'],
4687
                        $objExercise->getId(),
4688
                        $total_score,
4689
                        $total_weight,
4690
                        $sessionId,
4691
                        $learnpath_id,
4692
                        $learnpath_item_id,
4693
                        $learnpath_item_view_id,
4694
                        $exercise_stat_info['exe_duration'],
4695
                        $question_list
4696
                    );
4697
4698
                    $allowStats = ('true' === api_get_setting('gradebook.allow_gradebook_stats'));
4699
                    if ($allowStats) {
4700
                        $objExercise->generateStats(
4701
                            $objExercise->getId(),
4702
                            api_get_course_info(),
4703
                            $sessionId
4704
                        );
4705
                    }
4706
                }
4707
            }
4708
4709
            // Send notification at the end
4710
            if (!api_is_allowed_to_edit(null, true) &&
4711
                !api_is_excluded_user_type()
4712
            ) {
4713
                $objExercise->send_mail_notification_for_exam(
4714
                    'end',
4715
                    $question_list_answers,
4716
                    $origin,
4717
                    $exeId,
4718
                    $total_score,
4719
                    $total_weight
4720
                );
4721
            }
4722
        }
4723
4724
        if (in_array(
4725
            $objExercise->selectResultsDisabled(),
4726
            [RESULT_DISABLE_RANKING, RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING]
4727
        )) {
4728
            echo Display::page_header(get_lang('Ranking'), null, 'h4');
4729
            echo self::displayResultsInRanking(
4730
                $objExercise,
4731
                api_get_user_id(),
4732
                $courseId,
4733
                $sessionId
4734
            );
4735
        }
4736
4737
        if (!empty($remainingMessage)) {
4738
            echo Display::return_message($remainingMessage, 'normal', false);
4739
        }
4740
4741
        $failedAnswersCount = 0;
4742
        $wrongQuestionHtml = '';
4743
        $all = '';
4744
        foreach ($attemptResult as $item) {
4745
            if (false === $item['pass']) {
4746
                $failedAnswersCount++;
4747
                $wrongQuestionHtml .= $item['question_content'].'<br />';
4748
            }
4749
            $all .= $item['question_content'].'<br />';
4750
        }
4751
4752
        $passed = self::isPassPercentageAttemptPassed(
4753
            $objExercise,
4754
            $total_score,
4755
            $total_weight
4756
        );
4757
4758
        $percentage = 0;
4759
        if (!empty($total_weight)) {
4760
            $percentage = ($total_score / $total_weight) * 100;
4761
        }
4762
4763
        return [
4764
            'category_list' => $category_list,
4765
            'attempts_result_list' => $attemptResult, // array of results
4766
            'exercise_passed' => $passed, // boolean
4767
            'total_answers_count' => count($attemptResult), // int
4768
            'failed_answers_count' => $failedAnswersCount, // int
4769
            'failed_answers_html' => $wrongQuestionHtml,
4770
            'all_answers_html' => $all,
4771
            'total_score' => $total_score,
4772
            'total_weight' => $total_weight,
4773
            'total_percentage' => $percentage,
4774
            'count_pending_questions' => $countPendingQuestions,
4775
        ];
4776
    }
4777
4778
    /**
4779
     * Display the ranking of results in a exercise.
4780
     *
4781
     * @param Exercise $exercise
4782
     * @param int      $currentUserId
4783
     * @param int      $courseId
4784
     * @param int      $sessionId
4785
     *
4786
     * @return string
4787
     */
4788
    public static function displayResultsInRanking($exercise, $currentUserId, $courseId, $sessionId = 0)
4789
    {
4790
        $exerciseId = $exercise->iId;
4791
        $data = self::exerciseResultsInRanking($exerciseId, $courseId, $sessionId);
4792
4793
        $table = new HTML_Table(['class' => 'table table-hover table-striped table-bordered']);
4794
        $table->setHeaderContents(0, 0, get_lang('Position'), ['class' => 'text-right']);
4795
        $table->setHeaderContents(0, 1, get_lang('Username'));
4796
        $table->setHeaderContents(0, 2, get_lang('Score'), ['class' => 'text-right']);
4797
        $table->setHeaderContents(0, 3, get_lang('Date'), ['class' => 'text-center']);
4798
4799
        foreach ($data as $r => $item) {
4800
            if (!isset($item[1])) {
4801
                continue;
4802
            }
4803
            $selected = $item[1]->getId() == $currentUserId;
4804
4805
            foreach ($item as $c => $value) {
4806
                $table->setCellContents($r + 1, $c, $value);
4807
4808
                $attrClass = '';
4809
4810
                if (in_array($c, [0, 2])) {
4811
                    $attrClass = 'text-right';
4812
                } elseif (3 == $c) {
4813
                    $attrClass = 'text-center';
4814
                }
4815
4816
                if ($selected) {
4817
                    $attrClass .= ' warning';
4818
                }
4819
4820
                $table->setCellAttributes($r + 1, $c, ['class' => $attrClass]);
4821
            }
4822
        }
4823
4824
        return $table->toHtml();
4825
    }
4826
4827
    /**
4828
     * Get the ranking for results in a exercise.
4829
     * Function used internally by ExerciseLib::displayResultsInRanking.
4830
     *
4831
     * @param int $exerciseId
4832
     * @param int $courseId
4833
     * @param int $sessionId
4834
     *
4835
     * @return array
4836
     */
4837
    public static function exerciseResultsInRanking($exerciseId, $courseId, $sessionId = 0)
4838
    {
4839
        $em = Database::getManager();
4840
4841
        $dql = 'SELECT DISTINCT te.exeUserId FROM ChamiloCoreBundle:TrackEExercise te WHERE te.exeExoId = :id AND te.course = :cId';
4842
        $dql .= api_get_session_condition($sessionId, true, false, 'te.sessionId');
4843
4844
        $result = $em
4845
            ->createQuery($dql)
4846
            ->setParameters(['id' => $exerciseId, 'cId' => $courseId])
4847
            ->getScalarResult();
4848
4849
        $data = [];
4850
4851
        foreach ($result as $item) {
4852
            $data[] = self::get_best_attempt_by_user($item['exeUserId'], $exerciseId, $courseId, $sessionId);
4853
        }
4854
4855
        usort(
4856
            $data,
4857
            function ($a, $b) {
4858
                if ($a['score'] != $b['score']) {
4859
                    return $a['score'] > $b['score'] ? -1 : 1;
4860
                }
4861
4862
                if ($a['exe_date'] != $b['exe_date']) {
4863
                    return $a['exe_date'] < $b['exe_date'] ? -1 : 1;
4864
                }
4865
4866
                return 0;
4867
            }
4868
        );
4869
4870
        // flags to display the same position in case of tie
4871
        $lastScore = $data[0]['score'];
4872
        $position = 1;
4873
        $data = array_map(
4874
            function ($item) use (&$lastScore, &$position) {
4875
                if ($item['score'] < $lastScore) {
4876
                    $position++;
4877
                }
4878
4879
                $lastScore = $item['score'];
4880
4881
                return [
4882
                    $position,
4883
                    api_get_user_entity($item['exe_user_id']),
4884
                    self::show_score($item['score'], $item['max_score'], true, true, true),
4885
                    api_convert_and_format_date($item['exe_date'], DATE_TIME_FORMAT_SHORT),
4886
                ];
4887
            },
4888
            $data
4889
        );
4890
4891
        return $data;
4892
    }
4893
4894
    /**
4895
     * Get a special ribbon on top of "degree of certainty" questions (
4896
     * variation from getTotalScoreRibbon() for other question types).
4897
     *
4898
     * @param Exercise $objExercise
4899
     * @param float    $score
4900
     * @param float    $weight
4901
     * @param bool     $checkPassPercentage
4902
     *
4903
     * @return string
4904
     */
4905
    public static function getQuestionDiagnosisRibbon($objExercise, $score, $weight, $checkPassPercentage = false)
4906
    {
4907
        $displayChartDegree = true;
4908
        $ribbon = $displayChartDegree ? '<div class="ribbon">' : '';
4909
4910
        if ($checkPassPercentage) {
4911
            $passPercentage = $objExercise->selectPassPercentage();
4912
            $isSuccess = self::isSuccessExerciseResult($score, $weight, $passPercentage);
4913
            // Color the final test score if pass_percentage activated
4914
            $ribbonTotalSuccessOrError = '';
4915
            if (self::isPassPercentageEnabled($passPercentage)) {
4916
                if ($isSuccess) {
4917
                    $ribbonTotalSuccessOrError = ' ribbon-total-success';
4918
                } else {
4919
                    $ribbonTotalSuccessOrError = ' ribbon-total-error';
4920
                }
4921
            }
4922
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total '.$ribbonTotalSuccessOrError.'">' : '';
4923
        } else {
4924
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total">' : '';
4925
        }
4926
4927
        if ($displayChartDegree) {
4928
            $ribbon .= '<h3>'.get_lang('Score for the test').':&nbsp;';
4929
            $ribbon .= self::show_score($score, $weight, false, true);
4930
            $ribbon .= '</h3>';
4931
            $ribbon .= '</div>';
4932
        }
4933
4934
        if ($checkPassPercentage) {
4935
            $ribbon .= self::showSuccessMessage(
4936
                $score,
4937
                $weight,
4938
                $objExercise->selectPassPercentage()
4939
            );
4940
        }
4941
4942
        $ribbon .= $displayChartDegree ? '</div>' : '';
4943
4944
        return $ribbon;
4945
    }
4946
4947
    public static function isPassPercentageAttemptPassed($objExercise, $score, $weight)
4948
    {
4949
        $passPercentage = $objExercise->selectPassPercentage();
4950
4951
        return self::isSuccessExerciseResult($score, $weight, $passPercentage);
4952
    }
4953
4954
    /**
4955
     * @param float $score
4956
     * @param float $weight
4957
     * @param bool  $checkPassPercentage
4958
     * @param int   $countPendingQuestions
4959
     *
4960
     * @return string
4961
     */
4962
    public static function getTotalScoreRibbon(
4963
        Exercise $objExercise,
4964
        $score,
4965
        $weight,
4966
        $checkPassPercentage = false,
4967
        $countPendingQuestions = 0
4968
    ) {
4969
        $hide = (int) $objExercise->getPageConfigurationAttribute('hide_total_score');
4970
        if (1 === $hide) {
4971
            return '';
4972
        }
4973
4974
        $passPercentage = $objExercise->selectPassPercentage();
4975
        $ribbon = '<div class="title-score">';
4976
        if ($checkPassPercentage) {
4977
            $isSuccess = self::isSuccessExerciseResult(
4978
                $score,
4979
                $weight,
4980
                $passPercentage
4981
            );
4982
            // Color the final test score if pass_percentage activated
4983
            $class = '';
4984
            if (self::isPassPercentageEnabled($passPercentage)) {
4985
                if ($isSuccess) {
4986
                    $class = ' ribbon-total-success';
4987
                } else {
4988
                    $class = ' ribbon-total-error';
4989
                }
4990
            }
4991
            $ribbon .= '<div class="total '.$class.'">';
4992
        } else {
4993
            $ribbon .= '<div class="total">';
4994
        }
4995
        $ribbon .= '<h3>'.get_lang('Score for the test').':&nbsp;';
4996
        $ribbon .= self::show_score($score, $weight, false, true);
4997
        $ribbon .= '</h3>';
4998
        $ribbon .= '</div>';
4999
        if ($checkPassPercentage) {
5000
            $ribbon .= self::showSuccessMessage(
5001
                $score,
5002
                $weight,
5003
                $passPercentage
5004
            );
5005
        }
5006
        $ribbon .= '</div>';
5007
5008
        if (!empty($countPendingQuestions)) {
5009
            $ribbon .= '<br />';
5010
            $ribbon .= Display::return_message(
5011
                sprintf(
5012
                    get_lang('Temporary score: %s open question(s) not corrected yet.'),
5013
                    $countPendingQuestions
5014
                ),
5015
                'warning'
5016
            );
5017
        }
5018
5019
        return $ribbon;
5020
    }
5021
5022
    /**
5023
     * @param int $countLetter
5024
     *
5025
     * @return mixed
5026
     */
5027
    public static function detectInputAppropriateClass($countLetter)
5028
    {
5029
        $limits = [
5030
            0 => 'input-mini',
5031
            10 => 'input-mini',
5032
            15 => 'input-medium',
5033
            20 => 'input-xlarge',
5034
            40 => 'input-xlarge',
5035
            60 => 'input-xxlarge',
5036
            100 => 'input-xxlarge',
5037
            200 => 'input-xxlarge',
5038
        ];
5039
5040
        foreach ($limits as $size => $item) {
5041
            if ($countLetter <= $size) {
5042
                return $item;
5043
            }
5044
        }
5045
5046
        return $limits[0];
5047
    }
5048
5049
    /**
5050
     * @param int    $senderId
5051
     * @param array  $course_info
5052
     * @param string $test
5053
     * @param string $url
5054
     *
5055
     * @return string
5056
     */
5057
    public static function getEmailNotification($senderId, $course_info, $test, $url)
5058
    {
5059
        $teacher_info = api_get_user_info($senderId);
5060
        $fromName = api_get_person_name(
5061
            $teacher_info['firstname'],
5062
            $teacher_info['lastname'],
5063
            null,
5064
            PERSON_NAME_EMAIL_ADDRESS
5065
        );
5066
5067
        $params = [
5068
            'course_title' => Security::remove_XSS($course_info['name']),
5069
            'test_title' => Security::remove_XSS($test),
5070
            'url' => $url,
5071
            'teacher_name' => $fromName,
5072
        ];
5073
5074
        return Container::getTwig()->render(
5075
            '@ChamiloCore/Mailer/Exercise/result_alert_body.html.twig',
5076
            $params
5077
        );
5078
    }
5079
5080
    /**
5081
     * @return string
5082
     */
5083
    public static function getNotCorrectedYetText()
5084
    {
5085
        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');
5086
    }
5087
5088
    /**
5089
     * @param string $message
5090
     *
5091
     * @return string
5092
     */
5093
    public static function getFeedbackText($message)
5094
    {
5095
        return Display::return_message($message, 'warning', false);
5096
    }
5097
5098
    /**
5099
     * Get the recorder audio component for save a teacher audio feedback.
5100
     *
5101
     * @param int $attemptId
5102
     * @param int $questionId
5103
     *
5104
     * @return string
5105
     */
5106
    public static function getOralFeedbackForm($attemptId, $questionId)
5107
    {
5108
        $view = new Template('', false, false, false, false, false, false);
5109
        $view->assign('type', Asset::EXERCISE_FEEDBACK);
5110
        $view->assign('question_id', $questionId);
5111
        $view->assign('t_exercise_id', $attemptId);
5112
        $template = $view->get_template('exercise/oral_expression.html.twig');
5113
5114
        return $view->fetch($template);
5115
    }
5116
5117
    /**
5118
     * Retrieves the generated audio files for an oral question in an exercise attempt.
5119
     *
5120
     * @param int  $trackExerciseId The ID of the tracked exercise.
5121
     * @param int  $questionId      The ID of the question.
5122
     * @param bool $returnUrls      (Optional) If set to true, only the URLs of the audio files are returned. Default is false.
5123
     *
5124
     * @return array|string If $returnUrls is true, returns an array of URLs of the audio files. Otherwise, returns an HTML string with audio tags.
5125
     */
5126
    public static function getOralFileAudio(int $trackExerciseId, int $questionId, bool $returnUrls = false): array|string
5127
    {
5128
        /** @var TrackEExercise $trackExercise */
5129
        $trackExercise = Container::getTrackEExerciseRepository()->find($trackExerciseId);
5130
5131
        if (null === $trackExercise) {
5132
            return $returnUrls ? [] : '';
5133
        }
5134
5135
        $questionAttempt = $trackExercise->getAttemptByQuestionId($questionId);
5136
5137
        if (null === $questionAttempt) {
5138
            return $returnUrls ? [] : '';
5139
        }
5140
5141
        $assetRepo = Container::getAssetRepository();
5142
5143
        if ($returnUrls) {
5144
            $basePath = rtrim(api_get_path(WEB_PATH), '/');
5145
            $urls = [];
5146
            foreach ($questionAttempt->getAttemptFiles() as $attemptFile) {
5147
                $urls[] = $basePath.$assetRepo->getAssetUrl($attemptFile->getAsset());
5148
            }
5149
5150
            return $urls;
5151
        } else {
5152
            $html = '';
5153
            foreach ($questionAttempt->getAttemptFiles() as $attemptFile) {
5154
                $html .= Display::tag(
5155
                    'audio',
5156
                    '',
5157
                    [
5158
                        'src' => $assetRepo->getAssetUrl($attemptFile->getAsset()),
5159
                        'controls' => '',
5160
                    ]
5161
                );
5162
            }
5163
5164
            return $html;
5165
        }
5166
    }
5167
5168
    /**
5169
     * Get the audio component for a teacher audio feedback.
5170
     */
5171
    public static function getOralFeedbackAudio(int $attemptId, int $questionId): string
5172
    {
5173
        /** @var TrackEExercise $tExercise */
5174
        $tExercise = Container::getTrackEExerciseRepository()->find($attemptId);
5175
5176
        if (null === $tExercise) {
5177
            return '';
5178
        }
5179
5180
        $qAttempt = $tExercise->getAttemptByQuestionId($questionId);
5181
5182
        if (null === $qAttempt) {
5183
            return '';
5184
        }
5185
5186
        $html = '';
5187
5188
        $assetRepo = Container::getAssetRepository();
5189
5190
        foreach ($qAttempt->getAttemptFeedbacks() as $attemptFeedback) {
5191
            $html .= Display::tag(
5192
                'audio',
5193
                '',
5194
                [
5195
                    'src' => $assetRepo->getAssetUrl($attemptFeedback->getAsset()),
5196
                    'controls' => '',
5197
                ]
5198
5199
            );
5200
        }
5201
5202
        return $html;
5203
    }
5204
5205
    public static function getNotificationSettings(): array
5206
    {
5207
        return [
5208
            2 => get_lang('Paranoid: E-mail teacher when a student starts an exercise'),
5209
            1 => get_lang('Aware: E-mail teacher when a student ends an exercise'), // default
5210
            3 => get_lang('Relaxed open: E-mail teacher when a student ends an exercise, only if an open question is answered'),
5211
            4 => get_lang('Relaxed audio: E-mail teacher when a student ends an exercise, only if an oral question is answered'),
5212
        ];
5213
    }
5214
5215
    /**
5216
     * Get the additional actions added in exercise_additional_teacher_modify_actions configuration.
5217
     *
5218
     * @param int $exerciseId
5219
     * @param int $iconSize
5220
     *
5221
     * @return string
5222
     */
5223
    public static function getAdditionalTeacherActions($exerciseId, $iconSize = ICON_SIZE_SMALL)
5224
    {
5225
        $additionalActions = api_get_setting('exercise.exercise_additional_teacher_modify_actions', true) ?: [];
5226
        $actions = [];
5227
5228
        if (is_array($additionalActions)) {
5229
            foreach ($additionalActions as $additionalAction) {
5230
                $actions[] = call_user_func(
5231
                    $additionalAction,
5232
                    $exerciseId,
5233
                    $iconSize
5234
                );
5235
            }
5236
        }
5237
5238
        return implode(PHP_EOL, $actions);
5239
    }
5240
5241
    /**
5242
     * @param int $userId
5243
     * @param int $courseId
5244
     * @param int $sessionId
5245
     *
5246
     * @return int
5247
     */
5248
    public static function countAnsweredQuestionsByUserAfterTime(DateTime $time, $userId, $courseId, $sessionId)
5249
    {
5250
        $em = Database::getManager();
5251
5252
        if (empty($sessionId)) {
5253
            $sessionId = null;
5254
        }
5255
5256
        $time = api_get_utc_datetime($time->format('Y-m-d H:i:s'), false, true);
5257
5258
        $result = $em
5259
            ->createQuery('
5260
                SELECT COUNT(ea) FROM ChamiloCoreBundle:TrackEAttempt ea
5261
                WHERE ea.userId = :user AND ea.cId = :course AND ea.sessionId = :session
5262
                    AND ea.tms > :time
5263
            ')
5264
            ->setParameters(['user' => $userId, 'course' => $courseId, 'session' => $sessionId, 'time' => $time])
5265
            ->getSingleScalarResult();
5266
5267
        return $result;
5268
    }
5269
5270
    /**
5271
     * @param int $userId
5272
     * @param int $numberOfQuestions
5273
     * @param int $courseId
5274
     * @param int $sessionId
5275
     *
5276
     * @throws \Doctrine\ORM\Query\QueryException
5277
     *
5278
     * @return bool
5279
     */
5280
    public static function isQuestionsLimitPerDayReached($userId, $numberOfQuestions, $courseId, $sessionId)
5281
    {
5282
        $questionsLimitPerDay = (int) api_get_course_setting('quiz_question_limit_per_day');
5283
5284
        if ($questionsLimitPerDay <= 0) {
5285
            return false;
5286
        }
5287
5288
        $midnightTime = ChamiloApi::getServerMidnightTime();
5289
5290
        $answeredQuestionsCount = self::countAnsweredQuestionsByUserAfterTime(
5291
            $midnightTime,
5292
            $userId,
5293
            $courseId,
5294
            $sessionId
5295
        );
5296
5297
        return ($answeredQuestionsCount + $numberOfQuestions) > $questionsLimitPerDay;
5298
    }
5299
5300
    /**
5301
     * Check if an exercise complies with the requirements to be embedded in the mobile app or a video.
5302
     * By making sure it is set on one question per page and it only contains unique-answer or multiple-answer questions
5303
     * or unique-answer image. And that the exam does not have immediate feedback.
5304
     *
5305
     * @return bool
5306
     */
5307
    public static function isQuizEmbeddable(CQuiz $exercise)
5308
    {
5309
        $em = Database::getManager();
5310
5311
        if (ONE_PER_PAGE != $exercise->getType() ||
5312
            in_array($exercise->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])
5313
        ) {
5314
            return false;
5315
        }
5316
5317
        $countAll = $em
5318
            ->createQuery('SELECT COUNT(qq)
5319
                FROM ChamiloCourseBundle:CQuizQuestion qq
5320
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5321
                   WITH qq.iid = qrq.question
5322
                WHERE qrq.quiz = :id'
5323
            )
5324
            ->setParameter('id', $exercise->getIid())
5325
            ->getSingleScalarResult();
5326
5327
        $countOfAllowed = $em
5328
            ->createQuery('SELECT COUNT(qq)
5329
                FROM ChamiloCourseBundle:CQuizQuestion qq
5330
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5331
                   WITH qq.iid = qrq.question
5332
                WHERE qrq.quiz = :id AND qq.type IN (:types)'
5333
            )
5334
            ->setParameters(
5335
                [
5336
                    'id' => $exercise->getIid(),
5337
                    'types' => [UNIQUE_ANSWER, MULTIPLE_ANSWER, UNIQUE_ANSWER_IMAGE],
5338
                ]
5339
            )
5340
            ->getSingleScalarResult();
5341
5342
        return $countAll === $countOfAllowed;
5343
    }
5344
5345
    /**
5346
     * Generate a certificate linked to current quiz and.
5347
     * Return the HTML block with links to download and view the certificate.
5348
     *
5349
     * @param float $totalScore
5350
     * @param float $totalWeight
5351
     * @param int   $studentId
5352
     * @param int   $courseId
5353
     * @param int   $sessionId
5354
     *
5355
     * @return string
5356
     */
5357
    public static function generateAndShowCertificateBlock(
5358
        $totalScore,
5359
        $totalWeight,
5360
        Exercise $objExercise,
5361
        $studentId,
5362
        $courseId,
5363
        $sessionId = 0
5364
    ) {
5365
        if (('true' !== api_get_setting('exercise.quiz_generate_certificate_ending')) ||
5366
            !self::isSuccessExerciseResult($totalScore, $totalWeight, $objExercise->selectPassPercentage())
5367
        ) {
5368
            return '';
5369
        }
5370
5371
        $repo = Container::getGradeBookCategoryRepository();
5372
        /** @var GradebookCategory $category */
5373
        $category = $repo->findOneBy(
5374
            ['course' => $courseId, 'session' => $sessionId]
5375
        );
5376
5377
        if (null === $category) {
5378
            return '';
5379
        }
5380
5381
        /*$category = Category::load(null, null, $courseCode, null, null, $sessionId, 'ORDER By id');
5382
        if (empty($category)) {
5383
            return '';
5384
        }*/
5385
        $categoryId = $category->getId();
5386
        /*$link = LinkFactory::load(
5387
            null,
5388
            null,
5389
            $objExercise->getId(),
5390
            null,
5391
            $courseCode,
5392
            $categoryId
5393
        );*/
5394
5395
        if (empty($category->getLinks()->count())) {
5396
            return '';
5397
        }
5398
5399
        $resourceDeletedMessage = Category::show_message_resource_delete($courseId);
5400
        if (!empty($resourceDeletedMessage) || api_is_allowed_to_edit() || api_is_excluded_user_type()) {
5401
            return '';
5402
        }
5403
5404
        $certificate = Category::generateUserCertificate($category, $studentId);
5405
        if (!is_array($certificate)) {
5406
            return '';
5407
        }
5408
5409
        return Category::getDownloadCertificateBlock($certificate);
5410
    }
5411
5412
    /**
5413
     * @param int $exerciseId
5414
     */
5415
    public static function getExerciseTitleById($exerciseId)
5416
    {
5417
        $em = Database::getManager();
5418
5419
        return $em
5420
            ->createQuery('SELECT cq.title
5421
                FROM ChamiloCourseBundle:CQuiz cq
5422
                WHERE cq.iid = :iid'
5423
            )
5424
            ->setParameter('iid', $exerciseId)
5425
            ->getSingleScalarResult();
5426
    }
5427
5428
    /**
5429
     * @param int $exeId      ID from track_e_exercises
5430
     * @param int $userId     User ID
5431
     * @param int $exerciseId Exercise ID
5432
     * @param int $courseId   Optional. Coure ID.
5433
     *
5434
     * @return TrackEExercise|null
5435
     */
5436
    public static function recalculateResult($exeId, $userId, $exerciseId, $courseId = 0)
5437
    {
5438
        if (empty($userId) || empty($exerciseId)) {
5439
            return null;
5440
        }
5441
5442
        $em = Database::getManager();
5443
        /** @var TrackEExercise $trackedExercise */
5444
        $trackedExercise = $em->getRepository(TrackEExercise::class)->find($exeId);
5445
5446
        if (empty($trackedExercise)) {
5447
            return null;
5448
        }
5449
5450
        if ($trackedExercise->getUser()->getId() != $userId ||
5451
            $trackedExercise->getQuiz()?->getIid() != $exerciseId
5452
        ) {
5453
            return null;
5454
        }
5455
5456
        $questionList = $trackedExercise->getDataTracking();
5457
5458
        if (empty($questionList)) {
5459
            return null;
5460
        }
5461
5462
        $questionList = explode(',', $questionList);
5463
5464
        $exercise = new Exercise($courseId);
5465
        $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : [];
5466
5467
        if (false === $exercise->read($exerciseId)) {
5468
            return null;
5469
        }
5470
5471
        $totalScore = 0;
5472
        $totalWeight = 0;
5473
5474
        $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
5475
5476
        $formula = 'true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)
5477
            ? $pluginEvaluation->getFormulaForExercise($exerciseId)
5478
            : 0;
5479
5480
        if (empty($formula)) {
5481
            foreach ($questionList as $questionId) {
5482
                $question = Question::read($questionId, $courseInfo);
5483
5484
                if (false === $question) {
5485
                    continue;
5486
                }
5487
5488
                $totalWeight += $question->selectWeighting();
5489
5490
                // We're inside *one* question. Go through each possible answer for this question
5491
                $result = $exercise->manage_answer(
5492
                    $exeId,
5493
                    $questionId,
5494
                    [],
5495
                    'exercise_result',
5496
                    [],
5497
                    false,
5498
                    true,
5499
                    false,
5500
                    $exercise->selectPropagateNeg(),
5501
                    [],
5502
                    [],
5503
                    true
5504
                );
5505
5506
                //  Adding the new score.
5507
                $totalScore += $result['score'];
5508
            }
5509
        } else {
5510
            $totalScore = $pluginEvaluation->getResultWithFormula($exeId, $formula);
5511
            $totalWeight = $pluginEvaluation->getMaxScore();
5512
        }
5513
5514
        $trackedExercise
5515
            ->setScore($totalScore)
5516
            ->setMaxScore($totalWeight);
5517
5518
        $em->persist($trackedExercise);
5519
        $em->flush();
5520
5521
        return $trackedExercise;
5522
    }
5523
5524
    public static function getTotalQuestionAnswered($courseId, $exerciseId, $questionId, $onlyStudents = false): int
5525
    {
5526
        $courseId = (int) $courseId;
5527
        $exerciseId = (int) $exerciseId;
5528
        $questionId = (int) $questionId;
5529
5530
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5531
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5532
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
5533
        $courseUserJoin = "";
5534
        $studentsWhere = "";
5535
        if ($onlyStudents) {
5536
            $courseUserJoin = "
5537
            INNER JOIN $courseUser cu
5538
            ON cu.c_id = te.c_id AND cu.user_id = exe_user_id";
5539
            $studentsWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
5540
        }
5541
5542
        $sql = "SELECT count(distinct (te.exe_id)) total
5543
            FROM $attemptTable t
5544
            INNER JOIN $trackTable te
5545
            ON (t.exe_id = te.exe_id)
5546
            $courseUserJoin
5547
            WHERE
5548
                te.c_id = $courseId AND
5549
                exe_exo_id = $exerciseId AND
5550
                t.question_id = $questionId AND
5551
                te.status != 'incomplete'
5552
                $studentsWhere
5553
        ";
5554
        $queryTotal = Database::query($sql);
5555
        $totalRow = Database::fetch_assoc($queryTotal);
5556
        $total = 0;
5557
        if ($totalRow) {
5558
            $total = (int) $totalRow['total'];
5559
        }
5560
5561
        return $total;
5562
    }
5563
5564
    public static function getWrongQuestionResults($courseId, $exerciseId, $sessionId = 0, $limit = 10)
5565
    {
5566
        $courseId = (int) $courseId;
5567
        $exerciseId = (int) $exerciseId;
5568
        $limit = (int) $limit;
5569
5570
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
5571
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5572
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5573
5574
        $sessionCondition = '';
5575
        if (!empty($sessionId)) {
5576
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
5577
        }
5578
5579
        $sql = "SELECT q.question, question_id, count(q.iid) count
5580
                FROM $attemptTable t
5581
                INNER JOIN $questionTable q
5582
                ON (q.iid = t.question_id)
5583
                INNER JOIN $trackTable te
5584
                ON (t.exe_id = te.exe_id)
5585
                WHERE
5586
                    te.c_id = $courseId AND
5587
                    t.marks != q.ponderation AND
5588
                    exe_exo_id = $exerciseId AND
5589
                    status != 'incomplete'
5590
                    $sessionCondition
5591
                GROUP BY q.iid
5592
                ORDER BY count DESC
5593
                LIMIT $limit
5594
        ";
5595
5596
        $result = Database::query($sql);
5597
5598
        return Database::store_result($result, 'ASSOC');
5599
    }
5600
5601
    public static function getExerciseResultsCount($type, $courseId, $exerciseId, $sessionId = 0)
5602
    {
5603
        $courseId = (int) $courseId;
5604
        $exerciseId = (int) $exerciseId;
5605
5606
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5607
5608
        $sessionCondition = '';
5609
        if (!empty($sessionId)) {
5610
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
5611
        }
5612
5613
        $selectCount = 'count(DISTINCT te.exe_id)';
5614
        $scoreCondition = '';
5615
        switch ($type) {
5616
            case 'correct_student':
5617
                $selectCount = 'count(DISTINCT te.exe_user_id)';
5618
                $scoreCondition = ' AND score = max_score ';
5619
                break;
5620
            case 'wrong_student':
5621
                $selectCount = 'count(DISTINCT te.exe_user_id)';
5622
                $scoreCondition = ' AND score != max_score ';
5623
                break;
5624
            case 'correct':
5625
                $scoreCondition = ' AND score = max_score ';
5626
                break;
5627
            case 'wrong':
5628
                $scoreCondition = ' AND score != max_score ';
5629
                break;
5630
        }
5631
5632
        $sql = "SELECT $selectCount count
5633
                FROM $trackTable te
5634
                WHERE
5635
                    c_id = $courseId AND
5636
                    exe_exo_id = $exerciseId AND
5637
                    status != 'incomplete'
5638
                    $scoreCondition
5639
                    $sessionCondition
5640
        ";
5641
        $result = Database::query($sql);
5642
        $totalRow = Database::fetch_assoc($result);
5643
        $total = 0;
5644
        if ($totalRow) {
5645
            $total = (int) $totalRow['count'];
5646
        }
5647
5648
        return $total;
5649
    }
5650
5651
    public static function parseContent($content, $stats, Exercise $exercise, $trackInfo, $currentUserId = 0)
5652
    {
5653
        $wrongAnswersCount = $stats['failed_answers_count'];
5654
        $attemptDate = substr($trackInfo['exe_date'], 0, 10);
5655
        $exerciseId = $exercise->iId;
5656
        $resultsStudentUrl = api_get_path(WEB_CODE_PATH).
5657
            'exercise/result.php?id='.$exerciseId.'&'.api_get_cidreq();
5658
        $resultsTeacherUrl = api_get_path(WEB_CODE_PATH).
5659
            'exercise/exercise_show.php?action=edit&id='.$exerciseId.'&'.api_get_cidreq();
5660
5661
        $content = str_replace(
5662
            [
5663
                '((exercise_error_count))',
5664
                '((all_answers_html))',
5665
                '((all_answers_teacher_html))',
5666
                '((exercise_title))',
5667
                '((exercise_attempt_date))',
5668
                '((link_to_test_result_page_student))',
5669
                '((link_to_test_result_page_teacher))',
5670
            ],
5671
            [
5672
                $wrongAnswersCount,
5673
                $stats['all_answers_html'],
5674
                $stats['all_answers_teacher_html'],
5675
                $exercise->get_formated_title(),
5676
                $attemptDate,
5677
                $resultsStudentUrl,
5678
                $resultsTeacherUrl,
5679
            ],
5680
            $content
5681
        );
5682
5683
        $currentUserId = empty($currentUserId) ? api_get_user_id() : (int) $currentUserId;
5684
5685
        $content = AnnouncementManager::parseContent(
5686
            $currentUserId,
5687
            $content,
5688
            api_get_course_id(),
5689
            api_get_session_id()
5690
        );
5691
5692
        return $content;
5693
    }
5694
5695
    public static function sendNotification(
5696
        $currentUserId,
5697
        $objExercise,
5698
        $exercise_stat_info,
5699
        $courseInfo,
5700
        $attemptCountToSend,
5701
        $stats,
5702
        $statsTeacher
5703
    ) {
5704
        $notifications = api_get_configuration_value('exercise_finished_notification_settings');
5705
        if (empty($notifications)) {
5706
            return false;
5707
        }
5708
5709
        $studentId = $exercise_stat_info['exe_user_id'];
5710
        $exerciseExtraFieldValue = new ExtraFieldValue('exercise');
5711
        $wrongAnswersCount = $stats['failed_answers_count'];
5712
        $exercisePassed = $stats['exercise_passed'];
5713
        $countPendingQuestions = $stats['count_pending_questions'];
5714
        $stats['all_answers_teacher_html'] = $statsTeacher['all_answers_html'];
5715
5716
        // If there are no pending questions (Open questions).
5717
        if (0 === $countPendingQuestions) {
5718
            /*$extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5719
                $objExercise->iId,
5720
                'signature_mandatory'
5721
            );
5722
5723
            if ($extraFieldData && isset($extraFieldData['value']) && 1 === (int) $extraFieldData['value']) {
5724
                if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($objExercise)) {
5725
                    $signature = ExerciseSignaturePlugin::getSignature($studentId, $exercise_stat_info);
5726
                    if (false !== $signature) {
5727
                        //return false;
5728
                    }
5729
                }
5730
            }*/
5731
5732
            // Notifications.
5733
            $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5734
                $objExercise->iId,
5735
                'notifications'
5736
            );
5737
            $exerciseNotification = '';
5738
            if ($extraFieldData && isset($extraFieldData['value'])) {
5739
                $exerciseNotification = $extraFieldData['value'];
5740
            }
5741
5742
            $subject = sprintf(get_lang('WrongAttemptXInCourseX'), $attemptCountToSend, $courseInfo['title']);
5743
            if ($exercisePassed) {
5744
                $subject = sprintf(get_lang('ExerciseValidationInCourseX'), $courseInfo['title']);
5745
            }
5746
5747
            if ($exercisePassed) {
5748
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5749
                    $objExercise->iId,
5750
                    'MailSuccess'
5751
                );
5752
            } else {
5753
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5754
                    $objExercise->iId,
5755
                    'MailAttempt'.$attemptCountToSend
5756
                );
5757
            }
5758
5759
            // Blocking exercise.
5760
            $blockPercentageExtra = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5761
                $objExercise->iId,
5762
                'blocking_percentage'
5763
            );
5764
            $blockPercentage = false;
5765
            if ($blockPercentageExtra && isset($blockPercentageExtra['value']) && $blockPercentageExtra['value']) {
5766
                $blockPercentage = $blockPercentageExtra['value'];
5767
            }
5768
            if ($blockPercentage) {
5769
                $passBlock = $stats['total_percentage'] > $blockPercentage;
5770
                if (false === $passBlock) {
5771
                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5772
                        $objExercise->iId,
5773
                        'MailIsBlockByPercentage'
5774
                    );
5775
                }
5776
            }
5777
5778
            $extraFieldValueUser = new ExtraFieldValue('user');
5779
5780
            if ($extraFieldData && isset($extraFieldData['value'])) {
5781
                $content = $extraFieldData['value'];
5782
                $content = self::parseContent($content, $stats, $objExercise, $exercise_stat_info, $studentId);
5783
                //if (false === $exercisePassed) {
5784
                if (0 !== $wrongAnswersCount) {
5785
                    $content .= $stats['failed_answers_html'];
5786
                }
5787
5788
                $sendMessage = true;
5789
                if (!empty($exerciseNotification)) {
5790
                    foreach ($notifications as $name => $notificationList) {
5791
                        if ($exerciseNotification !== $name) {
5792
                            continue;
5793
                        }
5794
                        foreach ($notificationList as $notificationName => $attemptData) {
5795
                            if ('student_check' === $notificationName) {
5796
                                $sendMsgIfInList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : '';
5797
                                if (!empty($sendMsgIfInList)) {
5798
                                    foreach ($sendMsgIfInList as $skipVariable => $skipValues) {
5799
                                        $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
5800
                                            $studentId,
5801
                                            $skipVariable
5802
                                        );
5803
5804
                                        if (empty($userExtraFieldValue)) {
5805
                                            $sendMessage = false;
5806
                                            break;
5807
                                        } else {
5808
                                            $sendMessage = false;
5809
                                            if (isset($userExtraFieldValue['value']) &&
5810
                                                in_array($userExtraFieldValue['value'], $skipValues)
5811
                                            ) {
5812
                                                $sendMessage = true;
5813
                                                break;
5814
                                            }
5815
                                        }
5816
                                    }
5817
                                }
5818
                                break;
5819
                            }
5820
                        }
5821
                    }
5822
                }
5823
5824
                // Send to student.
5825
                if ($sendMessage) {
5826
                    MessageManager::send_message($currentUserId, $subject, $content);
5827
                }
5828
            }
5829
5830
            if (!empty($exerciseNotification)) {
5831
                foreach ($notifications as $name => $notificationList) {
5832
                    if ($exerciseNotification !== $name) {
5833
                        continue;
5834
                    }
5835
                    foreach ($notificationList as $attemptData) {
5836
                        $skipNotification = false;
5837
                        $skipNotificationList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : [];
5838
                        if (!empty($skipNotificationList)) {
5839
                            foreach ($skipNotificationList as $skipVariable => $skipValues) {
5840
                                $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
5841
                                    $studentId,
5842
                                    $skipVariable
5843
                                );
5844
5845
                                if (empty($userExtraFieldValue)) {
5846
                                    $skipNotification = true;
5847
                                    break;
5848
                                } else {
5849
                                    if (isset($userExtraFieldValue['value'])) {
5850
                                        if (!in_array($userExtraFieldValue['value'], $skipValues)) {
5851
                                            $skipNotification = true;
5852
                                            break;
5853
                                        }
5854
                                    } else {
5855
                                        $skipNotification = true;
5856
                                        break;
5857
                                    }
5858
                                }
5859
                            }
5860
                        }
5861
5862
                        if ($skipNotification) {
5863
                            continue;
5864
                        }
5865
5866
                        $email = isset($attemptData['email']) ? $attemptData['email'] : '';
5867
                        $emailList = explode(',', $email);
5868
                        if (empty($emailList)) {
5869
                            continue;
5870
                        }
5871
                        $attempts = isset($attemptData['attempts']) ? $attemptData['attempts'] : [];
5872
                        foreach ($attempts as $attempt) {
5873
                            $sendMessage = false;
5874
                            if (isset($attempt['attempt']) && $attemptCountToSend !== (int) $attempt['attempt']) {
5875
                                continue;
5876
                            }
5877
5878
                            if (!isset($attempt['status'])) {
5879
                                continue;
5880
                            }
5881
5882
                            if ($blockPercentage && isset($attempt['is_block_by_percentage'])) {
5883
                                if ($attempt['is_block_by_percentage']) {
5884
                                    if ($passBlock) {
5885
                                        continue;
5886
                                    }
5887
                                } else {
5888
                                    if (false === $passBlock) {
5889
                                        continue;
5890
                                    }
5891
                                }
5892
                            }
5893
5894
                            switch ($attempt['status']) {
5895
                                case 'passed':
5896
                                    if ($exercisePassed) {
5897
                                        $sendMessage = true;
5898
                                    }
5899
                                    break;
5900
                                case 'failed':
5901
                                    if (false === $exercisePassed) {
5902
                                        $sendMessage = true;
5903
                                    }
5904
                                    break;
5905
                                case 'all':
5906
                                    $sendMessage = true;
5907
                                    break;
5908
                            }
5909
5910
                            if ($sendMessage) {
5911
                                $attachments = [];
5912
                                if (isset($attempt['add_pdf']) && $attempt['add_pdf']) {
5913
                                    // Get pdf content
5914
                                    $pdfExtraData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5915
                                        $objExercise->iId,
5916
                                        $attempt['add_pdf']
5917
                                    );
5918
5919
                                    if ($pdfExtraData && isset($pdfExtraData['value'])) {
5920
                                        $pdfContent = self::parseContent(
5921
                                            $pdfExtraData['value'],
5922
                                            $stats,
5923
                                            $objExercise,
5924
                                            $exercise_stat_info,
5925
                                            $studentId
5926
                                        );
5927
5928
                                        @$pdf = new PDF();
5929
                                        $filename = get_lang('Exercise');
5930
                                        $cssFile = api_get_path(SYS_CSS_PATH).'themes/chamilo/default.css';
5931
                                        $pdfPath = @$pdf->content_to_pdf(
5932
                                            "<html><body>$pdfContent</body></html>",
5933
                                            file_get_contents($cssFile),
5934
                                            $filename,
5935
                                            api_get_course_id(),
5936
                                            'F',
5937
                                            false,
5938
                                            null,
5939
                                            false,
5940
                                            true
5941
                                        );
5942
                                        $attachments[] = ['filename' => $filename, 'path' => $pdfPath];
5943
                                    }
5944
                                }
5945
5946
                                $content = isset($attempt['content_default']) ? $attempt['content_default'] : '';
5947
                                if (isset($attempt['content'])) {
5948
                                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5949
                                        $objExercise->iId,
5950
                                        $attempt['content']
5951
                                    );
5952
                                    if ($extraFieldData && isset($extraFieldData['value']) && !empty($extraFieldData['value'])) {
5953
                                        $content = $extraFieldData['value'];
5954
                                    }
5955
                                }
5956
5957
                                if (!empty($content)) {
5958
                                    $content = self::parseContent(
5959
                                        $content,
5960
                                        $stats,
5961
                                        $objExercise,
5962
                                        $exercise_stat_info,
5963
                                        $studentId
5964
                                    );
5965
                                    foreach ($emailList as $email) {
5966
                                        if (empty($email)) {
5967
                                            continue;
5968
                                        }
5969
                                        api_mail_html(
5970
                                            null,
5971
                                            $email,
5972
                                            $subject,
5973
                                            $content,
5974
                                            null,
5975
                                            null,
5976
                                            [],
5977
                                            $attachments
5978
                                        );
5979
                                    }
5980
                                }
5981
5982
                                if (isset($attempt['post_actions'])) {
5983
                                    foreach ($attempt['post_actions'] as $action => $params) {
5984
                                        switch ($action) {
5985
                                            case 'subscribe_student_to_courses':
5986
                                                foreach ($params as $code) {
5987
                                                    $courseInfo = api_get_course_info($code);
5988
                                                    CourseManager::subscribeUser(
5989
                                                        $currentUserId,
5990
                                                        $courseInfo['real_id']
5991
                                                    );
5992
                                                    break;
5993
                                                }
5994
                                                break;
5995
                                        }
5996
                                    }
5997
                                }
5998
                            }
5999
                        }
6000
                    }
6001
                }
6002
            }
6003
        }
6004
    }
6005
6006
    /**
6007
     * Delete an exercise attempt.
6008
     *
6009
     * Log the exe_id deleted with the exe_user_id related.
6010
     *
6011
     * @param int $exeId
6012
     */
6013
    public static function deleteExerciseAttempt($exeId)
6014
    {
6015
        $exeId = (int) $exeId;
6016
6017
        $trackExerciseInfo = self::get_exercise_track_exercise_info($exeId);
6018
6019
        if (empty($trackExerciseInfo)) {
6020
            return;
6021
        }
6022
6023
        $tblTrackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6024
        $tblTrackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
6025
6026
        Database::query("DELETE FROM $tblTrackAttempt WHERE exe_id = $exeId");
6027
        Database::query("DELETE FROM $tblTrackExercises WHERE exe_id = $exeId");
6028
6029
        Event::addEvent(
6030
            LOG_EXERCISE_ATTEMPT_DELETE,
6031
            LOG_EXERCISE_ATTEMPT,
6032
            $exeId,
6033
            api_get_utc_datetime()
6034
        );
6035
        Event::addEvent(
6036
            LOG_EXERCISE_ATTEMPT_DELETE,
6037
            LOG_EXERCISE_AND_USER_ID,
6038
            $exeId.'-'.$trackExerciseInfo['exe_user_id'],
6039
            api_get_utc_datetime()
6040
        );
6041
    }
6042
6043
    public static function scorePassed($score, $total)
6044
    {
6045
        $compareResult = bccomp($score, $total, 3);
6046
        $scorePassed = 1 === $compareResult || 0 === $compareResult;
6047
        if (false === $scorePassed) {
6048
            $epsilon = 0.00001;
6049
            if (abs($score - $total) < $epsilon) {
6050
                $scorePassed = true;
6051
            }
6052
        }
6053
6054
        return $scorePassed;
6055
    }
6056
}
6057