Passed
Push — master ( 3fcb32...49d6fc )
by Julito
09:27
created

ExerciseLib::getTotalQuestionAnswered()   A

Complexity

Conditions 2

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
5199
        $sessionId = api_get_session_id();
5200
        $groupId = api_get_group_id();
5201
        $sysCourseDir = api_get_path(SYS_COURSE_PATH).$courseInfo['path'];
5202
        $webCourseDir = api_get_path(WEB_COURSE_PATH).$courseInfo['path'];
5203
        $fileName = "{$questionId}_{$userId}".DocumentManager::getDocumentSuffix($courseInfo, $sessionId, $groupId);
5204
        $filePath = null;
5205
5206
        $relFilePath = "/exercises/teacher_audio/$attemptId/$fileName";
5207
5208
        if (file_exists($sysCourseDir.$relFilePath.'.ogg')) {
5209
            $filePath = $webCourseDir.$relFilePath.'.ogg';
5210
        } elseif (file_exists($sysCourseDir.$relFilePath.'.wav.wav')) {
5211
            $filePath = $webCourseDir.$relFilePath.'.wav.wav';
5212
        } elseif (file_exists($sysCourseDir.$relFilePath.'.wav')) {
5213
            $filePath = $webCourseDir.$relFilePath.'.wav';
5214
        }
5215
5216
        if (!$filePath) {
5217
            return '';
5218
        }
5219
5220
        return Display::tag(
5221
            'audio',
5222
            null,
5223
            ['src' => $filePath]
5224
        );
5225
    }
5226
5227
    /**
5228
     * @return array
5229
     */
5230
    public static function getNotificationSettings()
5231
    {
5232
        $emailAlerts = [
5233
            2 => get_lang('SendEmailToTrainerWhenStudentStartQuiz'),
5234
            1 => get_lang('SendEmailToTrainerWhenStudentEndQuiz'), // default
5235
            3 => get_lang('SendEmailToTrainerWhenStudentEndQuizOnlyIfOpenQuestion'),
5236
            4 => get_lang('SendEmailToTrainerWhenStudentEndQuizOnlyIfOralQuestion'),
5237
        ];
5238
5239
        return $emailAlerts;
5240
    }
5241
5242
    /**
5243
     * Get the additional actions added in exercise_additional_teacher_modify_actions configuration.
5244
     *
5245
     * @param int $exerciseId
5246
     * @param int $iconSize
5247
     *
5248
     * @return string
5249
     */
5250
    public static function getAdditionalTeacherActions($exerciseId, $iconSize = ICON_SIZE_SMALL)
5251
    {
5252
        $additionalActions = api_get_configuration_value('exercise_additional_teacher_modify_actions') ?: [];
5253
        $actions = [];
5254
5255
        foreach ($additionalActions as $additionalAction) {
5256
            $actions[] = call_user_func(
5257
                $additionalAction,
5258
                $exerciseId,
5259
                $iconSize
5260
            );
5261
        }
5262
5263
        return implode(PHP_EOL, $actions);
5264
    }
5265
5266
    /**
5267
     * @param int $userId
5268
     * @param int $courseId
5269
     * @param int $sessionId
5270
     *
5271
     * @throws \Doctrine\ORM\Query\QueryException
5272
     *
5273
     * @return int
5274
     */
5275
    public static function countAnsweredQuestionsByUserAfterTime(DateTime $time, $userId, $courseId, $sessionId)
