Passed
Pull Request — master (#6221)
by
unknown
08:20
created

ExerciseLib::isQuizEmbeddable()   A

Complexity

Conditions 3

Size

Total Lines 36
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 15
c 0
b 0
f 0
nop 1
dl 0
loc 36
rs 9.7666
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
                            $sendMailUrl =  api_get_path(WEB_CODE_PATH).'exercise/exercise_report.php?'.api_get_cidreq().'&action=send_email&exerciseId='.$exercise_id.'&attemptId='.$results[$i]['exe_id'];
2361
                            $emailLink = '<a href="'.$sendMailUrl.'">'
2362
                                .Display::getMdiIcon(ActionIcon::SEND_SINGLE_EMAIL, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Send by email'))
2363
                                .'</a>';
2364
2365
                            $filterByUser = isset($_GET['filter_by_user']) ? (int) $_GET['filter_by_user'] : 0;
2366
                            $delete_link = '<a
2367
                                href="exercise_report.php?'.api_get_cidreq().'&filter_by_user='.$filterByUser.'&filter='.$filter.'&exerciseId='.$exercise_id.'&delete=delete&did='.$id.'"
2368
                                onclick=
2369
                                "javascript:if(!confirm(\''.sprintf(addslashes(get_lang('Delete attempt?')), $results[$i]['username'], $dt).'\')) return false;"
2370
                                >';
2371
                            $delete_link .= Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, addslashes(get_lang('Delete'))).'</a>';
2372
2373
                            if (api_is_drh() && !api_is_platform_admin()) {
2374
                                $delete_link = null;
2375
                            }
2376
                            if (api_is_session_admin()) {
2377
                                $delete_link = '';
2378
                            }
2379
                            if (3 == $revised) {
2380
                                $delete_link = null;
2381
                            }
2382
                            if (1 !== $revised) {
2383
                                $emailLink = '';
2384
                            }
2385
                            $actions .= $delete_link;
2386
                            $actions .= $emailLink;
2387
                        }
2388
                    } else {
2389
                        $attempt_url = api_get_path(WEB_CODE_PATH).'exercise/result.php?'.api_get_cidreq().'&id='.$results[$i]['exe_id'].'&sid='.$sessionId;
2390
                        $attempt_link = Display::url(
2391
                            get_lang('Show'),
2392
                            $attempt_url,
2393
                            [
2394
                                'class' => 'ajax btn btn--plain',
2395
                                'data-title' => get_lang('Show'),
2396
                            ]
2397
                        );
2398
                        $actions .= $attempt_link;
2399
                    }
2400
                    $actions .= '</div>';
2401
2402
                    if (!empty($userExtraFieldsToAdd)) {
2403
                        foreach ($userExtraFieldsToAdd as $variable) {
2404
                            $extraFieldValue = new ExtraFieldValue('user');
2405
                            $values = $extraFieldValue->get_values_by_handler_and_field_variable(
2406
                                $results[$i]['user_id'],
2407
                                $variable
2408
                            );
2409
                            if (isset($values['value'])) {
2410
                                $results[$i][$variable] = $values['value'];
2411
                            }
2412
                        }
2413
                    }
2414
2415
                    $exeId = $results[$i]['exe_id'];
2416
                    $results[$i]['id'] = $exeId;
2417
                    $category_list = [];
2418
                    if ($is_allowedToEdit) {
2419
                        $sessionName = '';
2420
                        $sessionStartAccessDate = '';
2421
                        if (!empty($results[$i]['session_id'])) {
2422
                            $sessionInfo = api_get_session_info($results[$i]['session_id']);
2423
                            if (!empty($sessionInfo)) {
2424
                                $sessionName = $sessionInfo['title'];
2425
                                $sessionStartAccessDate = api_get_local_time($sessionInfo['access_start_date']);
2426
                            }
2427
                        }
2428
2429
                        $objExercise = new Exercise($courseId);
2430
                        if ($showExerciseCategories) {
2431
                            // Getting attempt info
2432
                            $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
2433
                            if (!empty($exercise_stat_info['data_tracking'])) {
2434
                                $question_list = explode(',', $exercise_stat_info['data_tracking']);
2435
                                if (!empty($question_list)) {
2436
                                    foreach ($question_list as $questionId) {
2437
                                        $objQuestionTmp = Question::read($questionId, $objExercise->course);
2438
                                        // We're inside *one* question. Go through each possible answer for this question
2439
                                        $result = $objExercise->manage_answer(
2440
                                            $exeId,
2441
                                            $questionId,
2442
                                            null,
2443
                                            'exercise_result',
2444
                                            false,
2445
                                            false,
2446
                                            true,
2447
                                            false,
2448
                                            $objExercise->selectPropagateNeg(),
2449
                                            null,
2450
                                            true
2451
                                        );
2452
2453
                                        $my_total_score = $result['score'];
2454
                                        $my_total_weight = $result['weight'];
2455
2456
                                        // Category report
2457
                                        $category_was_added_for_this_test = false;
2458
                                        if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
2459
                                            if (!isset($category_list[$objQuestionTmp->category]['score'])) {
2460
                                                $category_list[$objQuestionTmp->category]['score'] = 0;
2461
                                            }
2462
                                            if (!isset($category_list[$objQuestionTmp->category]['total'])) {
2463
                                                $category_list[$objQuestionTmp->category]['total'] = 0;
2464
                                            }
2465
                                            $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
2466
                                            $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
2467
                                            $category_was_added_for_this_test = true;
2468
                                        }
2469
2470
                                        if (isset($objQuestionTmp->category_list) &&
2471
                                            !empty($objQuestionTmp->category_list)
2472
                                        ) {
2473
                                            foreach ($objQuestionTmp->category_list as $category_id) {
2474
                                                $category_list[$category_id]['score'] += $my_total_score;
2475
                                                $category_list[$category_id]['total'] += $my_total_weight;
2476
                                                $category_was_added_for_this_test = true;
2477
                                            }
2478
                                        }
2479
2480
                                        // No category for this question!
2481
                                        if (false == $category_was_added_for_this_test) {
2482
                                            if (!isset($category_list['none']['score'])) {
2483
                                                $category_list['none']['score'] = 0;
2484
                                            }
2485
                                            if (!isset($category_list['none']['total'])) {
2486
                                                $category_list['none']['total'] = 0;
2487
                                            }
2488
2489
                                            $category_list['none']['score'] += $my_total_score;
2490
                                            $category_list['none']['total'] += $my_total_weight;
2491
                                        }
2492
                                    }
2493
                                }
2494
                            }
2495
                        }
2496
2497
                        foreach ($category_list as $categoryId => $result) {
2498
                            $scoreToDisplay = self::show_score(
2499
                                $result['score'],
2500
                                $result['total'],
2501
                                true,
2502
                                true,
2503
                                false,
2504
                                false,
2505
                                $decimalSeparator,
2506
                                $thousandSeparator,
2507
                                $roundValues
2508
                            );
2509
                            $results[$i]['category_'.$categoryId] = $scoreToDisplay;
2510
                            $results[$i]['category_'.$categoryId.'_score_percentage'] = self::show_score(
2511
                                $result['score'],
2512
                                $result['total'],
2513
                                true,
2514
                                true,
2515
                                true, // $show_only_percentage = false
2516
                                true, // hide % sign
2517
                                $decimalSeparator,
2518
                                $thousandSeparator,
2519
                                $roundValues
2520
                            );
2521
                            $results[$i]['category_'.$categoryId.'_only_score'] = $result['score'];
2522
                            $results[$i]['category_'.$categoryId.'_total'] = $result['total'];
2523
                        }
2524
                        $results[$i]['session'] = $sessionName;
2525
                        $results[$i]['session_access_start_date'] = $sessionStartAccessDate;
2526
                        $results[$i]['status'] = $revisedLabel;
2527
                        $results[$i]['score'] = $score;
2528
                        $results[$i]['score_percentage'] = self::show_score(
2529
                            $my_res,
2530
                            $my_total,
2531
                            true,
2532
                            true,
2533
                            true,
2534
                            true,
2535
                            $decimalSeparator,
2536
                            $thousandSeparator,
2537
                            $roundValues
2538
                        );
2539
2540
                        if ($roundValues) {
2541
                            $whole = floor($my_res); // 1
2542
                            $fraction = $my_res - $whole; // .25
2543
                            if ($fraction >= 0.5) {
2544
                                $onlyScore = ceil($my_res);
2545
                            } else {
2546
                                $onlyScore = round($my_res);
2547
                            }
2548
                        } else {
2549
                            $onlyScore = $scoreDisplay->format_score(
2550
                                $my_res,
2551
                                false,
2552
                                $decimalSeparator,
2553
                                $thousandSeparator
2554
                            );
2555
                        }
2556
2557
                        $results[$i]['only_score'] = $onlyScore;
2558
2559
                        if ($roundValues) {
2560
                            $whole = floor($my_total); // 1
2561
                            $fraction = $my_total - $whole; // .25
2562
                            if ($fraction >= 0.5) {
2563
                                $onlyTotal = ceil($my_total);
2564
                            } else {
2565
                                $onlyTotal = round($my_total);
2566
                            }
2567
                        } else {
2568
                            $onlyTotal = $scoreDisplay->format_score(
2569
                                $my_total,
2570
                                false,
2571
                                $decimalSeparator,
2572
                                $thousandSeparator
2573
                            );
2574
                        }
2575
                        $results[$i]['total'] = $onlyTotal;
2576
                        $results[$i]['lp'] = $lp_name;
2577
                        $results[$i]['actions'] = $actions;
2578
                        $listInfo[] = $results[$i];
2579
                    } else {
2580
                        $results[$i]['status'] = $revisedLabel;
2581
                        $results[$i]['score'] = $score;
2582
                        $results[$i]['actions'] = $actions;
2583
                        $listInfo[] = $results[$i];
2584
                    }
2585
                }
2586
            }
2587
        }
2588
2589
        return $listInfo;
2590
    }
2591
2592
    /**
2593
     * Returns email content for a specific attempt.
2594
     */
2595
    public static function getEmailContentForAttempt(int $attemptId): array
2596
    {
2597
        $trackExerciseInfo = self::get_exercise_track_exercise_info($attemptId);
2598
2599
        if (empty($trackExerciseInfo)) {
2600
            return [
2601
                'to' => '',
2602
                'subject' => 'No exercise info found',
2603
                'message' => 'Attempt ID not found or invalid.',
2604
            ];
2605
        }
2606
2607
        $studentId = $trackExerciseInfo['exe_user_id'];
2608
        $courseInfo = api_get_course_info();
2609
        $teacherId = api_get_user_id();
2610
2611
        if (
2612
            empty($trackExerciseInfo['orig_lp_id']) ||
2613
            empty($trackExerciseInfo['orig_lp_item_id'])
2614
        ) {
2615
            $url = api_get_path(WEB_CODE_PATH).'exercise/result.php?id='.$trackExerciseInfo['exe_id'].'&'.api_get_cidreq()
2616
                .'&show_headers=1&id_session='.api_get_session_id();
2617
        } else {
2618
            $url = api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?action=view&item_id='
2619
                .$trackExerciseInfo['orig_lp_item_id'].'&lp_id='.$trackExerciseInfo['orig_lp_id'].'&'.api_get_cidreq()
2620
                .'&id_session='.api_get_session_id();
2621
        }
2622
2623
        $message = self::getEmailNotification(
2624
            $teacherId,
2625
            $courseInfo,
2626
            $trackExerciseInfo['title'],
2627
            $url
2628
        );
2629
2630
        return [
2631
            'to' => $studentId,
2632
            'subject' => get_lang('Corrected test result'),
2633
            'message' => $message,
2634
        ];
2635
    }
2636
2637
    /**
2638
     * Sends the exercise result email to the student.
2639
     */
2640
    public static function sendExerciseResultByEmail(int $attemptId): void
2641
    {
2642
        $content = self::getEmailContentForAttempt($attemptId);
2643
2644
        if (empty($content['to'])) {
2645
            return;
2646
        }
2647
2648
        MessageManager::send_message_simple(
2649
            $content['to'],
2650
            $content['subject'],
2651
            $content['message'],
2652
            api_get_user_id()
2653
        );
2654
    }
2655
2656
    /**
2657
     * Returns all reviewed attempts for a given exercise and session.
2658
     */
2659
    public static function getReviewedAttemptsInfo(int $exerciseId, int $sessionId): array
2660
    {
2661
        $courseId = api_get_course_int_id();
2662
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2663
        $qualifyTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY);
2664
2665
        $sessionCondition = api_get_session_condition($sessionId, true, false, 't.session_id');
2666
2667
        $sql = "
2668
            SELECT DISTINCT t.exe_id
2669
            FROM $trackTable t
2670
            INNER JOIN $qualifyTable q ON (t.exe_id = q.exe_id AND q.author > 0)
2671
            WHERE
2672
                t.c_id = $courseId AND
2673
                t.exe_exo_id = $exerciseId
2674
                $sessionCondition
2675
        ";
2676
2677
        return Database::store_result(Database::query($sql));
2678
    }
2679
2680
    /**
2681
     * @param $score
2682
     * @param $weight
2683
     *
2684
     * @return array
2685
     */
2686
    public static function convertScoreToPlatformSetting($score, $weight)
2687
    {
2688
        $maxNote = api_get_setting('exercise_max_score');
2689
        $minNote = api_get_setting('exercise_min_score');
2690
2691
        if ('' != $maxNote && '' != $minNote) {
2692
            if (!empty($weight) && (float) $weight !== (float) 0) {
2693
                $score = $minNote + ($maxNote - $minNote) * $score / $weight;
2694
            } else {
2695
                $score = $minNote;
2696
            }
2697
            $weight = $maxNote;
2698
        }
2699
2700
        return ['score' => $score, 'weight' => $weight];
2701
    }
2702
2703
    /**
2704
     * Converts the score with the exercise_max_note and exercise_min_score
2705
     * the platform settings + formats the results using the float_format function.
2706
     *
2707
     * @param float  $score
2708
     * @param float  $weight
2709
     * @param bool   $show_percentage       show percentage or not
2710
     * @param bool   $use_platform_settings use or not the platform settings
2711
     * @param bool   $show_only_percentage
2712
     * @param bool   $hidePercentageSign    hide "%" sign
2713
     * @param string $decimalSeparator
2714
     * @param string $thousandSeparator
2715
     * @param bool   $roundValues           This option rounds the float values into a int using ceil()
2716
     * @param bool   $removeEmptyDecimals
2717
     *
2718
     * @return string an html with the score modified
2719
     */
2720
    public static function show_score(
2721
        $score,
2722
        $weight,
2723
        $show_percentage = true,
2724
        $use_platform_settings = true,
2725
        $show_only_percentage = false,
2726
        $hidePercentageSign = false,
2727
        $decimalSeparator = '.',
2728
        $thousandSeparator = ',',
2729
        $roundValues = false,
2730
        $removeEmptyDecimals = false
2731
    ) {
2732
        if (is_null($score) && is_null($weight)) {
2733
            return '-';
2734
        }
2735
2736
        $decimalSeparator = empty($decimalSeparator) ? '.' : $decimalSeparator;
2737
        $thousandSeparator = empty($thousandSeparator) ? ',' : $thousandSeparator;
2738
2739
        if ($use_platform_settings) {
2740
            $result = self::convertScoreToPlatformSetting($score, $weight);
2741
            $score = $result['score'];
2742
            $weight = $result['weight'];
2743
        }
2744
2745
        $percentage = (100 * $score) / (0 != $weight ? $weight : 1);
2746
        // Formats values
2747
        $percentage = float_format($percentage, 1);
2748
        $score = float_format($score, 1);
2749
        $weight = float_format($weight, 1);
2750
2751
        if ($roundValues) {
2752
            $whole = floor($percentage); // 1
2753
            $fraction = $percentage - $whole; // .25
2754
2755
            // Formats values
2756
            if ($fraction >= 0.5) {
2757
                $percentage = ceil($percentage);
2758
            } else {
2759
                $percentage = round($percentage);
2760
            }
2761
2762
            $whole = floor($score); // 1
2763
            $fraction = $score - $whole; // .25
2764
            if ($fraction >= 0.5) {
2765
                $score = ceil($score);
2766
            } else {
2767
                $score = round($score);
2768
            }
2769
2770
            $whole = floor($weight); // 1
2771
            $fraction = $weight - $whole; // .25
2772
            if ($fraction >= 0.5) {
2773
                $weight = ceil($weight);
2774
            } else {
2775
                $weight = round($weight);
2776
            }
2777
        } else {
2778
            // Formats values
2779
            $percentage = float_format($percentage, 1, $decimalSeparator, $thousandSeparator);
2780
            $score = float_format($score, 1, $decimalSeparator, $thousandSeparator);
2781
            $weight = float_format($weight, 1, $decimalSeparator, $thousandSeparator);
2782
        }
2783
2784
        if ($show_percentage) {
2785
            $percentageSign = ' %';
2786
            if ($hidePercentageSign) {
2787
                $percentageSign = '';
2788
            }
2789
            $html = $percentage."$percentageSign ($score / $weight)";
2790
            if ($show_only_percentage) {
2791
                $html = $percentage.$percentageSign;
2792
            }
2793
        } else {
2794
            if ($removeEmptyDecimals) {
2795
                if (ScoreDisplay::hasEmptyDecimals($weight)) {
2796
                    $weight = round($weight);
2797
                }
2798
            }
2799
            $html = $score.' / '.$weight;
2800
        }
2801
2802
        // Over write score
2803
        $scoreBasedInModel = self::convertScoreToModel($percentage);
2804
        if (!empty($scoreBasedInModel)) {
2805
            $html = $scoreBasedInModel;
2806
        }
2807
2808
        // Ignore other formats and use the configuration['exercise_score_format'] value
2809
        // But also keep the round values settings.
2810
        $format = (int) api_get_setting('exercise.exercise_score_format');
2811
        if (!empty($format)) {
2812
            $html = ScoreDisplay::instance()->display_score([$score, $weight], $format);
2813
        }
2814
2815
        return Display::span($html, ['class' => 'score_exercise']);
2816
    }
