Passed
Push — master ( 5deee6...e0f7b0 )
by Julito
10:06
created

ExerciseLib::get_exam_results_data()   F

Complexity

Conditions 94

Size

Total Lines 721
Code Lines 450

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 94
eloc 450
nop 14
dl 0
loc 721
rs 3.3333
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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