5276
    {
5277
        $em = Database::getManager();
5278
5279
        $time = api_get_utc_datetime($time->format('Y-m-d H:i:s'), false, true);
5280
5281
        $result = $em
5282
            ->createQuery('
5283
                SELECT COUNT(ea) FROM ChamiloCoreBundle:TrackEAttempt ea
5284
                WHERE ea.userId = :user AND ea.cId = :course AND ea.sessionId = :session
5285
                    AND ea.tms > :time
5286
            ')
5287
            ->setParameters(['user' => $userId, 'course' => $courseId, 'session' => $sessionId, 'time' => $time])
5288
            ->getSingleScalarResult();
5289
5290
        return $result;
5291
    }
5292
5293
    /**
5294
     * @param int $userId
5295
     * @param int $numberOfQuestions
5296
     * @param int $courseId
5297
     * @param int $sessionId
5298
     *
5299
     * @throws \Doctrine\ORM\Query\QueryException
5300
     *
5301
     * @return bool
5302
     */
5303
    public static function isQuestionsLimitPerDayReached($userId, $numberOfQuestions, $courseId, $sessionId)
5304
    {
5305
        $questionsLimitPerDay = (int) api_get_course_setting('quiz_question_limit_per_day');
5306
5307
        if ($questionsLimitPerDay <= 0) {
5308
            return false;
5309
        }
5310
5311
        $midnightTime = ChamiloApi::getServerMidnightTime();
5312
5313
        $answeredQuestionsCount = self::countAnsweredQuestionsByUserAfterTime(
5314
            $midnightTime,
5315
            $userId,
5316
            $courseId,
5317
            $sessionId
5318
        );
5319
5320
        return ($answeredQuestionsCount + $numberOfQuestions) > $questionsLimitPerDay;
5321
    }
5322
5323
    /**
5324
     * Check if an exercise complies with the requirements to be embedded in the mobile app or a video.
5325
     * By making sure it is set on one question per page and it only contains unique-answer or multiple-answer questions
5326
     * or unique-answer image. And that the exam does not have immediate feedback.
5327
     *
5328
     * @return bool
5329
     */
5330
    public static function isQuizEmbeddable(CQuiz $exercise)
5331
    {
5332
        $em = Database::getManager();
5333
5334
        if (ONE_PER_PAGE != $exercise->getType() ||
5335
            in_array($exercise->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])
5336
        ) {
5337
            return false;
5338
        }
5339
5340
        $countAll = $em
5341
            ->createQuery('SELECT COUNT(qq)
5342
                FROM ChamiloCourseBundle:CQuizQuestion qq
5343
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5344
                   WITH qq.iid = qrq.question
5345
                WHERE qrq.quiz = :id'
5346
            )
5347
            ->setParameter('id', $exercise->getIid())
5348
            ->getSingleScalarResult();
5349
5350
        $countOfAllowed = $em
5351
            ->createQuery('SELECT COUNT(qq)
5352
                FROM ChamiloCourseBundle:CQuizQuestion qq
5353
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5354
                   WITH qq.iid = qrq.question
5355
                WHERE qrq.quiz = :id AND qq.type IN (:types)'
5356
            )
5357
            ->setParameters(
5358
                [
5359
                    'id' => $exercise->getIid(),
5360
                    'types' => [UNIQUE_ANSWER, MULTIPLE_ANSWER, UNIQUE_ANSWER_IMAGE],
5361
                ]
5362
            )
5363
            ->getSingleScalarResult();
5364
5365
        return $countAll === $countOfAllowed;
5366
    }
5367
5368
    /**
5369
     * Generate a certificate linked to current quiz and.
5370
     * Return the HTML block with links to download and view the certificate.
5371
     *
5372
     * @param float  $totalScore
5373
     * @param float  $totalWeight
5374
     * @param int    $studentId
5375
     * @param string $courseCode
5376
     * @param int    $sessionId
5377
     *
5378
     * @return string
5379
     */