2817
2818
    /**
2819
     * @param array $model
2820
     * @param float $percentage
2821
     *
2822
     * @return string
2823
     */
2824
    public static function getModelStyle($model, $percentage)
2825
    {
2826
        return '<span class="'.$model['css_class'].' p-2">'.$model['name'].'</span>';
2827
    }
2828
2829
    /**
2830
     * @param float $percentage value between 0 and 100
2831
     *
2832
     * @return string
2833
     */
2834
    public static function convertScoreToModel($percentage)
2835
    {
2836
        $model = self::getCourseScoreModel();
2837
        if (!empty($model)) {
2838
            $scoreWithGrade = [];
2839
            foreach ($model['score_list'] as $item) {
2840
                if ($percentage >= $item['min'] && $percentage <= $item['max']) {
2841
                    $scoreWithGrade = $item;
2842
                    break;
2843
                }
2844
            }
2845
2846
            if (!empty($scoreWithGrade)) {
2847
                return self::getModelStyle($scoreWithGrade, $percentage);
2848
            }
2849
        }
2850
2851
        return '';
2852
    }
2853
2854
    /**
2855
     * @return array
2856
     */
2857
    public static function getCourseScoreModel()
2858
    {
2859
        $modelList = self::getScoreModels();
2860
2861
        if (empty($modelList)) {
2862
            return [];
2863
        }
2864
2865
        $courseInfo = api_get_course_info();
2866
        if (!empty($courseInfo)) {
2867
            $scoreModelId = api_get_course_setting('score_model_id');
2868
            if (-1 != $scoreModelId) {
2869
                $modelIdList = array_column($modelList['models'], 'id');
2870
                if (in_array($scoreModelId, $modelIdList)) {
2871
                    foreach ($modelList['models'] as $item) {
2872
                        if ($item['id'] == $scoreModelId) {
2873
                            return $item;
2874
                        }
2875
                    }
2876
                }
2877
            }
2878
        }
2879
2880
        return [];
2881
    }
2882
2883
    /**
2884
     * @return array
2885
     */
2886
    public static function getScoreModels()
2887
    {
2888
        return api_get_setting('exercise.score_grade_model', true);
2889
    }
2890
2891
    /**
2892
     * @param float  $score
2893
     * @param float  $weight
2894
     * @param string $passPercentage
2895
     *
2896
     * @return bool
2897
     */
2898
    public static function isSuccessExerciseResult($score, $weight, $passPercentage)
2899
    {
2900
        $percentage = float_format(
2901
            ($score / (0 != $weight ? $weight : 1)) * 100,
2902
            1
2903
        );
2904
        if (isset($passPercentage) && !empty($passPercentage)) {
2905
            if ($percentage >= $passPercentage) {
2906
                return true;
2907
            }
2908
        }
2909
2910
        return false;
2911
    }
2912
2913
    /**
2914
     * @param string $name
2915
     * @param $weight
2916
     * @param $selected
2917
     *
2918
     * @return bool
2919
     */
2920
    public static function addScoreModelInput(
2921
        FormValidator $form,
2922
        $name,
2923
        $weight,
2924
        $selected
2925
    ) {
2926
        $model = self::getCourseScoreModel();
2927
        if (empty($model)) {
2928
            return false;
2929
        }
2930
2931
        /** @var HTML_QuickForm_select $element */
2932
        $element = $form->createElement(
2933
            'select',
2934
            $name,
2935
            get_lang('Score'),
2936
            [],
2937
            ['class' => 'exercise_mark_select']
2938
        );
2939
2940
        foreach ($model['score_list'] as $item) {
2941
            $i = api_number_format($item['score_to_qualify'] / 100 * $weight, 2);
2942
            $label = self::getModelStyle($item, $i);
2943
            $attributes = [
2944
                'class' => $item['css_class'],
2945
            ];
2946
            if ($selected == $i) {
2947
                $attributes['selected'] = 'selected';
2948
            }
2949
            $element->addOption($label, $i, $attributes);
2950
        }
2951
        $form->addElement($element);
2952
    }
2953
2954
    /**
2955
     * @return string
2956
     */
2957
    public static function getJsCode()
2958
    {
2959
        // Filling the scores with the right colors.
2960
        $models = self::getCourseScoreModel();
2961
        $cssListToString = '';
2962
        if (!empty($models)) {
2963
            $cssList = array_column($models['score_list'], 'css_class');
2964
            $cssListToString = implode(' ', $cssList);
2965
        }
2966
2967
        if (empty($cssListToString)) {
2968
            return '';
2969
        }
2970
        $js = <<<EOT
2971
2972
        function updateSelect(element) {
2973
            var spanTag = element.parent().find('span.filter-option');
2974
            var value = element.val();
2975
            var selectId = element.attr('id');
2976
            var optionClass = $('#' + selectId + ' option[value="'+value+'"]').attr('class');
2977
            spanTag.removeClass('$cssListToString');
2978
            spanTag.addClass(optionClass);
2979
        }
2980
2981
        $(function() {
2982
            // Loading values
2983
            $('.exercise_mark_select').on('loaded.bs.select', function() {
2984
                updateSelect($(this));
2985
            });
2986
            // On change
2987
            $('.exercise_mark_select').on('changed.bs.select', function() {
2988
                updateSelect($(this));
2989
            });
2990
        });
2991
EOT;
2992
2993
        return $js;
2994
    }
2995
2996
    /**
2997
     * @param float  $score
2998
     * @param float  $weight
2999
     * @param string $pass_percentage
3000
     *
3001
     * @return string
3002
     */
3003
    public static function showSuccessMessage($score, $weight, $pass_percentage)
3004
    {
3005
        $res = '';
3006
        if (self::isPassPercentageEnabled($pass_percentage)) {
3007
            $isSuccess = self::isSuccessExerciseResult(
3008
                $score,
3009
                $weight,
3010
                $pass_percentage
3011
            );
3012
3013
            if ($isSuccess) {
3014
                $html = get_lang('Congratulations you passed the test!');
3015
                $icon = Display::getMdiIcon('check-circle', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Correct'));
3016
            } else {
3017
                $html = get_lang('You didn\'t reach the minimum score');
3018
                $icon = Display::getMdiIcon('alert', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Wrong'));
3019
            }
3020
            $html = Display::tag('h4', $html);
3021
            $html .= Display::tag(
3022
                'h5',
3023
                $icon,
3024
                ['style' => 'width:40px; padding:2px 10px 0px 0px']
3025
            );
3026
            $res = $html;
3027
        }
3028
3029
        return $res;
3030
    }
3031
3032
    /**
3033
     * Return true if pass_pourcentage activated (we use the pass pourcentage feature
3034
     * return false if pass_percentage = 0 (we don't use the pass pourcentage feature.
3035
     *
3036
     * @param $value
3037
     *
3038
     * @return bool
3039
     *              In this version, pass_percentage and show_success_message are disabled if
3040
     *              pass_percentage is set to 0
3041
     */
3042
    public static function isPassPercentageEnabled($value)
3043
    {
3044
        return $value > 0;
3045
    }
3046
3047
    /**
3048
     * Converts a numeric value in a percentage example 0.66666 to 66.67 %.
3049
     *
3050
     * @param $value
3051
     *
3052
     * @return float Converted number
3053
     */
3054
    public static function convert_to_percentage($value)
3055
    {
3056
        $return = '-';
3057
        if ('' != $value) {
3058
            $return = float_format($value * 100, 1).' %';
3059
        }
3060
3061
        return $return;
3062
    }
3063
3064
    /**
3065
     * Getting all active exercises from a course from a session
3066
     * (if a session_id is provided we will show all the exercises in the course +
3067
     * all exercises in the session).
3068
     *
3069
     * @param array  $course_info
3070
     * @param int    $session_id
3071
     * @param bool   $check_publication_dates
3072
     * @param string $search                  Search exercise name
3073
     * @param bool   $search_all_sessions     Search exercises in all sessions
3074
     * @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...
3075
     *                  1 = only active exercises,
3076
     *                  2 = all exercises
3077
     *                  3 = active <> -1
3078
     *
3079
     * @return CQuiz[]
3080
     */
3081
    public static function get_all_exercises(
3082
        $course_info = null,
3083
        $session_id = 0,
3084
        $check_publication_dates = false,
3085
        $search = '',
3086
        $search_all_sessions = false,
3087
        $active = 2
3088
    ) {
3089
        $course_id = api_get_course_int_id();
3090
        if (!empty($course_info) && !empty($course_info['real_id'])) {
3091
            $course_id = $course_info['real_id'];
3092
        }
3093
3094
        if (-1 == $session_id) {
3095
            $session_id = 0;
3096
        }
3097
        $course = api_get_course_entity($course_id);
3098
        $session = api_get_session_entity($session_id);
3099
3100
        if (null === $course) {
3101
            return [];
3102
        }
3103
3104
        $repo = Container::getQuizRepository();
3105
3106
        return $repo->findAllByCourse($course, $session, (string) $search, $active)
3107
            ->getQuery()
3108
            ->getResult();
3109
    }
3110
3111
    /**
3112
     * Getting all exercises (active only or all)
3113
     * from a course from a session
3114
     * (if a session_id is provided we will show all the exercises in the
3115
     * course + all exercises in the session).
3116
     */
3117
    public static function get_all_exercises_for_course_id(
3118
        int $courseId,
3119
        int $sessionId = 0,
3120
        bool $onlyActiveExercises = true
3121
    ): array {
3122
3123
        if (!($courseId > 0)) {
3124
            return [];
3125
        }
3126
3127
        $course = api_get_course_entity($courseId);
3128
        $session = api_get_session_entity($sessionId);
3129
3130
        $repo = Container::getQuizRepository();
3131
3132
        $qb = $repo->getResourcesByCourse($course, $session);
3133
3134
        if ($onlyActiveExercises) {
3135
            $qb->andWhere('resource.active = 1');
3136
        } else {
3137
            $qb->andWhere('resource.active IN (1, 0)');
3138
        }
3139
3140
        $qb->orderBy('resource.title', 'ASC');
3141
3142
        $exercises = $qb->getQuery()->getResult();
3143
3144
        $exerciseList = [];
3145
        foreach ($exercises as $exercise) {
3146
            $exerciseList[] = [
3147
                'iid' => $exercise->getIid(),
3148
                'title' => $exercise->getTitle(),
3149
            ];
3150
        }
3151
3152
        return $exerciseList;
3153
    }
3154
3155
    /**
3156
     * Gets the position of the score based in a given score (result/weight)
3157
     * and the exe_id based in the user list
3158
     * (NO Exercises in LPs ).
3159
     *
3160
     * @param float  $my_score      user score to be compared *attention*
3161
     *                              $my_score = score/weight and not just the score
3162
     * @param int    $my_exe_id     exe id of the exercise
3163
     *                              (this is necessary because if 2 students have the same score the one
3164
     *                              with the minor exe_id will have a best position, just to be fair and FIFO)
3165
     * @param int    $exercise_id
3166
     * @param string $course_code
3167
     * @param int    $session_id
3168
     * @param array  $user_list
3169
     * @param bool   $return_string
3170
     *
3171
     * @return int the position of the user between his friends in a course
3172
     *             (or course within a session)
3173
     */
3174
    public static function get_exercise_result_ranking(
3175
        $my_score,
3176
        $my_exe_id,
3177
        $exercise_id,
3178
        $course_code,
3179
        $session_id = 0,
3180
        $user_list = [],
3181
        $return_string = true
3182
    ) {
3183
        //No score given we return
3184
        if (is_null($my_score)) {
3185
            return '-';
3186
        }
3187
        if (empty($user_list)) {
3188
            return '-';
3189
        }
3190
3191
        $best_attempts = [];
3192
        foreach ($user_list as $user_data) {
3193
            $user_id = $user_data['user_id'];
3194
            $best_attempts[$user_id] = self::get_best_attempt_by_user(
3195
                $user_id,
3196
                $exercise_id,
3197
                $course_code,
3198
                $session_id
3199
            );
3200
        }
3201
3202
        if (empty($best_attempts)) {
3203
            return 1;
3204
        } else {
3205
            $position = 1;
3206
            $my_ranking = [];
3207
            foreach ($best_attempts as $user_id => $result) {
3208
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3209
                    $my_ranking[$user_id] = $result['score'] / $result['max_score'];
3210
                } else {
3211
                    $my_ranking[$user_id] = 0;
3212
                }
3213
            }
3214
            //if (!empty($my_ranking)) {
3215
            asort($my_ranking);
3216
            $position = count($my_ranking);
3217
            if (!empty($my_ranking)) {
3218
                foreach ($my_ranking as $user_id => $ranking) {
3219
                    if ($my_score >= $ranking) {
3220
                        if ($my_score == $ranking && isset($best_attempts[$user_id]['exe_id'])) {
3221
                            $exe_id = $best_attempts[$user_id]['exe_id'];
3222
                            if ($my_exe_id < $exe_id) {
3223
                                $position--;
3224
                            }
3225
                        } else {
3226
                            $position--;
3227
                        }
3228
                    }
3229
                }
3230
            }
3231
            //}
3232
            $return_value = [
3233
                'position' => $position,
3234
                'count' => count($my_ranking),
3235
            ];
3236
3237
            if ($return_string) {
3238
                if (!empty($position) && !empty($my_ranking)) {
3239
                    $return_value = $position.'/'.count($my_ranking);
3240
                } else {
3241
                    $return_value = '-';
3242
                }
3243
            }
3244
3245
            return $return_value;
3246
        }
3247
    }
3248
3249
    /**
3250
     * Gets the position of the score based in a given score (result/weight) and the exe_id based in all attempts
3251
     * (NO Exercises in LPs ) old functionality by attempt.
3252
     *
3253
     * @param   float   user score to be compared attention => score/weight
3254
     * @param   int     exe id of the exercise
3255
     * (this is necessary because if 2 students have the same score the one
3256
     * with the minor exe_id will have a best position, just to be fair and FIFO)
3257
     * @param   int     exercise id
3258
     * @param   string  course code
3259
     * @param   int     session id
3260
     * @param bool $return_string
3261
     *
3262
     * @return int the position of the user between his friends in a course (or course within a session)
3263
     */
3264
    public static function get_exercise_result_ranking_by_attempt(
3265
        $my_score,
3266
        $my_exe_id,
3267
        $exercise_id,
3268
        $courseId,
3269
        $session_id = 0,
3270
        $return_string = true
3271
    ) {
3272
        if (empty($session_id)) {
3273
            $session_id = 0;
3274
        }
3275
        if (is_null($my_score)) {
3276
            return '-';
3277
        }
3278
        $user_results = Event::get_all_exercise_results(
3279
            $exercise_id,
3280
            $courseId,
3281
            $session_id,
3282
            false
3283
        );
3284
        $position_data = [];
3285
        if (empty($user_results)) {
3286
            return 1;
3287
        } else {
3288
            $position = 1;
3289
            $my_ranking = [];
3290
            foreach ($user_results as $result) {
3291
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3292
                    $my_ranking[$result['exe_id']] = $result['score'] / $result['max_score'];
3293
                } else {
3294
                    $my_ranking[$result['exe_id']] = 0;
3295
                }
3296
            }
3297
            asort($my_ranking);
3298
            $position = count($my_ranking);
3299
            if (!empty($my_ranking)) {
3300
                foreach ($my_ranking as $exe_id => $ranking) {
3301
                    if ($my_score >= $ranking) {
3302
                        if ($my_score == $ranking) {
3303
                            if ($my_exe_id < $exe_id) {
3304
                                $position--;
3305
                            }
3306
                        } else {
3307
                            $position--;
3308
                        }
3309
                    }
3310
                }
3311
            }
3312
            $return_value = [
3313
                'position' => $position,
3314
                'count' => count($my_ranking),
3315
            ];
3316
3317
            if ($return_string) {
3318
                if (!empty($position) && !empty($my_ranking)) {
3319
                    return $position.'/'.count($my_ranking);
3320
                }
3321
            }
3322
3323
            return $return_value;
3324
        }
3325
    }
