Passed
Push — master ( 3b9d17...c5f69b )
by Julito
17:12
created

ExerciseLib::getOralFeedbackAudio()   A

Complexity

Conditions 5

Size

Total Lines 29
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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