5380
    public static function generateAndShowCertificateBlock(
5381
        $totalScore,
5382
        $totalWeight,
5383
        Exercise $objExercise,
5384
        $studentId,
5385
        $courseCode,
5386
        $sessionId = 0
5387
    ) {
5388
        if (!api_get_configuration_value('quiz_generate_certificate_ending') ||
5389
            !self::isSuccessExerciseResult($totalScore, $totalWeight, $objExercise->selectPassPercentage())
5390
        ) {
5391
            return '';
5392
        }
5393
5394
        /** @var Category $category */
5395
        $category = Category::load(null, null, $courseCode, null, null, $sessionId, 'ORDER By id');
5396
5397
        if (empty($category)) {
5398
            return '';
5399
        }
5400
5401
        /** @var Category $category */
5402
        $category = $category[0];
5403
        $categoryId = $category->get_id();
5404
        $link = LinkFactory::load(
5405
            null,
5406
            null,
5407
            $objExercise->getId(),
5408
            null,
5409
            $courseCode,
5410
            $categoryId
5411
        );
5412
5413
        if (empty($link)) {
5414
            return '';
5415
        }
5416
5417
        $resourceDeletedMessage = $category->show_message_resource_delete($courseCode);
5418
5419
        if (false !== $resourceDeletedMessage || api_is_allowed_to_edit() || api_is_excluded_user_type()) {
5420
            return '';
5421
        }
5422
5423
        $certificate = Category::generateUserCertificate($categoryId, $studentId);
5424
5425
        if (!is_array($certificate)) {
5426
            return '';
5427
        }
5428
5429
        return Category::getDownloadCertificateBlock($certificate);
5430
    }
5431
5432
    /**
5433
     * @param int $exerciseId
5434
     */
5435
    public static function getExerciseTitleById($exerciseId)
5436
    {
5437
        $em = Database::getManager();
5438
5439
        return $em
5440
            ->createQuery('SELECT cq.title
5441
                FROM ChamiloCourseBundle:CQuiz cq
5442
                WHERE cq.iid = :iid'
5443
            )
5444
            ->setParameter('iid', $exerciseId)
5445
            ->getSingleScalarResult();
5446
    }
5447
5448
    /**
5449
     * @param int $exeId      ID from track_e_exercises
5450
     * @param int $userId     User ID
5451
     * @param int $exerciseId Exercise ID
5452
     * @param int $courseId   Optional. Coure ID.
5453
     *
5454
     * @return TrackEExercises|null
5455
     */
5456
    public static function recalculateResult($exeId, $userId, $exerciseId, $courseId = 0)
5457
    {
5458
        if (empty($userId) || empty($exerciseId)) {
5459
            return null;
5460
        }
5461
5462
        $em = Database::getManager();
5463
        /** @var TrackEExercises $trackedExercise */
5464
        $trackedExercise = $em->getRepository(TrackEExercises::class)->find($exeId);
5465
5466
        if (empty($trackedExercise)) {
5467
            return null;
5468
        }
5469
5470
        if ($trackedExercise->getExeUserId() != $userId ||
5471
            $trackedExercise->getExeExoId() != $exerciseId
5472
        ) {
5473
            return null;
5474
        }
5475
5476
        $questionList = $trackedExercise->getDataTracking();
5477
5478
        if (empty($questionList)) {
5479
            return null;
5480
        }
5481
5482
        $questionList = explode(',', $questionList);
5483
5484
        $exercise = new Exercise($courseId);
5485
        $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : [];
5486
5487
        if (false === $exercise->read($exerciseId)) {
5488
            return null;
5489
        }
5490
5491
        $totalScore = 0;
5492
        $totalWeight = 0;
5493
5494
        $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
5495
5496
        $formula = 'true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)
5497
            ? $pluginEvaluation->getFormulaForExercise($exerciseId)
5498
            : 0;
5499
5500
        if (empty($formula)) {
5501
            foreach ($questionList as $questionId) {
5502
                $question = Question::read($questionId, $courseInfo);
5503
5504
                if (false === $question) {
5505
                    continue;
5506
                }
5507
5508
                $totalWeight += $question->selectWeighting();
5509
5510
                // We're inside *one* question. Go through each possible answer for this question
5511
                $result = $exercise->manage_answer(
5512
                    $exeId,
5513
                    $questionId,
5514
                    [],
5515
                    'exercise_result',
5516
                    [],
5517
                    false,
5518
                    true,
5519
                    false,
5520
                    $exercise->selectPropagateNeg(),
5521
                    [],
5522
                    [],
5523
                    true
5524
                );
5525
5526
                //  Adding the new score.
5527
                $totalScore += $result['score'];
5528
            }
5529
        } else {
5530
            $totalScore = $pluginEvaluation->getResultWithFormula($exeId, $formula);
5531
            $totalWeight = $pluginEvaluation->getMaxScore();
5532
        }
5533
5534
        $trackedExercise
5535
            ->setScore($totalScore)
5536
            ->setMaxScore($totalWeight);
5537
5538
        $em->persist($trackedExercise);
5539
        $em->flush();
5540
5541
        return $trackedExercise;
5542
    }