3326
3327
    /**
3328
     * Get the best attempt in a exercise (NO Exercises in LPs ).
3329
     *
3330
     * @param int $exercise_id
3331
     * @param int $courseId
3332
     * @param int $session_id
3333
     *
3334
     * @return array
3335
     */
3336
    public static function get_best_attempt_in_course($exercise_id, $courseId, $session_id)
3337
    {
3338
        $user_results = Event::get_all_exercise_results(
3339
            $exercise_id,
3340
            $courseId,
3341
            $session_id,
3342
            false
3343
        );
3344
3345
        $best_score_data = [];
3346
        $best_score = 0;
3347
        if (!empty($user_results)) {
3348
            foreach ($user_results as $result) {
3349
                if (!empty($result['max_score']) &&
3350
                    0 != intval($result['max_score'])
3351
                ) {
3352
                    $score = $result['score'] / $result['max_score'];
3353
                    if ($score >= $best_score) {
3354
                        $best_score = $score;
3355
                        $best_score_data = $result;
3356
                    }
3357
                }
3358
            }
3359
        }
3360
3361
        return $best_score_data;
3362
    }
3363
3364
    /**
3365
     * Get the best score in a exercise (NO Exercises in LPs ).
3366
     *
3367
     * @param int $user_id
3368
     * @param int $exercise_id
3369
     * @param int $courseId
3370
     * @param int $session_id
3371
     *
3372
     * @return array
3373
     */
3374
    public static function get_best_attempt_by_user(
3375
        $user_id,
3376
        $exercise_id,
3377
        $courseId,
3378
        $session_id
3379
    ) {
3380
        $user_results = Event::get_all_exercise_results(
3381
            $exercise_id,
3382
            $courseId,
3383
            $session_id,
3384
            false,
3385
            $user_id
3386
        );
3387
        $best_score_data = [];
3388
        $best_score = 0;
3389
        if (!empty($user_results)) {
3390
            foreach ($user_results as $result) {
3391
                if (!empty($result['max_score']) && 0 != (float) $result['max_score']) {
3392
                    $score = $result['score'] / $result['max_score'];
3393
                    if ($score >= $best_score) {
3394
                        $best_score = $score;
3395
                        $best_score_data = $result;
3396
                    }
3397
                }
3398
            }
3399
        }
3400
3401
        return $best_score_data;
3402
    }
3403
3404
    /**
3405
     * Get average score (NO Exercises in LPs ).
3406
     *
3407
     * @param    int    exercise id
3408
     * @param int $courseId
3409
     * @param    int    session id
3410
     *
3411
     * @return float Average score
3412
     */
3413
    public static function get_average_score($exercise_id, $courseId, $session_id)
3414
    {
3415
        $user_results = Event::get_all_exercise_results(
3416
            $exercise_id,
3417
            $courseId,
3418
            $session_id
3419
        );
3420
        $avg_score = 0;
3421
        if (!empty($user_results)) {
3422
            foreach ($user_results as $result) {
3423
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3424
                    $score = $result['score'] / $result['max_score'];
3425
                    $avg_score += $score;
3426
                }
3427
            }
3428
            $avg_score = float_format($avg_score / count($user_results), 1);
3429
        }
3430
3431
        return $avg_score;
3432
    }
3433
3434
    /**
3435
     * Get average score by score (NO Exercises in LPs ).
3436
     *
3437
     * @param int $courseId
3438
     * @param    int    session id
3439
     *
3440
     * @return float Average score
3441
     */
3442
    public static function get_average_score_by_course($courseId, $session_id)
3443
    {
3444
        $user_results = Event::get_all_exercise_results_by_course(
3445
            $courseId,
3446
            $session_id,
3447
            false
3448
        );
3449
        $avg_score = 0;
3450
        if (!empty($user_results)) {
3451
            foreach ($user_results as $result) {
3452
                if (!empty($result['max_score']) && 0 != intval(
3453
                        $result['max_score']
3454
                    )
3455
                ) {
3456
                    $score = $result['score'] / $result['max_score'];
3457
                    $avg_score += $score;
3458
                }
3459
            }
3460
            // We assume that all max_score
3461
            $avg_score = $avg_score / count($user_results);
3462
        }
3463
3464
        return $avg_score;
3465
    }
3466
3467
    /**
3468
     * @param int $user_id
3469
     * @param int $courseId
3470
     * @param int $session_id
3471
     *
3472
     * @return float|int
3473
     */
3474
    public static function get_average_score_by_course_by_user(
3475
        $user_id,
3476
        $courseId,
3477
        $session_id
3478
    ) {
3479
        $user_results = Event::get_all_exercise_results_by_user(
3480
            $user_id,
3481
            $courseId,
3482
            $session_id
3483
        );
3484
        $avg_score = 0;
3485
        if (!empty($user_results)) {
3486
            foreach ($user_results as $result) {
3487
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3488
                    $score = $result['score'] / $result['max_score'];
3489
                    $avg_score += $score;
3490
                }
3491
            }
3492
            // We assume that all max_score
3493
            $avg_score = ($avg_score / count($user_results));
3494
        }
3495
3496
        return $avg_score;
3497
    }
3498
3499
    /**
3500
     * Get average score by score (NO Exercises in LPs ).
3501
     *
3502
     * @param int $exercise_id
3503
     * @param int $courseId
3504
     * @param int $session_id
3505
     * @param int $user_count
3506
     *
3507
     * @return float Best average score
3508
     */
3509
    public static function get_best_average_score_by_exercise(
3510
        $exercise_id,
3511
        $courseId,
3512
        $session_id,
3513
        $user_count
3514
    ) {
3515
        $user_results = Event::get_best_exercise_results_by_user(
3516
            $exercise_id,
3517
            $courseId,
3518
            $session_id
3519
        );
3520
        $avg_score = 0;
3521
        if (!empty($user_results)) {
3522
            foreach ($user_results as $result) {
3523
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3524
                    $score = $result['score'] / $result['max_score'];
3525
                    $avg_score += $score;
3526
                }
3527
            }
3528
            // We asumme that all max_score
3529
            if (!empty($user_count)) {
3530
                $avg_score = float_format($avg_score / $user_count, 1) * 100;
3531
            } else {
3532
                $avg_score = 0;
3533
            }
3534
        }
3535
3536
        return $avg_score;
3537
    }
3538
3539
    /**
3540
     * Get average score by score (NO Exercises in LPs ).
3541
     *
3542
     * @param int $exercise_id
3543
     * @param int $courseId
3544
     * @param int $session_id
3545
     *
3546
     * @return float Best average score
3547
     */
3548
    public static function getBestScoreByExercise(
3549
        $exercise_id,
3550
        $courseId,
3551
        $session_id
3552
    ) {
3553
        $user_results = Event::get_best_exercise_results_by_user(
3554
            $exercise_id,
3555
            $courseId,
3556
            $session_id
3557
        );
3558
        $avg_score = 0;
3559
        if (!empty($user_results)) {
3560
            foreach ($user_results as $result) {
3561
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3562
                    $score = $result['score'] / $result['max_score'];
3563
                    $avg_score += $score;
3564
                }
3565
            }
3566
        }
3567
3568
        return $avg_score;
3569
    }
3570
3571
    /**
3572
     * Get student results (only in completed exercises) stats by question.
3573
     *
3574
     * @throws \Doctrine\DBAL\Exception
3575
     */
3576
    public static function getStudentStatsByQuestion(
3577
        int $questionId,
3578
        int $exerciseId,
3579
        string $courseCode,
3580
        int $sessionId,
3581
        bool $onlyStudent = false
3582
    ): array
3583
    {
3584
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3585
        $trackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3586
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3587
3588
        $questionId = (int) $questionId;
3589
        $exerciseId = (int) $exerciseId;
3590
        $courseCode = Database::escape_string($courseCode);
3591
        $sessionId = (int) $sessionId;
3592
        $courseId = api_get_course_int_id($courseCode);
3593
3594
        $sql = "SELECT MAX(marks) as max, MIN(marks) as min, AVG(marks) as average
3595
                FROM $trackExercises e ";
3596
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
3597
        if ($onlyStudent) {
3598
            $courseCondition = '';
3599
            if (empty($sessionId)) {
3600
                $courseCondition = "
3601
                INNER JOIN $courseUser c
3602
                ON (
3603
                    e.exe_user_id = c.user_id AND
3604
                    e.c_id = c.c_id AND
3605
                    c.status = ".STUDENT." AND
3606
                    relation_type <> 2
3607
                )";
3608
            } else {
3609
                $sessionRelCourse = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3610
                $courseCondition = "
3611
            INNER JOIN $sessionRelCourse sc
3612
            ON (
3613
                        e.exe_user_id = sc.user_id AND
3614
                        e.c_id = sc.c_id AND
3615
                        e.session_id = sc.session_id AND
3616
                        sc.status = ".SessionEntity::STUDENT."
3617
                )";
3618
            }
3619
            $sql .= $courseCondition;
3620
        }
3621
        $sql .= "
3622
    		INNER JOIN $trackAttempt a
3623
    		ON (
3624
    		    a.exe_id = e.exe_id
3625
            )
3626
    		WHERE
3627
    		    exe_exo_id 	= $exerciseId AND
3628
                e.c_id = $courseId AND
3629
                question_id = $questionId AND
3630
                e.status = ''
3631
                $sessionCondition
3632
            LIMIT 1";
3633
        $result = Database::query($sql);
3634
        $return = [];
3635
        if ($result) {
3636
            $return = Database::fetch_assoc($result);
3637
        }
3638
3639
        return $return;
3640
    }
3641
3642
    /**
3643
     * Get the correct answer count for a fill blanks question.
3644
     *
3645
     * @param int $question_id
3646
     * @param int $exercise_id
3647
     *
3648
     * @return array
3649
     */
3650
    public static function getNumberStudentsFillBlanksAnswerCount(
3651
        $question_id,
3652
        $exercise_id
3653
    ) {
3654
        $listStudentsId = [];
3655
        $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
3656
            api_get_course_id(),
3657
            true
3658
        );
3659
        foreach ($listAllStudentInfo as $i => $listStudentInfo) {
3660
            $listStudentsId[] = $listStudentInfo['user_id'];
3661
        }
3662
3663
        $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3664
            $exercise_id,
3665
            $question_id,
3666
            $listStudentsId,
3667
            '1970-01-01',
3668
            '3000-01-01'
3669
        );
3670
3671
        $arrayCount = [];
3672
3673
        foreach ($listFillTheBlankResult as $resultCount) {
3674
            foreach ($resultCount as $index => $count) {
3675
                //this is only for declare the array index per answer
3676
                $arrayCount[$index] = 0;
3677
            }
3678
        }
3679
3680
        foreach ($listFillTheBlankResult as $resultCount) {
3681
            foreach ($resultCount as $index => $count) {
3682
                $count = (0 === $count) ? 1 : 0;
3683
                $arrayCount[$index] += $count;
3684
            }
3685
        }
3686
3687
        return $arrayCount;
3688
    }
3689
3690
    /**
3691
     * Get the number of questions with answers.
3692
     *
3693
     * @param int    $question_id
3694
     * @param int    $exercise_id
3695
     * @param string $course_code
3696
     * @param int    $session_id
3697
     * @param string $questionType
3698
     *
3699
     * @return int
3700
     */
3701
    public static function get_number_students_question_with_answer_count(
3702
        $question_id,
3703
        $exercise_id,
3704
        $course_code,
3705
        $session_id,
3706
        $questionType = ''
3707
    ) {
3708
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3709
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3710
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3711
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
3712
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3713
3714
        $question_id = intval($question_id);
3715
        $exercise_id = intval($exercise_id);
3716
        $courseId = api_get_course_int_id($course_code);
3717
        $session_id = intval($session_id);
3718
3719
        if (FILL_IN_BLANKS == $questionType) {
3720
            $listStudentsId = [];
3721
            $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
3722
                api_get_course_id(),
3723
                true
3724
            );
3725
            foreach ($listAllStudentInfo as $i => $listStudentInfo) {
3726
                $listStudentsId[] = $listStudentInfo['user_id'];
3727
            }
3728
3729
            $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3730
                $exercise_id,
3731
                $question_id,
3732
                $listStudentsId,
3733
                '1970-01-01',
3734
                '3000-01-01'
3735
            );
3736
3737
            return FillBlanks::getNbResultFillBlankAll($listFillTheBlankResult);
3738
        }
3739
3740
        if (empty($session_id)) {
3741
            $courseCondition = "
3742
            INNER JOIN $courseUser cu
3743
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
3744
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3745
        } else {
3746
            $courseCondition = "
3747
            INNER JOIN $courseUserSession cu
3748
            ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
3749
            $courseConditionWhere = " AND cu.status = ".SessionEntity::STUDENT;
3750
        }
3751
3752
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
3753
        $sql = "SELECT DISTINCT exe_user_id
3754
                FROM $track_exercises e
3755
                INNER JOIN $track_attempt a
3756
                ON (
3757
                    a.exe_id = e.exe_id
3758
                )
3759
                INNER JOIN $courseTable c
3760
                ON (c.id = e.c_id)
3761
                $courseCondition
3762
                WHERE
3763
                    exe_exo_id = $exercise_id AND
3764
                    e.c_id = $courseId AND
3765
                    question_id = $question_id AND
3766
                    answer <> '0' AND
3767
                    e.status = ''
3768
                    $courseConditionWhere
3769
                    $sessionCondition
3770
            ";
3771
        $result = Database::query($sql);
3772
        $return = 0;
3773
        if ($result) {
3774
            $return = Database::num_rows($result);
3775
        }
3776
3777
        return $return;
3778
    }
3779
3780
    /**
3781
     * Get number of answers to hotspot questions.
3782
     */
3783
    public static function getNumberStudentsAnswerHotspotCount(
3784
        int    $answerId,
3785
        int    $questionId,
3786
        int    $exerciseId,
3787
        string $courseCode,
3788
        int $sessionId
3789
    ): int
3790
    {
3791
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3792
        $trackHotspot = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
3793
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3794
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
3795
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3796
3797
        $questionId = (int) $questionId;
3798
        $answerId = (int) $answerId;
3799
        $exerciseId = (int) $exerciseId;
3800
        $courseId = api_get_course_int_id($courseCode);
3801
        $sessionId = (int) $sessionId;
3802
3803
        if (empty($sessionId)) {
3804
            $courseCondition = "
3805
            INNER JOIN $courseUser cu
3806
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
3807
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3808
        } else {
3809
            $courseCondition = "
3810
            INNER JOIN $courseUserSession cu
3811
            ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
3812
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
3813
        }
3814
3815
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
3816
        $sql = "SELECT DISTINCT exe_user_id
3817
                FROM $trackExercises e
3818
                INNER JOIN $trackHotspot a
3819
                ON (a.hotspot_exe_id = e.exe_id)
3820
                INNER JOIN $courseTable c
3821
                ON (a.c_id = c.id)
3822
                $courseCondition
3823
                WHERE
3824
                    exe_exo_id              = $exerciseId AND
3825
                    a.c_id 	= $courseId AND
3826
                    hotspot_answer_id       = $answerId AND
3827
                    hotspot_question_id     = $questionId AND
3828
                    hotspot_correct         =  1 AND
3829
                    e.status                = ''
3830
                    $courseConditionWhere
3831
                    $sessionCondition
3832
            ";
3833
        $result = Database::query($sql);
3834
        $return = 0;
3835
        if ($result) {
3836
            $return = Database::num_rows($result);
3837
        }
3838
3839
        return $return;
3840
    }
3841
3842
    /**
3843
     * @param int    $answer_id
3844
     * @param int    $question_id
3845
     * @param int    $exercise_id
3846
     * @param string $course_code
3847
     * @param int    $session_id
3848
     * @param string $question_type
3849
     * @param string $correct_answer
3850
     * @param string $current_answer
3851
     *
3852
     * @return int
3853
     */
