Passed
Push — master ( a9fdac...188f98 )
by Yannick
07:01
created

ExerciseLib::getWrongQuestionResults()   A

Complexity

Conditions 2

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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