5543
5544
    public static function getTotalQuestionAnswered($courseId, $exerciseId, $questionId)
5545
    {
5546
        $courseId = (int) $courseId;
5547
        $exerciseId = (int) $exerciseId;
5548
        $questionId = (int) $questionId;
5549
5550
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5551
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5552
5553
        $sql = "SELECT count(te.exe_id) total
5554
            FROM $attemptTable t
5555
            INNER JOIN $trackTable te
5556
            ON (te.c_id = t.c_id AND t.exe_id = te.exe_id)
5557
            WHERE
5558
                t.c_id = $courseId AND
5559
                exe_exo_id = $exerciseId AND
5560
                t.question_id = $questionId AND
5561
                status != 'incomplete'
5562
        ";
5563
        $queryTotal = Database::query($sql);
5564
        $totalRow = Database::fetch_array($queryTotal, 'ASSOC');
5565
        $total = 0;
5566
        if ($totalRow) {
5567
            $total = (int) $totalRow['total'];
5568
        }
5569
5570
        return $total;
5571
    }
5572
5573
    public static function getWrongQuestionResults($courseId, $exerciseId, $sessionId = 0, $limit = 10)
5574
    {
5575
        $courseId = (int) $courseId;
5576
        $exerciseId = (int) $exerciseId;
5577
        $limit = (int) $limit;
5578
5579
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
5580
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5581
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5582
5583
        $sessionCondition = '';
5584
        if (!empty($sessionId)) {
5585
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
5586
        }
5587
5588
        $sql = "SELECT q.question, question_id, count(q.iid) count
5589
                FROM $attemptTable t
5590
                INNER JOIN $questionTable q
5591
                ON (q.iid = t.question_id)
5592
                INNER JOIN $trackTable te
5593
                ON (t.exe_id = te.exe_id)
5594
                WHERE
5595
                    t.c_id = $courseId AND
5596
                    t.marks != q.ponderation AND
5597
                    exe_exo_id = $exerciseId AND
5598
                    status != 'incomplete'
5599
                    $sessionCondition
5600
                GROUP BY q.iid
5601
                ORDER BY count DESC
5602
                LIMIT $limit
5603
        ";
5604
5605
        $result = Database::query($sql);
5606
5607
        return Database::store_result($result, 'ASSOC');
5608
    }
5609
5610
    public static function getExerciseResultsCount($type, $courseId, $exerciseId, $sessionId = 0)