3854
    public static function get_number_students_answer_count(
3855
        $answer_id,
3856
        $question_id,
3857
        $exercise_id,
3858
        $course_code,
3859
        $session_id,
3860
        $question_type = null,
3861
        $correct_answer = null,
3862
        $current_answer = null
3863
    ) {
3864
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3865
        $track_attempt = 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
        $question_id = (int) $question_id;
3871
        $answer_id = (int) $answer_id;
3872
        $exercise_id = (int) $exercise_id;
3873
        $courseId = api_get_course_int_id($course_code);
3874
        $session_id = (int) $session_id;
3875
3876
        switch ($question_type) {
3877
            case FILL_IN_BLANKS:
3878
                $answer_condition = '';
3879
                $select_condition = ' e.exe_id, answer ';
3880
                break;
3881
            case MATCHING:
3882
            case MATCHING_DRAGGABLE:
3883
            default:
3884
                $answer_condition = " answer = $answer_id AND ";
3885
                $select_condition = ' DISTINCT exe_user_id ';
3886
        }
3887
3888
        if (empty($session_id)) {
3889
            $courseCondition = "
3890
            INNER JOIN $courseUser cu
3891
            ON cu.c_id = c.id AND cu.user_id = exe_user_id";
3892
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3893
        } else {
3894
            $courseCondition = "
3895
            INNER JOIN $courseUserSession cu
3896
            ON (cu.c_id = a.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
3897
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
3898
        }
3899
3900
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
3901
        $sql = "SELECT $select_condition
3902
                FROM $track_exercises e
3903
                INNER JOIN $track_attempt a
3904
                ON (
3905
                    a.exe_id = e.exe_id
3906
                )
3907
                INNER JOIN $courseTable c
3908
                ON c.id = e.c_id
3909
                $courseCondition
3910
                WHERE
3911
                    exe_exo_id = $exercise_id AND
3912
                    e.c_id = $courseId AND
3913
                    $answer_condition
3914
                    question_id = $question_id AND
3915
                    e.status = ''
3916
                    $courseConditionWhere
3917
                    $sessionCondition
3918
            ";
3919
        $result = Database::query($sql);
3920
        $return = 0;
3921
        if ($result) {
3922
            $good_answers = 0;
3923
            switch ($question_type) {
3924
                case FILL_IN_BLANKS:
3925
                    while ($row = Database::fetch_assoc($result)) {
3926
                        $fill_blank = self::check_fill_in_blanks(
3927
                            $correct_answer,
3928
                            $row['answer'],
3929
                            $current_answer
3930
                        );
3931
                        if (isset($fill_blank[$current_answer]) && 1 == $fill_blank[$current_answer]) {
3932
                            $good_answers++;
3933
                        }
3934
                    }
3935
3936
                    return $good_answers;
3937
                    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...
3938
                case MATCHING:
3939
                case MATCHING_DRAGGABLE:
3940
                default:
3941
                    $return = Database::num_rows($result);
3942
            }
3943
        }
3944
3945
        return $return;
3946
    }
3947
3948
    /**
3949
     * Get the number of times an answer was selected.
3950
     */
3951
    public static function getCountOfAnswers(
3952
        int $answerId,
3953
        int $questionId,
3954
        int $exerciseId,
3955
        string $courseCode,
3956
        int $sessionId,
3957
        $questionType = null,
3958
    ): int
3959
    {
3960
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3961
        $trackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3962
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
3963
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3964
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3965
3966
        $answerId = (int) $answerId;
3967
        $questionId = (int) $questionId;
3968
        $exerciseId = (int) $exerciseId;
3969
        $courseId = api_get_course_int_id($courseCode);
3970
        $sessionId = (int) $sessionId;
3971
        $return = 0;
3972
3973
        $answerCondition = match ($questionType) {
3974
            FILL_IN_BLANKS => '',
3975
            default => " answer = $answerId AND ",
3976
        };
3977
3978
        if (empty($sessionId)) {
3979
            $courseCondition = "
3980
            INNER JOIN $courseUser cu
3981
            ON cu.c_id = c.id AND cu.user_id = exe_user_id";
3982
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3983
        } else {
3984
            $courseCondition = "
3985
            INNER JOIN $courseUserSession cu
3986
            ON (cu.c_id = a.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
3987
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
3988
        }
3989
3990
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
3991
        $sql = "SELECT count(a.answer) as total
3992
                FROM $trackExercises e
3993
                INNER JOIN $trackAttempt a
3994
                ON (
3995
                    a.exe_id = e.exe_id
3996
                )
3997
                INNER JOIN $courseTable c
3998
                ON c.id = e.c_id
3999
                $courseCondition
4000
                WHERE
4001
                    exe_exo_id = $exerciseId AND
4002
                    e.c_id = $courseId AND
4003
                    $answerCondition
4004
                    question_id = $questionId AND
4005
                    e.status = ''
4006
                    $courseConditionWhere
4007
                    $sessionCondition
4008
            ";
4009
        $result = Database::query($sql);
4010
        if ($result) {
4011
            $count = Database::fetch_array($result);
4012
            $return = (int) $count['total'];
4013
        }
4014
        return $return;
4015
    }
4016
4017
    /**
4018
     * @param array  $answer
4019
     * @param string $user_answer
4020
     *
4021
     * @return array
4022
     */
4023
    public static function check_fill_in_blanks($answer, $user_answer, $current_answer)
4024
    {
4025
        // the question is encoded like this
4026
        // [A] B [C] D [E] F::10,10,10@1
4027
        // number 1 before the "@" means that is a switchable fill in blank question
4028
        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4029
        // means that is a normal fill blank question
4030
        // first we explode the "::"
4031
        $pre_array = explode('::', $answer);
4032
        // is switchable fill blank or not
4033
        $last = count($pre_array) - 1;
4034
        $is_set_switchable = explode('@', $pre_array[$last]);
4035
        $switchable_answer_set = false;
4036
        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
4037
            $switchable_answer_set = true;
4038
        }
4039
        $answer = '';
4040
        for ($k = 0; $k < $last; $k++) {
4041
            $answer .= $pre_array[$k];
4042
        }
4043
        // splits weightings that are joined with a comma
4044
        $answerWeighting = explode(',', $is_set_switchable[0]);
4045
4046
        // we save the answer because it will be modified
4047
        //$temp = $answer;
4048
        $temp = $answer;
4049
4050
        $answer = '';
4051
        $j = 0;
4052
        //initialise answer tags
4053
        $user_tags = $correct_tags = $real_text = [];
4054
        // the loop will stop at the end of the text
4055
        while (1) {
4056
            // quits the loop if there are no more blanks (detect '[')
4057
            if (false === ($pos = api_strpos($temp, '['))) {
4058
                // adds the end of the text
4059
                $answer = $temp;
4060
                $real_text[] = $answer;
4061
                break; //no more "blanks", quit the loop
4062
            }
4063
            // adds the piece of text that is before the blank
4064
            //and ends with '[' into a general storage array
4065
            $real_text[] = api_substr($temp, 0, $pos + 1);
4066
            $answer .= api_substr($temp, 0, $pos + 1);
4067
            //take the string remaining (after the last "[" we found)
4068
            $temp = api_substr($temp, $pos + 1);
4069
            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4070
            if (false === ($pos = api_strpos($temp, ']'))) {
4071
                // adds the end of the text
4072
                $answer .= $temp;
4073
                break;
4074
            }
4075
4076
            $str = $user_answer;
4077
4078
            preg_match_all('#\[([^[]*)\]#', $str, $arr);
4079
            $str = str_replace('\r\n', '', $str);
4080
            $choices = $arr[1];
4081
            $choice = [];
4082
            $check = false;
4083
            $i = 0;
4084
            foreach ($choices as $item) {
4085
                if ($current_answer === $item) {
4086
                    $check = true;
4087
                }
4088
                if ($check) {
4089
                    $choice[] = $item;
4090
                    $i++;
4091
                }
4092
                if (3 == $i) {
4093
                    break;
4094
                }
4095
            }
4096
            $tmp = api_strrpos($choice[$j], ' / ');
4097
4098
            if (false !== $tmp) {
4099
                $choice[$j] = api_substr($choice[$j], 0, $tmp);
4100
            }
4101
4102
            $choice[$j] = trim($choice[$j]);
4103
4104
            //Needed to let characters ' and " to work as part of an answer
4105
            $choice[$j] = stripslashes($choice[$j]);
4106
4107
            $user_tags[] = api_strtolower($choice[$j]);
4108
            //put the contents of the [] answer tag into correct_tags[]
4109
            $correct_tags[] = api_strtolower(api_substr($temp, 0, $pos));
4110
            $j++;
4111
            $temp = api_substr($temp, $pos + 1);
4112
        }
4113
4114
        $answer = '';
4115
        $real_correct_tags = $correct_tags;
4116
        $chosen_list = [];
4117
        $good_answer = [];
4118
4119
        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...
4120
            if (!$switchable_answer_set) {
4121
                //needed to parse ' and " characters
4122
                $user_tags[$i] = stripslashes($user_tags[$i]);
4123
                if ($correct_tags[$i] == $user_tags[$i]) {
4124
                    $good_answer[$correct_tags[$i]] = 1;
4125
                } elseif (!empty($user_tags[$i])) {
4126
                    $good_answer[$correct_tags[$i]] = 0;
4127
                } else {
4128
                    $good_answer[$correct_tags[$i]] = 0;
4129
                }
4130
            } else {
4131
                // switchable fill in the blanks
4132
                if (in_array($user_tags[$i], $correct_tags)) {
4133
                    $correct_tags = array_diff($correct_tags, $chosen_list);
4134
                    $good_answer[$correct_tags[$i]] = 1;
4135
                } elseif (!empty($user_tags[$i])) {
4136
                    $good_answer[$correct_tags[$i]] = 0;
4137
                } else {
4138
                    $good_answer[$correct_tags[$i]] = 0;
4139
                }
4140
            }
4141
            // adds the correct word, followed by ] to close the blank
4142
            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4143
            if (isset($real_text[$i + 1])) {
4144
                $answer .= $real_text[$i + 1];
4145
            }
4146
        }
4147
4148
        return $good_answer;
4149
    }
4150
4151
    /**
4152
     * Return an HTML select menu with the student groups.
4153
     *
4154
     * @param string $name     is the name and the id of the <select>
4155
     * @param string $default  default value for option
4156
     * @param string $onchange
4157
     *
4158
     * @return string the html code of the <select>
4159
     */
4160
    public static function displayGroupMenu($name, $default, $onchange = "")
4161
    {
4162
        // check the default value of option
4163
        $tabSelected = [$default => " selected='selected' "];
4164
        $res = "<select name='$name' id='$name' onchange='".$onchange."' >";
4165
        $res .= "<option value='-1'".$tabSelected["-1"].">-- ".get_lang('AllGroups')." --</option>";
4166
        $res .= "<option value='0'".$tabSelected["0"].">- ".get_lang('NotInAGroup')." -</option>";
4167
        $groups = GroupManager::get_group_list();
4168
        $currentCatId = 0;
4169
        $countGroups = count($groups);
4170
        for ($i = 0; $i < $countGroups; $i++) {
4171
            $category = GroupManager::get_category_from_group($groups[$i]['iid']);
4172
            if ($category['id'] != $currentCatId) {
4173
                $res .= "<option value='-1' disabled='disabled'>".$category['title']."</option>";
4174
                $currentCatId = $category['id'];
4175
            }
4176
            $res .= "<option ".$tabSelected[$groups[$i]['id']]."style='margin-left:40px' value='".
4177
                $groups[$i]["iid"]."'>".
4178
                $groups[$i]["name"].
4179
                "</option>";
4180
        }
4181
        $res .= "</select>";
4182
4183
        return $res;
4184
    }
4185
4186
    /**
4187
     * @param int $exe_id
4188
     */
4189
    public static function create_chat_exercise_session($exe_id)
4190
    {
4191
        if (!isset($_SESSION['current_exercises'])) {
4192
            $_SESSION['current_exercises'] = [];
4193
        }
4194
        $_SESSION['current_exercises'][$exe_id] = true;
4195
    }
4196
4197
    /**
4198
     * @param int $exe_id
4199
     */
4200
    public static function delete_chat_exercise_session($exe_id)
4201
    {
4202
        if (isset($_SESSION['current_exercises'])) {
4203
            $_SESSION['current_exercises'][$exe_id] = false;
4204
        }
4205
    }
4206
4207
    /**
4208
     * Display the exercise results.
4209
     *
4210
     * @param Exercise $objExercise
4211
     * @param int      $exeId
4212
     * @param bool     $save_user_result save users results (true) or just show the results (false)
4213
     * @param string   $remainingMessage
4214
     * @param bool     $allowSignature
4215
     * @param bool     $allowExportPdf
4216
     * @param bool     $isExport
4217
     */
4218
    public static function displayQuestionListByAttempt(
4219
        $objExercise,
4220
        $exeId,
4221
        $save_user_result = false,
4222
        $remainingMessage = '',
4223
        $allowSignature = false,
4224
        $allowExportPdf = false,
4225
        $isExport = false
4226
    ) {
4227
        $origin = api_get_origin();
4228
        $courseId = api_get_course_int_id();
4229
        $courseCode = api_get_course_id();
4230
        $sessionId = api_get_session_id();
4231
4232
        // Getting attempt info
4233
        $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
4234
4235
        // Getting question list
4236
        $question_list = [];
4237
        $studentInfo = [];
4238
        if (!empty($exercise_stat_info['data_tracking'])) {
4239
            $studentInfo = api_get_user_info($exercise_stat_info['exe_user_id']);
4240
            $question_list = explode(',', $exercise_stat_info['data_tracking']);
4241
        } else {
4242
            // Try getting the question list only if save result is off
4243
            if (false == $save_user_result) {
4244
                $question_list = $objExercise->get_validated_question_list();
4245
            }
4246
            if (in_array(
4247
                $objExercise->getFeedbackType(),
4248
                [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4249
            )) {
4250
                $question_list = $objExercise->get_validated_question_list();
4251
            }
4252
        }
4253
4254
        if ($objExercise->getResultAccess()) {
4255
            if (false === $objExercise->hasResultsAccess($exercise_stat_info)) {
4256
                echo Display::return_message(
4257
                    sprintf(get_lang('YouPassedTheLimitOfXMinutesToSeeTheResults'), $objExercise->getResultsAccess())
4258
                );
4259
4260
                return false;
4261
            }
4262
4263
            if (!empty($objExercise->getResultAccess())) {
4264
                $url = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$objExercise->id;
4265
                echo $objExercise->returnTimeLeftDiv();
4266
                echo $objExercise->showSimpleTimeControl(
4267
                    $objExercise->getResultAccessTimeDiff($exercise_stat_info),
4268
                    $url
4269
                );
4270
            }
4271
        }
4272
4273
        $counter = 1;
4274
        $total_score = $total_weight = 0;
4275
        $exerciseContent = null;
4276
4277
        // Hide results
4278
        $show_results = false;
4279
        $show_only_score = false;
4280
        if (in_array($objExercise->results_disabled,
4281
            [
4282
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4283
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
4284
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4285
            ]
4286
        )) {
4287
            $show_results = true;
4288
        }
4289
4290
        if (in_array(
4291
            $objExercise->results_disabled,
4292
            [
4293
                RESULT_DISABLE_SHOW_SCORE_ONLY,
4294
                RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES,
4295
                RESULT_DISABLE_RANKING,
4296
            ]
4297
        )
4298
        ) {
4299
            $show_only_score = true;
4300
        }
4301
4302
        // Not display expected answer, but score, and feedback
4303
        $show_all_but_expected_answer = false;
4304
        if (RESULT_DISABLE_SHOW_SCORE_ONLY == $objExercise->results_disabled &&
4305
            EXERCISE_FEEDBACK_TYPE_END == $objExercise->getFeedbackType()
4306
        ) {
4307
            $show_all_but_expected_answer = true;
4308
            $show_results = true;
4309
            $show_only_score = false;
4310
        }
4311
4312
        $showTotalScoreAndUserChoicesInLastAttempt = true;
4313
        $showTotalScore = true;
4314
        $showQuestionScore = true;
4315
        $attemptResult = [];
4316
4317
        if (in_array(
4318
            $objExercise->results_disabled,
4319
            [
4320
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
4321
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
4322
                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
4323
            ])
4324
        ) {
4325
            $show_only_score = true;
4326
            $show_results = true;
4327
            $numberAttempts = 0;
4328
            if ($objExercise->attempts > 0) {
4329
                $attempts = Event::getExerciseResultsByUser(
4330
                    api_get_user_id(),
4331
                    $objExercise->id,
4332
                    $courseId,
4333
                    $sessionId,
4334
                    $exercise_stat_info['orig_lp_id'],
4335
                    $exercise_stat_info['orig_lp_item_id'],
4336
                    'desc'
4337
                );
4338
                if ($attempts) {
4339
                    $numberAttempts = count($attempts);
4340
                }
4341
4342
                if ($save_user_result) {
4343
                    $numberAttempts++;
4344
                }
4345
4346
                $showTotalScore = false;
4347
                if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT == $objExercise->results_disabled) {
4348
                    $showTotalScore = true;
4349
                }
4350
                $showTotalScoreAndUserChoicesInLastAttempt = false;
4351
                if ($numberAttempts >= $objExercise->attempts) {
4352
                    $showTotalScore = true;
4353
                    $show_results = true;
4354
                    $show_only_score = false;
4355
                    $showTotalScoreAndUserChoicesInLastAttempt = true;
4356
                }
4357
4358
                if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $objExercise->results_disabled) {
4359
                    $showTotalScore = true;
4360
                    $show_results = true;
4361
                    $show_only_score = false;
4362
                    $showTotalScoreAndUserChoicesInLastAttempt = false;
4363
                    if ($numberAttempts >= $objExercise->attempts) {
4364
                        $showTotalScoreAndUserChoicesInLastAttempt = true;
4365
                    }
4366
4367
                    // Check if the current attempt is the last.
4368
                    if (false === $save_user_result && !empty($attempts)) {
4369
                        $showTotalScoreAndUserChoicesInLastAttempt = false;
4370
                        $position = 1;
4371
                        foreach ($attempts as $attempt) {
4372
                            if ($exeId == $attempt['exe_id']) {
4373
                                break;
4374
                            }
4375
                            $position++;
4376
                        }
4377
4378
                        if ($position == $objExercise->attempts) {
4379
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
4380
                        }
4381
                    }
4382
                }
4383
            }
4384
4385
            if (RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK ==
4386
                $objExercise->results_disabled
4387
            ) {
4388
                $show_only_score = false;
4389
                $show_results = true;
4390
                $show_all_but_expected_answer = false;
4391
                $showTotalScore = false;
4392
                $showQuestionScore = false;
4393
                if ($numberAttempts >= $objExercise->attempts) {
4394
                    $showTotalScore = true;
4395
                    $showQuestionScore = true;
4396
                }
4397
            }
4398
        }
