ExerciseLib::getNumberStudentsAnswerHotspotCount()   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 57
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 35
nop 5
dl 0
loc 57
rs 9.36
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

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

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