5611
    {
5612
        $courseId = (int) $courseId;
5613
        $exerciseId = (int) $exerciseId;
5614
5615
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5616
5617
        $sessionCondition = '';
5618
        if (!empty($sessionId)) {
5619
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
5620
        }
5621
5622
        $selectCount = 'count(DISTINCT te.exe_id)';
5623
        $scoreCondition = '';
5624
        switch ($type) {
5625
            case 'correct_student':
5626
                $selectCount = 'count(DISTINCT te.exe_user_id)';
5627
                $scoreCondition = ' AND exe_result = exe_weighting ';
5628
                break;
5629
            case 'wrong_student':
5630
                $selectCount = 'count(DISTINCT te.exe_user_id)';
5631
                $scoreCondition = ' AND  exe_result != exe_weighting ';
5632
                break;
5633
            case 'correct':
5634
                $scoreCondition = ' AND exe_result = exe_weighting ';
5635
                break;
5636
            case 'wrong':
5637
                $scoreCondition = ' AND exe_result != exe_weighting ';
5638
                break;
5639
        }
5640
5641
        $sql = "SELECT $selectCount count
5642
                FROM $trackTable te
5643
                WHERE
5644
                    c_id = $courseId AND
5645
                    exe_exo_id = $exerciseId AND
5646
                    status != 'incomplete'
5647
                    $scoreCondition
5648
                    $sessionCondition
5649
        ";
5650
        $result = Database::query($sql);
5651
        $totalRow = Database::fetch_array($result, 'ASSOC');
5652
        $total = 0;
5653
        if ($totalRow) {
5654
            $total = (int) $totalRow['count'];
5655
        }
5656
5657
        return $total;
5658
    }
5659
5660
    public static function parseContent($content, $stats, Exercise $exercise, $trackInfo, $currentUserId = 0)
5661
    {
5662
        $wrongAnswersCount = $stats['failed_answers_count'];
5663
        $attemptDate = substr($trackInfo['exe_date'], 0, 10);
5664
        $exerciseId = $exercise->iId;
5665
        $resultsStudentUrl = api_get_path(WEB_CODE_PATH).
5666
            'exercise/result.php?id='.$exerciseId.'&'.api_get_cidreq();
5667
        $resultsTeacherUrl = api_get_path(WEB_CODE_PATH).
5668
            'exercise/exercise_show.php?action=edit&id='.$exerciseId.'&'.api_get_cidreq();
5669
5670
        $content = str_replace(
5671
            [
5672
                '((exercise_error_count))',
5673
                '((all_answers_html))',
5674
                '((all_answers_teacher_html))',
5675
                '((exercise_title))',
5676
                '((exercise_attempt_date))',
5677
                '((link_to_test_result_page_student))',
5678
                '((link_to_test_result_page_teacher))',
5679
            ],
5680
            [
5681
                $wrongAnswersCount,
5682
                $stats['all_answers_html'],
5683
                $stats['all_answers_teacher_html'],
5684
                $exercise->get_formated_title(),
5685
                $attemptDate,
5686
                $resultsStudentUrl,
5687
                $resultsTeacherUrl,
5688
            ],
5689
            $content
5690
        );
5691
5692
        $currentUserId = empty($currentUserId) ? api_get_user_id() : (int) $currentUserId;
5693
5694
        $content = AnnouncementManager::parseContent(
5695
            $currentUserId,
5696
            $content,
5697
            api_get_course_id(),
5698
            api_get_session_id()
5699
        );
5700
5701
        return $content;
5702
    }