4399
4400
        // When exporting to PDF hide feedback/comment/score show warning in hotspot.
4401
        if ($allowExportPdf && $isExport) {
4402
            $showTotalScore = false;
4403
            $showQuestionScore = false;
4404
            $objExercise->feedback_type = 2;
4405
            $objExercise->hideComment = true;
4406
            $objExercise->hideNoAnswer = true;
4407
            $objExercise->results_disabled = 0;
4408
            $objExercise->hideExpectedAnswer = true;
4409
            $show_results = true;
4410
        }
4411
4412
        if ('embeddable' !== $origin &&
4413
            !empty($exercise_stat_info['exe_user_id']) &&
4414
            !empty($studentInfo)
4415
        ) {
4416
            // Shows exercise header.
4417
            echo $objExercise->showExerciseResultHeader(
4418
                $studentInfo,
4419
                $exercise_stat_info,
4420
                $save_user_result,
4421
                $allowSignature,
4422
                $allowExportPdf
4423
            );
4424
        }
4425
4426
        $question_list_answers = [];
4427
        $category_list = [];
4428
        $loadChoiceFromSession = false;
4429
        $fromDatabase = true;
4430
        $exerciseResult = null;
4431
        $exerciseResultCoordinates = null;
4432
        $delineationResults = null;
4433
        if (true === $save_user_result && in_array(
4434
            $objExercise->getFeedbackType(),
4435
            [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4436
        )) {
4437
            $loadChoiceFromSession = true;
4438
            $fromDatabase = false;
4439
            $exerciseResult = Session::read('exerciseResult');
4440
            $exerciseResultCoordinates = Session::read('exerciseResultCoordinates');
4441
            $delineationResults = Session::read('hotspot_delineation_result');
4442
            $delineationResults = isset($delineationResults[$objExercise->id]) ? $delineationResults[$objExercise->id] : null;
4443
        }
4444
4445
        $countPendingQuestions = 0;
4446
        $result = [];
4447
        // Loop over all question to show results for each of them, one by one
4448
        if (!empty($question_list)) {
4449
            foreach ($question_list as $questionId) {
4450
                // Creates a temporary Question object
4451
                $objQuestionTmp = Question::read($questionId, $objExercise->course);
4452
                // This variable came from exercise_submit_modal.php
4453
                ob_start();
4454
                $choice = null;
4455
                $delineationChoice = null;
4456
                if ($loadChoiceFromSession) {
4457
                    $choice = isset($exerciseResult[$questionId]) ? $exerciseResult[$questionId] : null;
4458
                    $delineationChoice = isset($delineationResults[$questionId]) ? $delineationResults[$questionId] : null;
4459
                }
4460
4461
                // We're inside *one* question. Go through each possible answer for this question
4462
                $result = $objExercise->manage_answer(
4463
                    $exeId,
4464
                    $questionId,
4465
                    $choice,
4466
                    'exercise_result',
4467
                    $exerciseResultCoordinates,
4468
                    $save_user_result,
4469
                    $fromDatabase,
4470
                    $show_results,
4471
                    $objExercise->selectPropagateNeg(),
4472
                    $delineationChoice,
4473
                    $showTotalScoreAndUserChoicesInLastAttempt
4474
                );
4475
4476
                if (empty($result)) {
4477
                    continue;
4478
                }
4479
4480
                $total_score += $result['score'];
4481
                $total_weight += $result['weight'];
4482
4483
                $question_list_answers[] = [
4484
                    'question' => $result['open_question'],
4485
                    'answer' => $result['open_answer'],
4486
                    'answer_type' => $result['answer_type'],
4487
                    'generated_oral_file' => $result['generated_oral_file'],
4488
                ];
4489
4490
                $my_total_score = $result['score'];
4491
                $my_total_weight = $result['weight'];
4492
                $scorePassed = self::scorePassed($my_total_score, $my_total_weight);
4493
4494
                // Category report
4495
                $category_was_added_for_this_test = false;
4496
                if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
4497
                    if (!isset($category_list[$objQuestionTmp->category]['score'])) {
4498
                        $category_list[$objQuestionTmp->category]['score'] = 0;
4499
                    }
4500
                    if (!isset($category_list[$objQuestionTmp->category]['total'])) {
4501
                        $category_list[$objQuestionTmp->category]['total'] = 0;
4502
                    }
4503
                    if (!isset($category_list[$objQuestionTmp->category]['total_questions'])) {
4504
                        $category_list[$objQuestionTmp->category]['total_questions'] = 0;
4505
                    }
4506
                    if (!isset($category_list[$objQuestionTmp->category]['passed'])) {
4507
                        $category_list[$objQuestionTmp->category]['passed'] = 0;
4508
                    }
4509
                    if (!isset($category_list[$objQuestionTmp->category]['wrong'])) {
4510
                        $category_list[$objQuestionTmp->category]['wrong'] = 0;
4511
                    }
4512
                    if (!isset($category_list[$objQuestionTmp->category]['no_answer'])) {
4513
                        $category_list[$objQuestionTmp->category]['no_answer'] = 0;
4514
                    }
4515
4516
                    $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
4517
                    $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
4518
                    if ($scorePassed) {
4519
                        // Only count passed if score is not empty
4520
                        if (!empty($my_total_score)) {
4521
                            $category_list[$objQuestionTmp->category]['passed']++;
4522
                        }
4523
                    } else {
4524
                        if ($result['user_answered']) {
4525
                            $category_list[$objQuestionTmp->category]['wrong']++;
4526
                        } else {
4527
                            $category_list[$objQuestionTmp->category]['no_answer']++;
4528
                        }
4529
                    }
4530
4531
                    $category_list[$objQuestionTmp->category]['total_questions']++;
4532
                    $category_was_added_for_this_test = true;
4533
                }
4534
                if (isset($objQuestionTmp->category_list) && !empty($objQuestionTmp->category_list)) {
4535
                    foreach ($objQuestionTmp->category_list as $category_id) {
4536
                        $category_list[$category_id]['score'] += $my_total_score;
4537
                        $category_list[$category_id]['total'] += $my_total_weight;
4538
                        $category_was_added_for_this_test = true;
4539
                    }
4540
                }
4541
4542
                // No category for this question!
4543
                if (false == $category_was_added_for_this_test) {
4544
                    if (!isset($category_list['none']['score'])) {
4545
                        $category_list['none']['score'] = 0;
4546
                    }
4547
                    if (!isset($category_list['none']['total'])) {
4548
                        $category_list['none']['total'] = 0;
4549
                    }
4550
4551
                    $category_list['none']['score'] += $my_total_score;
4552
                    $category_list['none']['total'] += $my_total_weight;
4553
                }
4554
4555
                if (0 == $objExercise->selectPropagateNeg() && $my_total_score < 0) {
4556
                    $my_total_score = 0;
4557
                }
4558
4559
                $comnt = null;
4560
                if ($show_results) {
4561
                    $comnt = Event::get_comments($exeId, $questionId);
4562
                    $teacherAudio = self::getOralFeedbackAudio(
4563
                        $exeId,
4564
                        $questionId
4565
                    );
4566
4567
                    if (!empty($comnt) || $teacherAudio) {
4568
                        echo '<b>'.get_lang('Feedback').'</b>';
4569
                    }
4570
4571
                    if (!empty($comnt)) {
4572
                        echo self::getFeedbackText($comnt);
4573
                    }
4574
4575
                    if ($teacherAudio) {
4576
                        echo $teacherAudio;
4577
                    }
4578
                }
4579
4580
                $calculatedScore = [
4581
                    'result' => self::show_score(
4582
                        $my_total_score,
4583
                        $my_total_weight,
4584
                        false
4585
                    ),
4586
                    'pass' => $scorePassed,
4587
                    'score' => $my_total_score,
4588
                    'weight' => $my_total_weight,
4589
                    'comments' => $comnt,
4590
                    'user_answered' => $result['user_answered'],
4591
                ];
4592
4593
                $score = [];
4594
                if ($show_results) {
4595
                    $score = $calculatedScore;
4596
                }
4597
                if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION])) {
4598
                    $reviewScore = [
4599
                        'score' => $my_total_score,
4600
                        'comments' => Event::get_comments($exeId, $questionId),
4601
                    ];
4602
                    $check = $objQuestionTmp->isQuestionWaitingReview($reviewScore);
4603
                    if (false === $check) {
4604
                        $countPendingQuestions++;
4605
                    }
4606
                }
4607
4608
                $contents = ob_get_clean();
4609
                $questionContent = '';
4610
                if ($show_results) {
4611
                    $questionContent = '<div class="question-answer-result">';
4612
                    if (false === $showQuestionScore) {
4613
                        $score = [];
4614
                    }
4615
4616
                    // Shows question title an description
4617
                    $questionContent .= $objQuestionTmp->return_header(
4618
                        $objExercise,
4619
                        $counter,
4620
                        $score
4621
                    );
4622
                }
4623
                $counter++;
4624
                $questionContent .= $contents;
4625
                if ($show_results) {
4626
                    $questionContent .= '</div>';
4627
                }
4628
4629
                $calculatedScore['question_content'] = $questionContent;
4630
                $attemptResult[] = $calculatedScore;
4631
4632
                if ($objExercise->showExpectedChoice()) {
4633
                    $exerciseContent .= Display::panel($questionContent);
4634
                } else {
4635
                    // $show_all_but_expected_answer should not happen at
4636
                    // the same time as $show_results
4637
                    if ($show_results && !$show_only_score) {
4638
                        $exerciseContent .= Display::panel($questionContent);
4639
                    }
4640
                }
4641
            }
4642
        }
4643
4644
        // Display text when test is finished #4074 and for LP #4227
4645
        $endOfMessage = $objExercise->getFinishText($total_score, $total_weight);
4646
        if (!empty($endOfMessage)) {
4647
            echo Display::div(
4648
                $endOfMessage,
4649
                ['id' => 'quiz_end_message']
4650
            );
4651
        }
4652
4653
        $totalScoreText = null;
4654
        $certificateBlock = '';
4655
        if (($show_results || $show_only_score) && $showTotalScore) {
4656
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4657
                echo '<h1 style="text-align : center; margin : 20px 0;">'.get_lang('Your results').'</h1><br />';
4658
            }
4659
            $totalScoreText .= '<div class="question_row_score">';
4660
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4661
                $totalScoreText .= self::getQuestionDiagnosisRibbon(
4662
                    $objExercise,
4663
                    $total_score,
4664
                    $total_weight,
4665
                    true
4666
                );
4667
            } else {
4668
                $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
4669
                if ('true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)) {
4670
                    $formula = $pluginEvaluation->getFormulaForExercise($objExercise->getId());
4671
4672
                    if (!empty($formula)) {
4673
                        $total_score = $pluginEvaluation->getResultWithFormula($exeId, $formula);
4674
                        $total_weight = $pluginEvaluation->getMaxScore();
4675
                    }
4676
                }
4677
4678
                $totalScoreText .= self::getTotalScoreRibbon(
4679
                    $objExercise,
4680
                    $total_score,
4681
                    $total_weight,
4682
                    true,
4683
                    $countPendingQuestions
4684
                );
4685
            }
4686
            $totalScoreText .= '</div>';
4687
4688
            if (!empty($studentInfo)) {
4689
                $certificateBlock = self::generateAndShowCertificateBlock(
4690
                    $total_score,
4691
                    $total_weight,
4692
                    $objExercise,
4693
                    $studentInfo['id'],
4694
                    $courseId,
4695
                    $sessionId
4696
                );
4697
            }
4698
        }
4699
4700
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4701
            $chartMultiAnswer = MultipleAnswerTrueFalseDegreeCertainty::displayStudentsChartResults(
4702
                $exeId,
4703
                $objExercise
4704
            );
4705
            echo $chartMultiAnswer;
4706
        }
4707
4708
        if (!empty($category_list) &&
4709
            ($show_results || $show_only_score || RESULT_DISABLE_RADAR == $objExercise->results_disabled)
4710
        ) {
4711
            // Adding total
4712
            $category_list['total'] = [
4713
                'score' => $total_score,
4714
                'total' => $total_weight,
4715
            ];
4716
            echo TestCategory::get_stats_table_by_attempt($objExercise, $category_list);
4717
        }
4718
4719
        if ($show_all_but_expected_answer) {
4720
            $exerciseContent .= Display::return_message(get_lang('Note: This test has been setup to hide the expected answers.'));
4721
        }
4722
4723
        // Remove audio auto play from questions on results page - refs BT#7939
4724
        $exerciseContent = preg_replace(
4725
            ['/autoplay[\=\".+\"]+/', '/autostart[\=\".+\"]+/'],
4726
            '',
4727
            $exerciseContent
4728
        );
4729
4730
        echo $totalScoreText;
4731
        echo $certificateBlock;
4732
4733
        // Ofaj change BT#11784
4734
        if (('true' === api_get_setting('exercise.quiz_show_description_on_results_page')) &&
4735
            !empty($objExercise->description)
4736
        ) {
4737
            echo Display::div($objExercise->description, ['class' => 'exercise_description']);
4738
        }
4739
4740
        echo $exerciseContent;
4741
        if (!$show_only_score) {
4742
            echo $totalScoreText;
4743
        }
4744
4745
        if ($save_user_result) {
4746
            // Tracking of results
4747
            if ($exercise_stat_info) {
4748
                $learnpath_id = $exercise_stat_info['orig_lp_id'];
4749
                $learnpath_item_id = $exercise_stat_info['orig_lp_item_id'];
4750
                $learnpath_item_view_id = $exercise_stat_info['orig_lp_item_view_id'];
4751
4752
                if (api_is_allowed_to_session_edit()) {
4753
                    Event::updateEventExercise(
4754
                        $exercise_stat_info['exe_id'],
4755
                        $objExercise->getId(),
4756
                        $total_score,
4757
                        $total_weight,
4758
                        $sessionId,
4759
                        $learnpath_id,
4760
                        $learnpath_item_id,
4761
                        $learnpath_item_view_id,
4762
                        $exercise_stat_info['exe_duration'],
4763
                        $question_list
4764
                    );
4765
4766
                    $allowStats = ('true' === api_get_setting('gradebook.allow_gradebook_stats'));
4767
                    if ($allowStats) {
4768
                        $objExercise->generateStats(
4769
                            $objExercise->getId(),
4770
                            api_get_course_info(),
4771
                            $sessionId
4772
                        );
4773
                    }
4774
                }
4775
            }
4776
4777
            // Send notification at the end
4778
            if (!api_is_allowed_to_edit(null, true) &&
4779
                !api_is_excluded_user_type()
4780
            ) {
4781
                $objExercise->send_mail_notification_for_exam(
4782
                    'end',
4783
                    $question_list_answers,
4784
                    $origin,
4785
                    $exeId,
4786
                    $total_score,
4787
                    $total_weight
4788
                );
4789
            }
4790
        }
4791
4792
        if (in_array(
4793
            $objExercise->selectResultsDisabled(),
4794
            [RESULT_DISABLE_RANKING, RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING]
4795
        )) {
4796
            echo Display::page_header(get_lang('Ranking'), null, 'h4');
4797
            echo self::displayResultsInRanking(
4798
                $objExercise,
4799
                api_get_user_id(),
4800
                $courseId,
4801
                $sessionId
4802
            );
4803
        }
4804
4805
        if (!empty($remainingMessage)) {
4806
            echo Display::return_message($remainingMessage, 'normal', false);
4807
        }
4808
4809
        $failedAnswersCount = 0;
4810
        $wrongQuestionHtml = '';
4811
        $all = '';
4812
        foreach ($attemptResult as $item) {
4813
            if (false === $item['pass']) {
4814
                $failedAnswersCount++;
4815
                $wrongQuestionHtml .= $item['question_content'].'<br />';
4816
            }
4817
            $all .= $item['question_content'].'<br />';
4818
        }
4819
4820
        $passed = self::isPassPercentageAttemptPassed(
4821
            $objExercise,
4822
            $total_score,
4823
            $total_weight
4824
        );
4825
4826
        $percentage = 0;
