Passed
Push — master ( 49946d...85f21d )
by
unknown
10:50 queued 42s
created

ExerciseLib::getNotCorrectedYetText()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
6119
                }
6120
            }
6121
        }
6122
6123
        if (empty($mainPath) && !$exportOk) {
6124
            Display::addFlash(
6125
                Display::return_message(
6126
                    get_lang('ExportExerciseNoResult'),
6127
                    'warning',
6128
                    false
6129
                )
6130
            );
6131
        }
6132
6133
        return false;
6134
    }
6135
6136
    /**
6137
     * Generates and saves a PDF file for a specific exercise attempt result.
6138
     */
6139
    public static function saveFileExerciseResultPdf(
6140
        int $exeId,
6141
        int $courseId,
6142
        int $sessionId
6143
    ): void
6144
    {
6145
        $cidReq = 'cid='.$courseId.'&sid='.$sessionId.'&gid=0&gradebook=0';
6146
        $url = api_get_path(WEB_PATH).'main/exercise/exercise_show.php?'.$cidReq.'&id='.$exeId.'&action=export&export_type=all_results';
6147
        $ch = curl_init();
6148
        curl_setopt($ch, CURLOPT_URL, $url);
6149
        curl_setopt($ch, CURLOPT_COOKIE, session_id());
6150
        curl_setopt($ch, CURLOPT_AUTOREFERER, true);
6151
        curl_setopt($ch, CURLOPT_COOKIESESSION, true);
6152
        curl_setopt($ch, CURLOPT_FAILONERROR, false);
6153
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
6154
        curl_setopt($ch, CURLOPT_HEADER, true);
6155
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
6156
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
6157
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
6158
6159
        $result = curl_exec($ch);
6160
6161
        if (false === $result) {
6162
            error_log('saveFileExerciseResultPdf error: '.curl_error($ch));
6163
        }
6164
6165
        curl_close($ch);
6166
    }
6167
}
6168