5703
5704
    public static function sendNotification(
5705
        $currentUserId,
5706
        $objExercise,
5707
        $exercise_stat_info,
5708
        $courseInfo,
5709
        $attemptCountToSend,
5710
        $stats,
5711
        $statsTeacher
5712
    ) {
5713
        $notifications = api_get_configuration_value('exercise_finished_notification_settings');
5714
        if (empty($notifications)) {
5715
            return false;
5716
        }
5717
5718
        $studentId = $exercise_stat_info['exe_user_id'];
5719
        $exerciseExtraFieldValue = new ExtraFieldValue('exercise');
5720
        $wrongAnswersCount = $stats['failed_answers_count'];
5721
        $exercisePassed = $stats['exercise_passed'];
5722
        $countPendingQuestions = $stats['count_pending_questions'];
5723
        $stats['all_answers_teacher_html'] = $statsTeacher['all_answers_html'];
5724
5725
        // If there are no pending questions (Open questions).
5726
        if (0 === $countPendingQuestions) {
5727
            /*$extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5728
                $objExercise->iId,
5729
                'signature_mandatory'
5730
            );
5731
5732
            if ($extraFieldData && isset($extraFieldData['value']) && 1 === (int) $extraFieldData['value']) {
5733
                if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($objExercise)) {
5734
                    $signature = ExerciseSignaturePlugin::getSignature($studentId, $exercise_stat_info);
5735
                    if (false !== $signature) {
5736
                        //return false;
5737
                    }
5738
                }
5739
            }*/
5740
5741
            // Notifications.
5742
            $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5743
                $objExercise->iId,
5744
                'notifications'
5745
            );
5746
            $exerciseNotification = '';
5747
            if ($extraFieldData && isset($extraFieldData['value'])) {
5748
                $exerciseNotification = $extraFieldData['value'];
5749
            }
5750
5751
            $subject = sprintf(get_lang('WrongAttemptXInCourseX'), $attemptCountToSend, $courseInfo['title']);
5752
            if ($exercisePassed) {
5753
                $subject = sprintf(get_lang('ExerciseValidationInCourseX'), $courseInfo['title']);
5754
            }
5755
5756
            if ($exercisePassed) {
5757
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5758
                    $objExercise->iId,
5759
                    'MailSuccess'
5760
                );
5761
            } else {
5762
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5763
                    $objExercise->iId,
5764
                    'MailAttempt'.$attemptCountToSend
5765
                );
5766
            }
5767
5768
            // Blocking exercise.
5769
            $blockPercentageExtra = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5770
                $objExercise->iId,
5771
                'blocking_percentage'
5772
            );
5773
            $blockPercentage = false;
5774
            if ($blockPercentageExtra && isset($blockPercentageExtra['value']) && $blockPercentageExtra['value']) {
5775
                $blockPercentage = $blockPercentageExtra['value'];
5776
            }
5777
            if ($blockPercentage) {
5778
                $passBlock = $stats['total_percentage'] > $blockPercentage;
5779
                if (false === $passBlock) {
5780
                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5781
                        $objExercise->iId,
5782
                        'MailIsBlockByPercentage'
5783
                    );
5784
                }
5785
            }
5786
5787
            $extraFieldValueUser = new ExtraFieldValue('user');
5788
5789
            if ($extraFieldData && isset($extraFieldData['value'])) {
5790
                $content = $extraFieldData['value'];
5791
                $content = self::parseContent($content, $stats, $objExercise, $exercise_stat_info, $studentId);
5792
                //if (false === $exercisePassed) {
5793
                if (0 !== $wrongAnswersCount) {
5794
                    $content .= $stats['failed_answers_html'];
5795
                }
5796
5797
                $sendMessage = true;
5798
                if (!empty($exerciseNotification)) {
5799
                    foreach ($notifications as $name => $notificationList) {
5800
                        if ($exerciseNotification !== $name) {
5801
                            continue;
5802
                        }
5803
                        foreach ($notificationList as $notificationName => $attemptData) {
5804
                            if ('student_check' === $notificationName) {
5805
                                $sendMsgIfInList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : '';
5806
                                if (!empty($sendMsgIfInList)) {
5807
                                    foreach ($sendMsgIfInList as $skipVariable => $skipValues) {
5808
                                        $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
5809
                                            $studentId,
5810
                                            $skipVariable
5811
                                        );
5812
5813
                                        if (empty($userExtraFieldValue)) {
5814
                                            $sendMessage = false;
5815
                                            break;
5816
                                        } else {
5817
                                            $sendMessage = false;
5818
                                            if (isset($userExtraFieldValue['value']) &&
5819
                                                in_array($userExtraFieldValue['value'], $skipValues)
5820
                                            ) {
5821
                                                $sendMessage = true;
5822
                                                break;
5823
                                            }
5824
                                        }
5825
                                    }
5826
                                }
5827
                                break;
5828
                            }
5829
                        }
5830
                    }
5831
                }
5832
5833
                // Send to student.
5834
                if ($sendMessage) {
5835
                    MessageManager::send_message($currentUserId, $subject, $content);
5836
                }