4827
        if (!empty($total_weight)) {
4828
            $percentage = ($total_score / $total_weight) * 100;
4829
        }
4830
4831
        return [
4832
            'category_list' => $category_list,
4833
            'attempts_result_list' => $attemptResult, // array of results
4834
            'exercise_passed' => $passed, // boolean
4835
            'total_answers_count' => count($attemptResult), // int
4836
            'failed_answers_count' => $failedAnswersCount, // int
4837
            'failed_answers_html' => $wrongQuestionHtml,
4838
            'all_answers_html' => $all,
4839
            'total_score' => $total_score,
4840
            'total_weight' => $total_weight,
4841
            'total_percentage' => $percentage,
4842
            'count_pending_questions' => $countPendingQuestions,
4843
        ];
4844
    }
4845
4846
    /**
4847
     * Display the ranking of results in a exercise.
4848
     *
4849
     * @param Exercise $exercise
4850
     * @param int      $currentUserId
4851
     * @param int      $courseId
4852
     * @param int      $sessionId
4853
     *
4854
     * @return string
4855
     */
4856
    public static function displayResultsInRanking($exercise, $currentUserId, $courseId, $sessionId = 0)
4857
    {
4858
        $exerciseId = $exercise->iId;
4859
        $data = self::exerciseResultsInRanking($exerciseId, $courseId, $sessionId);
4860
4861
        $table = new HTML_Table(['class' => 'table table-hover table-striped table-bordered']);
4862
        $table->setHeaderContents(0, 0, get_lang('Position'), ['class' => 'text-right']);
4863
        $table->setHeaderContents(0, 1, get_lang('Username'));
4864
        $table->setHeaderContents(0, 2, get_lang('Score'), ['class' => 'text-right']);
4865
        $table->setHeaderContents(0, 3, get_lang('Date'), ['class' => 'text-center']);
4866
4867
        foreach ($data as $r => $item) {
4868
            if (!isset($item[1])) {
4869
                continue;
4870
            }
4871
            $selected = $item[1]->getId() == $currentUserId;
4872
4873
            foreach ($item as $c => $value) {
4874
                $table->setCellContents($r + 1, $c, $value);
4875
4876
                $attrClass = '';
4877
4878
                if (in_array($c, [0, 2])) {
4879
                    $attrClass = 'text-right';
4880
                } elseif (3 == $c) {
4881
                    $attrClass = 'text-center';
4882
                }
4883
4884
                if ($selected) {
4885
                    $attrClass .= ' warning';
4886
                }
4887
4888
                $table->setCellAttributes($r + 1, $c, ['class' => $attrClass]);
4889
            }
4890
        }
4891
4892
        return $table->toHtml();
4893
    }
4894
4895
    /**
4896
     * Get the ranking for results in a exercise.
4897
     * Function used internally by ExerciseLib::displayResultsInRanking.
4898
     *
4899
     * @param int $exerciseId
4900
     * @param int $courseId
4901
     * @param int $sessionId
4902
     *
4903
     * @return array
4904
     */
4905
    public static function exerciseResultsInRanking($exerciseId, $courseId, $sessionId = 0)
4906
    {
4907
        $em = Database::getManager();
4908
4909
        $dql = 'SELECT DISTINCT u.id FROM ChamiloCoreBundle:TrackEExercise te JOIN te.user u WHERE te.quiz = :id AND te.course = :cId';
4910
        $dql .= api_get_session_condition($sessionId, true, false, 'te.session');
4911
4912
        $result = $em
4913
            ->createQuery($dql)
4914
            ->setParameters(['id' => $exerciseId, 'cId' => $courseId])
4915
            ->getScalarResult();
4916
4917
        $data = [];
4918
4919
        foreach ($result as $item) {
4920
            $attempt = self::get_best_attempt_by_user($item['id'], $exerciseId, $courseId, $sessionId);
4921
            if (!empty($attempt) && isset($attempt['score']) && isset($attempt['exe_date'])) {
4922
                $data[] = $attempt;
4923
            }
4924
        }
4925
4926
        if (empty($data)) {
4927
            return [];
4928
        }
4929
4930
        usort(
4931
            $data,
4932
            function ($a, $b) {
4933
                if ($a['score'] != $b['score']) {
4934
                    return $a['score'] > $b['score'] ? -1 : 1;
4935
                }
4936
4937
                if ($a['exe_date'] != $b['exe_date']) {
4938
                    return $a['exe_date'] < $b['exe_date'] ? -1 : 1;
4939
                }
4940
4941
                return 0;
4942
            }
4943
        );
4944
4945
        // flags to display the same position in case of tie
4946
        $lastScore = $data[0]['score'];
4947
        $position = 1;
4948
        $data = array_map(
4949
            function ($item) use (&$lastScore, &$position) {
4950
                if ($item['score'] < $lastScore) {
4951
                    $position++;
4952
                }
4953
4954
                $lastScore = $item['score'];
4955
4956
                return [
4957
                    $position,
4958
                    api_get_user_entity($item['exe_user_id']),
4959
                    self::show_score($item['score'], $item['max_score'], true, true, true),
4960
                    api_convert_and_format_date($item['exe_date'], DATE_TIME_FORMAT_SHORT),
4961
                ];
4962
            },
4963
            $data
4964
        );
4965
4966
        return $data;
4967
    }
4968
4969
    /**
4970
     * Get a special ribbon on top of "degree of certainty" questions (
4971
     * variation from getTotalScoreRibbon() for other question types).
4972
     *
4973
     * @param Exercise $objExercise
4974
     * @param float    $score
4975
     * @param float    $weight
4976
     * @param bool     $checkPassPercentage
4977
     *
4978
     * @return string
4979
     */
4980
    public static function getQuestionDiagnosisRibbon($objExercise, $score, $weight, $checkPassPercentage = false)
4981
    {
4982
        $displayChartDegree = true;
4983
        $ribbon = $displayChartDegree ? '<div class="ribbon">' : '';
4984
4985
        if ($checkPassPercentage) {
4986
            $passPercentage = $objExercise->selectPassPercentage();
4987
            $isSuccess = self::isSuccessExerciseResult($score, $weight, $passPercentage);
4988
            // Color the final test score if pass_percentage activated
4989
            $ribbonTotalSuccessOrError = '';
4990
            if (self::isPassPercentageEnabled($passPercentage)) {
4991
                if ($isSuccess) {
4992
                    $ribbonTotalSuccessOrError = ' ribbon-total-success';
4993
                } else {
4994
                    $ribbonTotalSuccessOrError = ' ribbon-total-error';
4995
                }
4996
            }
4997
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total '.$ribbonTotalSuccessOrError.'">' : '';
4998
        } else {
4999
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total">' : '';
5000
        }
5001
5002
        if ($displayChartDegree) {
5003
            $ribbon .= '<h3>'.get_lang('Score for the test').':&nbsp;';
5004
            $ribbon .= self::show_score($score, $weight, false, true);
5005
            $ribbon .= '</h3>';
5006
            $ribbon .= '</div>';
5007
        }
5008
5009
        if ($checkPassPercentage) {
5010
            $ribbon .= self::showSuccessMessage(
5011
                $score,
5012
                $weight,
5013
                $objExercise->selectPassPercentage()
5014
            );
5015
        }
5016
5017
        $ribbon .= $displayChartDegree ? '</div>' : '';
5018
5019
        return $ribbon;
5020
    }
5021
5022
    public static function isPassPercentageAttemptPassed($objExercise, $score, $weight)
5023
    {
5024
        $passPercentage = $objExercise->selectPassPercentage();
5025
5026
        return self::isSuccessExerciseResult($score, $weight, $passPercentage);
5027
    }
5028
5029
    /**
5030
     * @param float $score
5031
     * @param float $weight
5032
     * @param bool  $checkPassPercentage
5033
     * @param int   $countPendingQuestions
5034
     *
5035
     * @return string
5036
     */
5037
    public static function getTotalScoreRibbon(
5038
        Exercise $objExercise,
5039
        $score,
5040
        $weight,
5041
        $checkPassPercentage = false,
5042
        $countPendingQuestions = 0
5043
    ) {
5044
        $hide = (int) $objExercise->getPageConfigurationAttribute('hide_total_score');
5045
        if (1 === $hide) {
5046
            return '';
5047
        }
5048
5049
        $passPercentage = $objExercise->selectPassPercentage();
5050
        $ribbon = '<div class="title-score">';
5051
        if ($checkPassPercentage) {
5052
            $isSuccess = self::isSuccessExerciseResult(
5053
                $score,
5054
                $weight,
5055
                $passPercentage
5056
            );
5057
            // Color the final test score if pass_percentage activated
5058
            $class = '';
5059
            if (self::isPassPercentageEnabled($passPercentage)) {
5060
                if ($isSuccess) {
5061
                    $class = ' ribbon-total-success';
5062
                } else {
5063
                    $class = ' ribbon-total-error';
5064
                }
5065
            }
5066
            $ribbon .= '<div class="total '.$class.'">';
5067
        } else {
5068
            $ribbon .= '<div class="total">';
5069
        }
5070
        $ribbon .= '<h3>'.get_lang('Score for the test').':&nbsp;';
5071
        $ribbon .= self::show_score($score, $weight, false, true);
5072
        $ribbon .= '</h3>';
5073
        $ribbon .= '</div>';
5074
        if ($checkPassPercentage) {
5075
            $ribbon .= self::showSuccessMessage(
5076
                $score,
5077
                $weight,
5078
                $passPercentage
5079
            );
5080
        }
5081
        $ribbon .= '</div>';
5082
5083
        if (!empty($countPendingQuestions)) {
5084
            $ribbon .= '<br />';
5085
            $ribbon .= Display::return_message(
5086
                sprintf(
5087
                    get_lang('Temporary score: %s open question(s) not corrected yet.'),
5088
                    $countPendingQuestions
5089
                ),
5090
                'warning'
5091
            );
5092
        }
5093
5094
        return $ribbon;
5095
    }
5096
5097
    /**
5098
     * @param int $countLetter
5099
     *
5100
     * @return mixed
5101
     */
5102
    public static function detectInputAppropriateClass($countLetter)
5103
    {
5104
        $limits = [
5105
            0 => 'input-mini',
5106
            10 => 'input-mini',
5107
            15 => 'input-medium',
5108
            20 => 'input-xlarge',
5109
            40 => 'input-xlarge',
5110
            60 => 'input-xxlarge',
5111
            100 => 'input-xxlarge',
5112
            200 => 'input-xxlarge',
5113
        ];
5114
5115
        foreach ($limits as $size => $item) {
5116
            if ($countLetter <= $size) {
5117
                return $item;
5118
            }
5119
        }
5120
5121
        return $limits[0];
5122
    }
5123
5124
    /**
5125
     * @param int    $senderId
5126
     * @param array  $course_info
5127
     * @param string $test
5128
     * @param string $url
5129
     *
5130
     * @return string
5131
     */
5132
    public static function getEmailNotification($senderId, $course_info, $test, $url)
5133
    {
5134
        $teacher_info = api_get_user_info($senderId);
5135
        $fromName = api_get_person_name(
5136
            $teacher_info['firstname'],
5137
            $teacher_info['lastname'],
5138
            null,
5139
            PERSON_NAME_EMAIL_ADDRESS
5140
        );
5141
5142
        $params = [
5143
            'course_title' => Security::remove_XSS($course_info['name']),
5144
            'test_title' => Security::remove_XSS($test),
5145
            'url' => $url,
5146
            'teacher_name' => $fromName,
5147
        ];
5148
5149
        return Container::getTwig()->render(
5150
            '@ChamiloCore/Mailer/Exercise/result_alert_body.html.twig',
5151
            $params
5152
        );
5153
    }
5154
5155
    /**
5156
     * @return string
5157
     */
5158
    public static function getNotCorrectedYetText()
5159
    {
5160
        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');
5161
    }
5162
5163
    /**
5164
     * @param string $message
5165
     *
5166
     * @return string
5167
     */
5168
    public static function getFeedbackText($message)
5169
    {
5170
        return Display::return_message($message, 'warning', false);
5171
    }
5172
5173
    /**
5174
     * Get the recorder audio component for save a teacher audio feedback.
5175
     *
5176
     * @param int $attemptId
5177
     * @param int $questionId
5178
     *
5179
     * @return string
5180
     */
5181
    public static function getOralFeedbackForm($attemptId, $questionId)
5182
    {
5183
        $view = new Template('', false, false, false, false, false, false);
5184
        $view->assign('type', Asset::EXERCISE_FEEDBACK);
5185
        $view->assign('question_id', $questionId);
5186
        $view->assign('t_exercise_id', $attemptId);
5187
        $template = $view->get_template('exercise/oral_expression.html.twig');
5188
5189
        return $view->fetch($template);
5190
    }
5191
5192
    /**
5193
     * Retrieves the generated audio files for an oral question in an exercise attempt.
5194
     *
5195
     * @param int  $trackExerciseId The ID of the tracked exercise.
5196
     * @param int  $questionId      The ID of the question.
5197
     * @param bool $returnUrls      (Optional) If set to true, only the URLs of the audio files are returned. Default is false.
5198
     *
5199
     * @return array|string If $returnUrls is true, returns an array of URLs of the audio files. Otherwise, returns an HTML string with audio tags.
5200
     */
5201
    public static function getOralFileAudio(int $trackExerciseId, int $questionId, bool $returnUrls = false): array|string
5202
    {
5203
        /** @var TrackEExercise $trackExercise */
5204
        $trackExercise = Container::getTrackEExerciseRepository()->find($trackExerciseId);
5205
5206
        if (null === $trackExercise) {
5207
            return $returnUrls ? [] : '';
5208
        }
5209
5210
        $questionAttempt = $trackExercise->getAttemptByQuestionId($questionId);
5211
5212
        if (null === $questionAttempt) {
5213
            return $returnUrls ? [] : '';
5214
        }
5215
5216
        $basePath = rtrim(api_get_path(WEB_PATH), '/');
5217
        $assetRepo = Container::getAssetRepository();
5218
5219
        if ($returnUrls) {
5220
            $urls = [];
5221
            foreach ($questionAttempt->getAttemptFiles() as $attemptFile) {
5222
                $urls[] = $basePath.$assetRepo->getAssetUrl($attemptFile->getAsset());
5223
            }
5224
5225
            return $urls;
5226
        } else {
5227
            $html = '';
5228
            foreach ($questionAttempt->getAttemptFiles() as $attemptFile) {
5229
                $html .= Display::tag(
5230
                    'audio',
5231
                    '',
5232
                    [
5233
                        'src' => $basePath.$assetRepo->getAssetUrl($attemptFile->getAsset()),
5234
                        'controls' => '',
5235
                    ]
5236
                );
5237
            }
5238
5239
            return $html;
5240
        }
5241
    }
5242
5243
    /**
5244
     * Get the audio component for a teacher audio feedback.
5245
     */
5246
    public static function getOralFeedbackAudio(int $attemptId, int $questionId): string
5247
    {
5248
        /** @var TrackEExercise $tExercise */
5249
        $tExercise = Container::getTrackEExerciseRepository()->find($attemptId);
5250
5251
        if (null === $tExercise) {
5252
            return '';
5253
        }
5254
5255
        $qAttempt = $tExercise->getAttemptByQuestionId($questionId);
5256
5257
        if (null === $qAttempt) {
5258
            return '';
5259
        }
5260
5261
        $html = '';
5262
5263
        $assetRepo = Container::getAssetRepository();
5264
5265
        foreach ($qAttempt->getAttemptFeedbacks() as $attemptFeedback) {
5266
            $html .= Display::tag(
5267
                'audio',
5268
                '',
5269
                [
5270
                    'src' => $assetRepo->getAssetUrl($attemptFeedback->getAsset()),
5271
                    'controls' => '',
5272
                ]
5273
5274
            );
5275
        }
5276
5277
        return $html;
5278
    }
5279
5280
    public static function getNotificationSettings(): array
5281
    {
5282
        return [
5283
            2 => get_lang('Paranoid: E-mail teacher when a student starts an exercise'),
5284
            1 => get_lang('Aware: E-mail teacher when a student ends an exercise'), // default
5285
            3 => get_lang('Relaxed open: E-mail teacher when a student ends an exercise, only if an open question is answered'),
5286
            4 => get_lang('Relaxed audio: E-mail teacher when a student ends an exercise, only if an oral question is answered'),
5287
        ];
5288
    }
5289
5290
    /**
5291
     * Get the additional actions added in exercise_additional_teacher_modify_actions configuration.
5292
     *
5293
     * @param int $exerciseId
5294
     * @param int $iconSize
5295
     *
5296
     * @return string
5297
     */
5298
    public static function getAdditionalTeacherActions($exerciseId, $iconSize = ICON_SIZE_SMALL)
