Passed
Pull Request — master (#6637)
by
unknown
08:00
created

ExerciseLib::isQuestionsLimitPerDayReached()   A

Complexity

Conditions 2

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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