5837
            }
5838
5839
            if (!empty($exerciseNotification)) {
5840
                foreach ($notifications as $name => $notificationList) {
5841
                    if ($exerciseNotification !== $name) {
5842
                        continue;
5843
                    }
5844
                    foreach ($notificationList as $attemptData) {
5845
                        $skipNotification = false;
5846
                        $skipNotificationList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : [];
5847
                        if (!empty($skipNotificationList)) {
5848
                            foreach ($skipNotificationList as $skipVariable => $skipValues) {
5849
                                $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
5850
                                    $studentId,
5851
                                    $skipVariable
5852
                                );
5853
5854
                                if (empty($userExtraFieldValue)) {
5855
                                    $skipNotification = true;
5856
                                    break;
5857
                                } else {
5858
                                    if (isset($userExtraFieldValue['value'])) {
5859
                                        if (!in_array($userExtraFieldValue['value'], $skipValues)) {
5860
                                            $skipNotification = true;
5861
                                            break;
5862
                                        }
5863
                                    } else {
5864
                                        $skipNotification = true;
5865
                                        break;
5866
                                    }
5867
                                }
5868
                            }
5869
                        }
5870
5871
                        if ($skipNotification) {
5872
                            continue;
5873
                        }
5874
5875
                        $email = isset($attemptData['email']) ? $attemptData['email'] : '';
5876
                        $emailList = explode(',', $email);
5877
                        if (empty($emailList)) {
5878
                            continue;
5879
                        }
5880
                        $attempts = isset($attemptData['attempts']) ? $attemptData['attempts'] : [];
5881
                        foreach ($attempts as $attempt) {
5882
                            $sendMessage = false;
5883
                            if (isset($attempt['attempt']) && $attemptCountToSend !== (int) $attempt['attempt']) {
5884
                                continue;
5885
                            }
5886
5887
                            if (!isset($attempt['status'])) {
5888
                                continue;
5889
                            }
5890
5891
                            if ($blockPercentage && isset($attempt['is_block_by_percentage'])) {
5892
                                if ($attempt['is_block_by_percentage']) {
5893
                                    if ($passBlock) {
5894
                                        continue;
5895
                                    }
5896
                                } else {
5897
                                    if (false === $passBlock) {
5898
                                        continue;
5899
                                    }
5900
                                }
5901
                            }
5902
5903
                            switch ($attempt['status']) {
5904
                                case 'passed':
5905
                                    if ($exercisePassed) {
5906
                                        $sendMessage = true;
5907
                                    }
5908
                                    break;
5909
                                case 'failed':
5910
                                    if (false === $exercisePassed) {
5911
                                        $sendMessage = true;
5912
                                    }
5913
                                    break;
5914
                                case 'all':
5915
                                    $sendMessage = true;
5916
                                    break;
5917
                            }
5918
5919
                            if ($sendMessage) {
5920
                                $attachments = [];
5921
                                if (isset($attempt['add_pdf']) && $attempt['add_pdf']) {
5922
                                    // Get pdf content
5923
                                    $pdfExtraData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5924
                                        $objExercise->iId,
5925
                                        $attempt['add_pdf']
5926
                                    );
5927
5928
                                    if ($pdfExtraData && isset($pdfExtraData['value'])) {
5929
                                        $pdfContent = self::parseContent(
5930
                                            $pdfExtraData['value'],
5931
                                            $stats,
5932
                                            $objExercise,
5933
                                            $exercise_stat_info,
5934
                                            $studentId
5935
                                        );
5936
5937
                                        @$pdf = new PDF();
5938
                                        $filename = get_lang('Exercise');
5939
                                        $cssFile = api_get_path(SYS_CSS_PATH).'themes/chamilo/default.css';
5940
                                        $pdfPath = @$pdf->content_to_pdf(
5941
                                            "<html><body>$pdfContent</body></html>",
5942
                                            file_get_contents($cssFile),
5943
                                            $filename,
5944
                                            api_get_course_id(),
5945
                                            'F',
5946
                                            false,
5947
                                            null,
5948
                                            false,
5949
                                            true
5950
                                        );
5951
                                        $attachments[] = ['filename' => $filename, 'path' => $pdfPath];
5952
                                    }
5953
                                }
5954
5955
                                $content = isset($attempt['content_default']) ? $attempt['content_default'] : '';
5956
                                if (isset($attempt['content'])) {
5957
                                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5958
                                        $objExercise->iId,
5959
                                        $attempt['content']
5960
                                    );
5961
                                    if ($extraFieldData && isset($extraFieldData['value']) && !empty($extraFieldData['value'])) {
5962
                                        $content = $extraFieldData['value'];
5963
                                    }
5964
                                }
5965
5966
                                if (!empty($content)) {
5967
                                    $content = self::parseContent(
5968
                                        $content,
5969
                                        $stats,
5970
                                        $objExercise,
5971
                                        $exercise_stat_info,
5972
                                        $studentId
5973
                                    );
5974
                                    foreach ($emailList as $email) {
5975
                                        if (empty($email)) {
5976
                                            continue;
5977
                                        }
5978
                                        api_mail_html(
5979
                                            null,
5980
                                            $email,
5981
                                            $subject,
5982
                                            $content,
5983
                                            null,
5984
                                            null,
5985
                                            [],
5986
                                            $attachments
5987
                                        );
5988
                                    }
5989
                                }
5990
5991
                                if (isset($attempt['post_actions'])) {
5992
                                    foreach ($attempt['post_actions'] as $action => $params) {
5993
                                        switch ($action) {
5994
                                            case 'subscribe_student_to_courses':
5995
                                                foreach ($params as $code) {
5996
                                                    CourseManager::subscribeUser($currentUserId, $code);
5997
                                                    break;
5998
                                                }
5999
                                                break;
6000
                                        }
6001
                                    }
6002
                                }
6003
                            }
6004
                        }