5299
    {
5300
        $additionalActions = api_get_setting('exercise.exercise_additional_teacher_modify_actions', true) ?: [];
5301
        $actions = [];
5302
5303
        if (is_array($additionalActions)) {
5304
            foreach ($additionalActions as $additionalAction) {
5305
                $actions[] = call_user_func(
5306
                    $additionalAction,
5307
                    $exerciseId,
5308
                    $iconSize
5309
                );
5310
            }
5311
        }
5312
5313
        return implode(PHP_EOL, $actions);
5314
    }
5315
5316
    /**
5317
     * @param int $userId
5318
     * @param int $courseId
5319
     * @param int $sessionId
5320
     *
5321
     * @return int
5322
     */
5323
    public static function countAnsweredQuestionsByUserAfterTime(DateTime $time, $userId, $courseId, $sessionId)
5324
    {
5325
        $em = Database::getManager();
5326
5327
        if (empty($sessionId)) {
5328
            $sessionId = null;
5329
        }
5330
5331
        $time = api_get_utc_datetime($time->format('Y-m-d H:i:s'), false, true);
5332
5333
        $result = $em
5334
            ->createQuery('
5335
                SELECT COUNT(ea) FROM ChamiloCoreBundle:TrackEAttempt ea
5336
                WHERE ea.userId = :user AND ea.cId = :course AND ea.sessionId = :session
5337
                    AND ea.tms > :time
5338
            ')
5339
            ->setParameters(['user' => $userId, 'course' => $courseId, 'session' => $sessionId, 'time' => $time])
5340
            ->getSingleScalarResult();
5341
5342
        return $result;
5343
    }
5344
5345
    /**
5346
     * @param int $userId
5347
     * @param int $numberOfQuestions
5348
     * @param int $courseId
5349
     * @param int $sessionId
5350
     *
5351
     * @throws \Doctrine\ORM\Query\QueryException
5352
     *
5353
     * @return bool
5354
     */
5355
    public static function isQuestionsLimitPerDayReached($userId, $numberOfQuestions, $courseId, $sessionId)
5356
    {
5357
        $questionsLimitPerDay = (int) api_get_course_setting('quiz_question_limit_per_day');
5358
5359
        if ($questionsLimitPerDay <= 0) {
5360
            return false;
5361
        }
5362
5363
        $midnightTime = ChamiloApi::getServerMidnightTime();
5364
5365
        $answeredQuestionsCount = self::countAnsweredQuestionsByUserAfterTime(
5366
            $midnightTime,
5367
            $userId,
5368
            $courseId,
5369
            $sessionId
5370
        );
5371
5372
        return ($answeredQuestionsCount + $numberOfQuestions) > $questionsLimitPerDay;
5373
    }
5374
5375
    /**
5376
     * Check if an exercise complies with the requirements to be embedded in the mobile app or a video.
5377
     * By making sure it is set on one question per page and it only contains unique-answer or multiple-answer questions
5378
     * or unique-answer image. And that the exam does not have immediate feedback.
5379
     *
5380
     * @return bool
5381
     */
5382
    public static function isQuizEmbeddable(CQuiz $exercise)
5383
    {
5384
        $em = Database::getManager();
5385
5386
        if (ONE_PER_PAGE != $exercise->getType() ||
5387
            in_array($exercise->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])
5388
        ) {
5389
            return false;
5390
        }
5391
5392
        $countAll = $em
5393
            ->createQuery('SELECT COUNT(qq)
5394
                FROM ChamiloCourseBundle:CQuizQuestion qq
5395
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5396
                   WITH qq.iid = qrq.question
5397
                WHERE qrq.quiz = :id'
5398
            )
5399
            ->setParameter('id', $exercise->getIid())
5400
            ->getSingleScalarResult();
5401
5402
        $countOfAllowed = $em
5403
            ->createQuery('SELECT COUNT(qq)
5404
                FROM ChamiloCourseBundle:CQuizQuestion qq
5405
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5406
                   WITH qq.iid = qrq.question
5407
                WHERE qrq.quiz = :id AND qq.type IN (:types)'
5408
            )
5409
            ->setParameters(
5410
                [
5411
                    'id' => $exercise->getIid(),
5412
                    'types' => [UNIQUE_ANSWER, MULTIPLE_ANSWER, UNIQUE_ANSWER_IMAGE],
5413
                ]
5414
            )
5415
            ->getSingleScalarResult();
5416
5417
        return $countAll === $countOfAllowed;
5418
    }
5419
5420
    /**
5421
     * Generate a certificate linked to current quiz and.
5422
     * Return the HTML block with links to download and view the certificate.
5423
     *
5424
     * @param float $totalScore
5425
     * @param float $totalWeight
5426
     * @param int   $studentId
5427
     * @param int   $courseId
5428
     * @param int   $sessionId
5429
     *
5430
     * @return string
5431
     */
5432
    public static function generateAndShowCertificateBlock(
5433
        $totalScore,
5434
        $totalWeight,
5435
        Exercise $objExercise,
5436
        $studentId,
5437
        $courseId,
5438
        $sessionId = 0
5439
    ) {
5440
        if (('true' !== api_get_setting('exercise.quiz_generate_certificate_ending')) ||
5441
            !self::isSuccessExerciseResult($totalScore, $totalWeight, $objExercise->selectPassPercentage())
5442
        ) {
5443
            return '';
5444
        }
5445
5446
        $repo = Container::getGradeBookCategoryRepository();
5447
        /** @var GradebookCategory $category */
5448
        $category = $repo->findOneBy(
5449
            ['course' => $courseId, 'session' => $sessionId]
5450
        );
5451
5452
        if (null === $category) {
5453
            return '';
5454
        }
5455
5456
        /*$category = Category::load(null, null, $courseCode, null, null, $sessionId, 'ORDER By id');
5457
        if (empty($category)) {
5458
            return '';
5459
        }*/
5460
        $categoryId = $category->getId();
5461
        /*$link = LinkFactory::load(
5462
            null,
5463
            null,
5464
            $objExercise->getId(),
5465
            null,
5466
            $courseCode,
5467
            $categoryId
5468
        );*/
5469
5470
        if (empty($category->getLinks()->count())) {
5471
            return '';
5472
        }
5473
5474
        $resourceDeletedMessage = Category::show_message_resource_delete($courseId);
5475
        if (!empty($resourceDeletedMessage) || api_is_allowed_to_edit() || api_is_excluded_user_type()) {
5476
            return '';
5477
        }
5478
5479
        $certificate = Category::generateUserCertificate($category, $studentId);
5480
        if (!is_array($certificate)) {
5481
            return '';
5482
        }
5483
5484
        return Category::getDownloadCertificateBlock($certificate);
5485
    }
5486
5487
    /**
5488
     * @param int $exerciseId
5489
     */
5490
    public static function getExerciseTitleById($exerciseId)
5491
    {
5492
        $em = Database::getManager();
5493
5494
        return $em
5495
            ->createQuery('SELECT cq.title
5496
                FROM ChamiloCourseBundle:CQuiz cq
5497
                WHERE cq.iid = :iid'
5498
            )
5499
            ->setParameter('iid', $exerciseId)
5500
            ->getSingleScalarResult();
5501
    }
5502
5503
    /**
5504
     * @param int $exeId      ID from track_e_exercises
5505
     * @param int $userId     User ID
5506
     * @param int $exerciseId Exercise ID
5507
     * @param int $courseId   Optional. Coure ID.
5508
     *
5509
     * @return TrackEExercise|null
5510
     */
5511
    public static function recalculateResult($exeId, $userId, $exerciseId, $courseId = 0)
5512
    {
5513
        if (empty($userId) || empty($exerciseId)) {
5514
            return null;
5515
        }
5516
5517
        $em = Database::getManager();
5518
        /** @var TrackEExercise $trackedExercise */
5519
        $trackedExercise = $em->getRepository(TrackEExercise::class)->find($exeId);
5520
5521
        if (empty($trackedExercise)) {
5522
            return null;
5523
        }
5524
5525
        if ($trackedExercise->getUser()->getId() != $userId ||
5526
            $trackedExercise->getQuiz()?->getIid() != $exerciseId
5527
        ) {
5528
            return null;
5529
        }
5530
5531
        $questionList = $trackedExercise->getDataTracking();
5532
5533
        if (empty($questionList)) {
5534
            return null;
5535
        }
5536
5537
        $questionList = explode(',', $questionList);
5538
5539
        $exercise = new Exercise($courseId);
5540
        $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : [];
5541
5542
        if (false === $exercise->read($exerciseId)) {
5543
            return null;
5544
        }
5545
5546
        $totalScore = 0;
5547
        $totalWeight = 0;
5548
5549
        $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
5550
5551
        $formula = 'true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)
5552
            ? $pluginEvaluation->getFormulaForExercise($exerciseId)
5553
            : 0;
5554
5555
        if (empty($formula)) {
5556
            foreach ($questionList as $questionId) {
5557
                $question = Question::read($questionId, $courseInfo);
5558
5559
                if (false === $question) {
5560
                    continue;
5561
                }
5562
5563
                $totalWeight += $question->selectWeighting();
5564
5565
                // We're inside *one* question. Go through each possible answer for this question
5566
                $result = $exercise->manage_answer(
5567
                    $exeId,
5568
                    $questionId,
5569
                    [],
5570
                    'exercise_result',
5571
                    [],
5572
                    false,
5573
                    true,
5574
                    false,
5575
                    $exercise->selectPropagateNeg(),
5576
                    [],
5577
                    [],
5578
                    true
5579
                );
5580
5581
                //  Adding the new score.
5582
                $totalScore += $result['score'];
5583
            }
5584
        } else {
5585
            $totalScore = $pluginEvaluation->getResultWithFormula($exeId, $formula);
5586
            $totalWeight = $pluginEvaluation->getMaxScore();
5587
        }
5588
5589
        $trackedExercise
5590
            ->setScore($totalScore)
5591
            ->setMaxScore($totalWeight);
5592
5593
        $em->persist($trackedExercise);
5594
        $em->flush();
5595
        $lpItemId = $trackedExercise->getOrigLpItemId();
5596
        $lpId = $trackedExercise->getOrigLpId();
5597
        $lpItemViewId = $trackedExercise->getOrigLpItemViewId();
5598
        if ($lpId && $lpItemId && $lpItemViewId) {
5599
            $lpItem = $em->getRepository(CLpItem::class)->find($lpItemId);
5600
            if ($lpItem && 'quiz' === $lpItem->getItemType()) {
5601
                $lpItemView = $em->getRepository(CLpItemView::class)->find($lpItemViewId);
5602
                if ($lpItemView) {
5603
                    $lpItemView->setScore($totalScore);
5604
                    $em->persist($lpItemView);
5605
                    $em->flush();
5606
                }
5607
            }
5608
        }
5609
5610
        return $trackedExercise;
5611
    }
5612
5613
    public static function getTotalQuestionAnswered($courseId, $exerciseId, $questionId, $onlyStudents = false): int
5614
    {
5615
        $courseId = (int) $courseId;
5616
        $exerciseId = (int) $exerciseId;
5617
        $questionId = (int) $questionId;
5618
5619
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5620
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5621
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
5622
        $courseUserJoin = "";
5623
        $studentsWhere = "";
5624
        if ($onlyStudents) {
5625
            $courseUserJoin = "
5626
            INNER JOIN $courseUser cu
5627
            ON cu.c_id = te.c_id AND cu.user_id = exe_user_id";
5628
            $studentsWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
5629
        }
5630
5631
        $sql = "SELECT count(distinct (te.exe_id)) total
5632
            FROM $attemptTable t
5633
            INNER JOIN $trackTable te
5634
            ON (t.exe_id = te.exe_id)
5635
            $courseUserJoin
5636
            WHERE
5637
                te.c_id = $courseId AND
5638
                exe_exo_id = $exerciseId AND
5639
                t.question_id = $questionId AND
5640
                te.status != 'incomplete'
5641
                $studentsWhere
5642
        ";
5643
        $queryTotal = Database::query($sql);
5644
        $totalRow = Database::fetch_assoc($queryTotal);
5645
        $total = 0;
5646
        if ($totalRow) {
5647
            $total = (int) $totalRow['total'];
5648
        }
5649
5650
        return $total;
5651
    }
5652
5653
    public static function getWrongQuestionResults($courseId, $exerciseId, $sessionId = 0, $limit = 10)
5654
    {
5655
        $courseId = (int) $courseId;
5656
        $exerciseId = (int) $exerciseId;
5657
        $limit = (int) $limit;
5658
5659
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
5660
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5661
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5662
5663
        $sessionCondition = '';
5664
        if (!empty($sessionId)) {
5665
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
5666
        }
5667
5668
        $sql = "SELECT q.question, question_id, count(q.iid) count
5669
                FROM $attemptTable t
5670
                INNER JOIN $questionTable q
5671
                ON (q.iid = t.question_id)
5672
                INNER JOIN $trackTable te
5673
                ON (t.exe_id = te.exe_id)
5674
                WHERE
5675
                    te.c_id = $courseId AND
5676
                    t.marks != q.ponderation AND
5677
                    exe_exo_id = $exerciseId AND
5678
                    status != 'incomplete'
5679
                    $sessionCondition
5680
                GROUP BY q.iid
5681
                ORDER BY count DESC
5682
                LIMIT $limit
5683
        ";
5684
5685
        $result = Database::query($sql);
5686
5687
        return Database::store_result($result, 'ASSOC');
5688
    }
5689
5690
    public static function getExerciseResultsCount($type, $courseId, $exerciseId, $sessionId = 0)
5691
    {
5692
        $courseId = (int) $courseId;
5693
        $exerciseId = (int) $exerciseId;
5694
5695
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5696
5697
        $sessionCondition = '';
5698
        if (!empty($sessionId)) {
5699
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
5700
        }
5701
5702
        $selectCount = 'count(DISTINCT te.exe_id)';
5703
        $scoreCondition = '';
5704
        switch ($type) {
5705
            case 'correct_student':
5706
                $selectCount = 'count(DISTINCT te.exe_user_id)';
5707
                $scoreCondition = ' AND score = max_score ';
5708
                break;
5709
            case 'wrong_student':
5710
                $selectCount = 'count(DISTINCT te.exe_user_id)';
5711
                $scoreCondition = ' AND score != max_score ';
5712
                break;
5713
            case 'correct':
5714
                $scoreCondition = ' AND score = max_score ';
5715
                break;
5716
            case 'wrong':
5717
                $scoreCondition = ' AND score != max_score ';
5718
                break;
5719
        }
5720
5721
        $sql = "SELECT $selectCount count
5722
                FROM $trackTable te
5723
                WHERE
5724
                    c_id = $courseId AND
5725
                    exe_exo_id = $exerciseId AND
5726
                    status != 'incomplete'
5727
                    $scoreCondition
5728
                    $sessionCondition
5729
        ";
5730
        $result = Database::query($sql);
5731
        $totalRow = Database::fetch_assoc($result);
5732
        $total = 0;
5733
        if ($totalRow) {
5734
            $total = (int) $totalRow['count'];
5735
        }
5736
5737
        return $total;
5738
    }
5739
5740
    public static function parseContent($content, $stats, Exercise $exercise, $trackInfo, $currentUserId = 0)
5741
    {
5742
        $wrongAnswersCount = $stats['failed_answers_count'];
5743
        $attemptDate = substr($trackInfo['exe_date'], 0, 10);
5744
        $exerciseId = $exercise->iId;
5745
        $resultsStudentUrl = api_get_path(WEB_CODE_PATH).
5746
            'exercise/result.php?id='.$exerciseId.'&'.api_get_cidreq();
5747
        $resultsTeacherUrl = api_get_path(WEB_CODE_PATH).
5748
            'exercise/exercise_show.php?action=edit&id='.$exerciseId.'&'.api_get_cidreq();
5749
5750
        $content = str_replace(
5751
            [
5752
                '((exercise_error_count))',
5753
                '((all_answers_html))',
5754
                '((all_answers_teacher_html))',
5755
                '((exercise_title))',
5756
                '((exercise_attempt_date))',
5757
                '((link_to_test_result_page_student))',
5758
                '((link_to_test_result_page_teacher))',
5759
            ],
5760
            [
5761
                $wrongAnswersCount,
5762
                $stats['all_answers_html'],
5763
                $stats['all_answers_teacher_html'],
5764
                $exercise->get_formated_title(),
5765
                $attemptDate,
5766
                $resultsStudentUrl,
5767
                $resultsTeacherUrl,
5768
            ],
5769
            $content
5770
        );
5771
5772
        $currentUserId = empty($currentUserId) ? api_get_user_id() : (int) $currentUserId;
5773
5774
        $content = AnnouncementManager::parseContent(
5775
            $currentUserId,
5776
            $content,
5777
            api_get_course_id(),
5778
            api_get_session_id()
5779
        );
5780
5781
        return $content;
5782
    }