6005
                    }
6006
                }
6007
            }
6008
        }
6009
    }
6010
6011
    /**
6012
     * Delete an exercise attempt.
6013
     *
6014
     * Log the exe_id deleted with the exe_user_id related.
6015
     *
6016
     * @param int $exeId
6017
     */
6018
    public static function deleteExerciseAttempt($exeId)
6019
    {
6020
        $exeId = (int) $exeId;
6021
6022
        $trackExerciseInfo = self::get_exercise_track_exercise_info($exeId);
6023
6024
        if (empty($trackExerciseInfo)) {
6025
            return;
6026
        }
6027
6028
        $tblTrackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6029
        $tblTrackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
6030
6031
        Database::query("DELETE FROM $tblTrackExercises WHERE exe_id = $exeId");
6032
        Database::query("DELETE FROM $tblTrackAttempt WHERE exe_id = $exeId");
6033
6034
        Event::addEvent(
6035
            LOG_EXERCISE_ATTEMPT_DELETE,
6036
            LOG_EXERCISE_ATTEMPT,
6037
            $exeId,
6038
            api_get_utc_datetime()
6039
        );
6040
        Event::addEvent(
6041
            LOG_EXERCISE_ATTEMPT_DELETE,
6042
            LOG_EXERCISE_AND_USER_ID,
6043
            $exeId.'-'.$trackExerciseInfo['exe_user_id'],
6044
            api_get_utc_datetime()
6045
        );
6046
    }
6047
6048
    public static function scorePassed($score, $total)
6049
    {
6050
        $compareResult = bccomp($score, $total, 3);
6051
        $scorePassed = 1 === $compareResult || 0 === $compareResult;
6052
        if (false === $scorePassed) {
6053
            $epsilon = 0.00001;
6054
            if (abs($score - $total) < $epsilon) {
6055
                $scorePassed = true;
6056
            }
6057
        }
6058
6059
        return $scorePassed;
6060
    }
6061
}
6062