5783
5784
    public static function sendNotification(
5785
        $currentUserId,
5786
        $objExercise,
5787
        $exercise_stat_info,
5788
        $courseInfo,
5789
        $attemptCountToSend,
5790
        $stats,
5791
        $statsTeacher
5792
    ) {
5793
        $notifications = api_get_configuration_value('exercise_finished_notification_settings');
5794
        if (empty($notifications)) {
5795
            return false;
5796
        }
5797
5798
        $studentId = $exercise_stat_info['exe_user_id'];
5799
        $exerciseExtraFieldValue = new ExtraFieldValue('exercise');
5800
        $wrongAnswersCount = $stats['failed_answers_count'];
5801
        $exercisePassed = $stats['exercise_passed'];
5802
        $countPendingQuestions = $stats['count_pending_questions'];
5803
        $stats['all_answers_teacher_html'] = $statsTeacher['all_answers_html'];
5804
5805
        // If there are no pending questions (Open questions).
5806
        if (0 === $countPendingQuestions) {
5807
            /*$extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5808
                $objExercise->iId,
5809
                'signature_mandatory'
5810
            );
5811
5812
            if ($extraFieldData && isset($extraFieldData['value']) && 1 === (int) $extraFieldData['value']) {
5813
                if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($objExercise)) {
5814
                    $signature = ExerciseSignaturePlugin::getSignature($studentId, $exercise_stat_info);
5815
                    if (false !== $signature) {
5816
                        //return false;
5817
                    }
5818
                }
5819
            }*/
5820
5821
            // Notifications.
5822
            $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5823
                $objExercise->iId,
5824
                'notifications'
5825
            );
5826
            $exerciseNotification = '';
5827
            if ($extraFieldData && isset($extraFieldData['value'])) {
5828
                $exerciseNotification = $extraFieldData['value'];
5829
            }
5830
5831
            $subject = sprintf(get_lang('WrongAttemptXInCourseX'), $attemptCountToSend, $courseInfo['title']);
5832
            if ($exercisePassed) {
5833
                $subject = sprintf(get_lang('ExerciseValidationInCourseX'), $courseInfo['title']);
5834
            }
5835
5836
            if ($exercisePassed) {
5837
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5838
                    $objExercise->iId,
5839
                    'MailSuccess'
5840
                );
5841
            } else {
5842
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5843
                    $objExercise->iId,
5844
                    'MailAttempt'.$attemptCountToSend
5845
                );
5846
            }
5847
5848
            // Blocking exercise.
5849
            $blockPercentageExtra = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5850
                $objExercise->iId,
5851
                'blocking_percentage'
5852
            );
5853
            $blockPercentage = false;
5854
            if ($blockPercentageExtra && isset($blockPercentageExtra['value']) && $blockPercentageExtra['value']) {
5855
                $blockPercentage = $blockPercentageExtra['value'];
5856
            }
5857
            if ($blockPercentage) {
5858
                $passBlock = $stats['total_percentage'] > $blockPercentage;
5859
                if (false === $passBlock) {
5860
                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5861
                        $objExercise->iId,
5862
                        'MailIsBlockByPercentage'
5863
                    );
5864
                }
5865
            }
5866
5867
            $extraFieldValueUser = new ExtraFieldValue('user');
5868
5869
            if ($extraFieldData && isset($extraFieldData['value'])) {
5870
                $content = $extraFieldData['value'];
5871
                $content = self::parseContent($content, $stats, $objExercise, $exercise_stat_info, $studentId);
5872
                //if (false === $exercisePassed) {
5873
                if (0 !== $wrongAnswersCount) {
5874
                    $content .= $stats['failed_answers_html'];
5875
                }
5876
5877
                $sendMessage = true;
5878
                if (!empty($exerciseNotification)) {
5879
                    foreach ($notifications as $name => $notificationList) {
5880
                        if ($exerciseNotification !== $name) {
5881
                            continue;
5882
                        }
5883
                        foreach ($notificationList as $notificationName => $attemptData) {
5884
                            if ('student_check' === $notificationName) {
5885
                                $sendMsgIfInList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : '';
5886
                                if (!empty($sendMsgIfInList)) {
5887
                                    foreach ($sendMsgIfInList as $skipVariable => $skipValues) {
5888
                                        $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
5889
                                            $studentId,
5890
                                            $skipVariable
5891
                                        );
5892
5893
                                        if (empty($userExtraFieldValue)) {
5894
                                            $sendMessage = false;
5895
                                            break;
5896
                                        } else {
5897
                                            $sendMessage = false;
5898
                                            if (isset($userExtraFieldValue['value']) &&
5899
                                                in_array($userExtraFieldValue['value'], $skipValues)
5900
                                            ) {
5901
                                                $sendMessage = true;
5902
                                                break;
5903
                                            }
5904
                                        }
5905
                                    }
5906
                                }
5907
                                break;
5908
                            }
5909
                        }
5910
                    }
5911
                }
5912
5913
                // Send to student.
5914
                if ($sendMessage) {
5915
                    MessageManager::send_message($currentUserId, $subject, $content);
5916
                }
5917
            }
5918
5919
            if (!empty($exerciseNotification)) {
5920
                foreach ($notifications as $name => $notificationList) {
5921
                    if ($exerciseNotification !== $name) {
5922
                        continue;
5923
                    }
5924
                    foreach ($notificationList as $attemptData) {
5925
                        $skipNotification = false;
5926
                        $skipNotificationList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : [];
5927
                        if (!empty($skipNotificationList)) {
5928
                            foreach ($skipNotificationList as $skipVariable => $skipValues) {
5929
                                $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
5930
                                    $studentId,
5931
                                    $skipVariable
5932
                                );
5933
5934
                                if (empty($userExtraFieldValue)) {
5935
                                    $skipNotification = true;
5936
                                    break;
5937
                                } else {
5938
                                    if (isset($userExtraFieldValue['value'])) {
5939
                                        if (!in_array($userExtraFieldValue['value'], $skipValues)) {
5940
                                            $skipNotification = true;
5941
                                            break;
5942
                                        }
5943
                                    } else {
5944
                                        $skipNotification = true;
5945
                                        break;
5946
                                    }
5947
                                }
5948
                            }
5949
                        }
5950
5951
                        if ($skipNotification) {
5952
                            continue;
5953
                        }
5954
5955
                        $email = isset($attemptData['email']) ? $attemptData['email'] : '';
5956
                        $emailList = explode(',', $email);
5957
                        if (empty($emailList)) {
5958
                            continue;
5959
                        }
5960
                        $attempts = isset($attemptData['attempts']) ? $attemptData['attempts'] : [];
5961
                        foreach ($attempts as $attempt) {
5962
                            $sendMessage = false;
5963
                            if (isset($attempt['attempt']) && $attemptCountToSend !== (int) $attempt['attempt']) {
5964
                                continue;
5965
                            }
5966
5967
                            if (!isset($attempt['status'])) {
5968
                                continue;
5969
                            }
5970
5971
                            if ($blockPercentage && isset($attempt['is_block_by_percentage'])) {
5972
                                if ($attempt['is_block_by_percentage']) {
5973
                                    if ($passBlock) {
5974
                                        continue;
5975
                                    }
5976
                                } else {
5977
                                    if (false === $passBlock) {
5978
                                        continue;
5979
                                    }
5980
                                }
5981
                            }
5982
5983
                            switch ($attempt['status']) {
5984
                                case 'passed':
5985
                                    if ($exercisePassed) {
5986
                                        $sendMessage = true;
5987
                                    }
5988
                                    break;
5989
                                case 'failed':
5990
                                    if (false === $exercisePassed) {
5991
                                        $sendMessage = true;
5992
                                    }
5993
                                    break;
5994
                                case 'all':
5995
                                    $sendMessage = true;
5996
                                    break;
5997
                            }
5998
5999
                            if ($sendMessage) {
6000
                                $attachments = [];
6001
                                if (isset($attempt['add_pdf']) && $attempt['add_pdf']) {
6002
                                    // Get pdf content
6003
                                    $pdfExtraData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6004
                                        $objExercise->iId,
6005
                                        $attempt['add_pdf']
6006
                                    );
6007
6008
                                    if ($pdfExtraData && isset($pdfExtraData['value'])) {
6009
                                        $pdfContent = self::parseContent(
6010
                                            $pdfExtraData['value'],
6011
                                            $stats,
6012
                                            $objExercise,
6013
                                            $exercise_stat_info,
6014
                                            $studentId
6015
                                        );
6016
6017
                                        @$pdf = new PDF();
6018
                                        $filename = get_lang('Exercise');
6019
                                        $pdfPath = @$pdf->content_to_pdf(
6020
                                            "<html><body>$pdfContent</body></html>",
6021
                                            null,
6022
                                            $filename,
6023
                                            api_get_course_id(),
6024
                                            'F',
6025
                                            false,
6026
                                            null,
6027
                                            false,
6028
                                            true
6029
                                        );
6030
                                        $attachments[] = ['filename' => $filename, 'path' => $pdfPath];
6031
                                    }
6032
                                }
6033
6034
                                $content = isset($attempt['content_default']) ? $attempt['content_default'] : '';
6035
                                if (isset($attempt['content'])) {
6036
                                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6037
                                        $objExercise->iId,
6038
                                        $attempt['content']
6039
                                    );
6040
                                    if ($extraFieldData && isset($extraFieldData['value']) && !empty($extraFieldData['value'])) {
6041
                                        $content = $extraFieldData['value'];
6042
                                    }
6043
                                }
6044
6045
                                if (!empty($content)) {
6046
                                    $content = self::parseContent(
6047
                                        $content,
6048
                                        $stats,
6049
                                        $objExercise,
6050
                                        $exercise_stat_info,
6051
                                        $studentId
6052
                                    );
6053
                                    foreach ($emailList as $email) {
6054
                                        if (empty($email)) {
6055
                                            continue;
6056
                                        }
6057
                                        api_mail_html(
6058
                                            null,
6059
                                            $email,
6060
                                            $subject,
6061
                                            $content,
6062
                                            null,
6063
                                            null,
6064
                                            [],
6065
                                            $attachments
6066
                                        );
6067
                                    }
6068
                                }
6069
6070
                                if (isset($attempt['post_actions'])) {
6071
                                    foreach ($attempt['post_actions'] as $action => $params) {
6072
                                        switch ($action) {
6073
                                            case 'subscribe_student_to_courses':
6074
                                                foreach ($params as $code) {
6075
                                                    $courseInfo = api_get_course_info($code);
6076
                                                    CourseManager::subscribeUser(
6077
                                                        $currentUserId,
6078
                                                        $courseInfo['real_id']
6079
                                                    );
6080
                                                    break;
6081
                                                }
6082
                                                break;
6083
                                        }
6084
                                    }
6085
                                }
6086
                            }
6087
                        }
6088
                    }
6089
                }
6090
            }
6091
        }
6092
    }
6093
6094
    /**
6095
     * Delete an exercise attempt.
6096
     *
6097
     * Log the exe_id deleted with the exe_user_id related.
6098
     *
6099
     * @param int $exeId
6100
     */
6101
    public static function deleteExerciseAttempt($exeId)
6102
    {
6103
        $exeId = (int) $exeId;
6104
6105
        $trackExerciseInfo = self::get_exercise_track_exercise_info($exeId);
6106
6107
        if (empty($trackExerciseInfo)) {
6108
            return;
6109
        }
6110
6111
        $tblTrackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6112
        $tblTrackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
6113
6114
        Database::query("DELETE FROM $tblTrackAttempt WHERE exe_id = $exeId");
6115
        Database::query("DELETE FROM $tblTrackExercises WHERE exe_id = $exeId");
6116
6117
        Event::addEvent(
6118
            LOG_EXERCISE_ATTEMPT_DELETE,
6119
            LOG_EXERCISE_ATTEMPT,
6120
            $exeId,
6121
            api_get_utc_datetime()
6122
        );
6123
        Event::addEvent(
6124
            LOG_EXERCISE_ATTEMPT_DELETE,
6125
            LOG_EXERCISE_AND_USER_ID,
6126
            $exeId.'-'.$trackExerciseInfo['exe_user_id'],
6127
            api_get_utc_datetime()
6128
        );
6129
    }
6130
6131
    public static function scorePassed($score, $total)
6132
    {
6133
        $compareResult = bccomp($score, $total, 3);
6134
        $scorePassed = 1 === $compareResult || 0 === $compareResult;
6135
        if (false === $scorePassed) {
6136
            $epsilon = 0.00001;
6137
            if (abs($score - $total) < $epsilon) {
6138
                $scorePassed = true;
6139
            }
6140
        }
6141
6142
        return $scorePassed;
6143
    }
6144
6145
    /**
6146
     * Export all results of *one* exercise to a ZIP file containing individual PDFs.
6147
     *
6148
     * @return false|void
6149
     * @throws Exception
6150
     */
6151
    public static function exportExerciseAllResultsZip(
6152
        int $sessionId,
6153
        int $courseId,
6154
        int $exerciseId,
6155
        array $filterDates = [],
6156
        string $mainPath = ''
6157
    ) {
6158
        $objExerciseTmp = new Exercise($courseId);
6159
        $exeResults = $objExerciseTmp->getExerciseAndResult(
6160
            $courseId,
6161
            $sessionId,
6162
            $exerciseId
6163
        );
6164
6165
        $exportOk = false;
6166
        if (!empty($exeResults)) {
6167
            $exportName = 'S'.$sessionId.'-C'.$courseId.'-T'.$exerciseId;
6168
            $baseDir = api_get_path(SYS_ARCHIVE_PATH);
6169
            $folderName = 'pdfexport-'.$exportName;
6170
            $exportFolderPath = $baseDir.$folderName;
6171
6172
            // 1. Cleans the export folder if it exists.
6173
            if (is_dir($exportFolderPath)) {
6174
                rmdirr($exportFolderPath);
6175
            }
6176
6177
            // 2. Create the pdfs inside a new export folder path.
6178
            foreach ($exeResults as $exeResult) {
6179
                $exeId = (int) $exeResult['exe_id'];
6180
                self::saveFileExerciseResultPdf($exeId, $courseId, $sessionId);
6181
            }
6182
6183
            // 3. If export folder is not empty will be zipped.
6184
            $isFolderPathEmpty = (file_exists($exportFolderPath) && 2 == count(scandir($exportFolderPath)));
6185
            if (is_dir($exportFolderPath) && !$isFolderPathEmpty) {
6186
                $exportOk = true;
6187
                $exportFilePath = $baseDir.$exportName.'.zip';
6188
                $zip = new \ZipArchive();
6189
                if ($zip->open($exportFilePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) === true) {
6190
                    $files = new RecursiveIteratorIterator(
6191
                        new RecursiveDirectoryIterator($exportFolderPath),
6192
                        RecursiveIteratorIterator::LEAVES_ONLY
6193
                    );
6194
6195
                    foreach ($files as $name => $file) {
6196
                        if (!$file->isDir()) {
6197
                            $filePath = $file->getRealPath();
6198
                            $relativePath = substr($filePath, strlen($exportFolderPath) + 1);
6199
                            $zip->addFile($filePath, $relativePath);
6200
                        }
6201
                    }
6202
6203
                    $zip->close();
6204
                } else {
6205
                    throw new Exception('Failed to create ZIP file');
6206
                }
6207
6208
                rmdirr($exportFolderPath);
6209
6210
                if (!empty($mainPath) && file_exists($exportFilePath)) {
6211
                    @rename($exportFilePath, $mainPath.'/'.$exportName.'.zip');
6212
                } else {
6213
                    DocumentManager::file_send_for_download($exportFilePath, true, $exportName.'.zip');
6214
                    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...
6215
                }
6216
            }
6217
        }
6218
6219
        if (empty($mainPath) && !$exportOk) {
6220
            Display::addFlash(
6221
                Display::return_message(
6222
                    get_lang('ExportExerciseNoResult'),
6223
                    'warning',
6224
                    false
6225
                )
6226
            );
6227
        }
6228
6229
        return false;
6230
    }
6231
6232
    /**
6233
     * Generates and saves a PDF file for a specific exercise attempt result.
6234
     */
6235
    public static function saveFileExerciseResultPdf(
6236
        int $exeId,
6237
        int $courseId,
6238
        int $sessionId
6239
    ): void
6240
    {
6241
        $cidReq = 'cid='.$courseId.'&sid='.$sessionId.'&gid=0&gradebook=0';
6242
        $url = api_get_path(WEB_PATH).'main/exercise/exercise_show.php?'.$cidReq.'&id='.$exeId.'&action=export&export_type=all_results';
6243
        $ch = curl_init();
6244
        curl_setopt($ch, CURLOPT_URL, $url);
6245
        curl_setopt($ch, CURLOPT_COOKIE, session_id());
6246
        curl_setopt($ch, CURLOPT_AUTOREFERER, true);
6247
        curl_setopt($ch, CURLOPT_COOKIESESSION, true);
6248
        curl_setopt($ch, CURLOPT_FAILONERROR, false);
6249
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
6250
        curl_setopt($ch, CURLOPT_HEADER, true);
6251
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
6252
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
6253
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
6254
6255
        $result = curl_exec($ch);
6256
6257
        if (false === $result) {
6258
            error_log('saveFileExerciseResultPdf error: '.curl_error($ch));
6259
        }
6260
6261
        curl_close($ch);
6262
    }
6263
}
6264