Passed
Push — master ( 2cb959...2b5b03 )
by Julito
11:13 queued 10s
created

ExerciseLib::get_number_students_finish_exercise()   A

Complexity

Conditions 2

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 16
nop 3
dl 0
loc 28
rs 9.7333
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
6
use Chamilo\CoreBundle\Entity\GradebookCategory;
7
use Chamilo\CoreBundle\Entity\TrackExercise;
8
use Chamilo\CoreBundle\Framework\Container;
9
use Chamilo\CourseBundle\Entity\CQuiz;
10
use ChamiloSession as Session;
11
12
/**
13
 * Class ExerciseLib
14
 * shows a question and its answers.
15
 *
16
 * @author Olivier Brouckaert <[email protected]>
17
 * @author Hubert Borderiou 2011-10-21
18
 * @author ivantcholakov2009-07-20
19
 * @author Julio Montoya
20
 */
21
class ExerciseLib
22
{
23
    /**
24
     * Shows a question.
25
     *
26
     * @param Exercise $exercise
27
     * @param int      $questionId     $questionId question id
28
     * @param bool     $only_questions if true only show the questions, no exercise title
29
     * @param bool     $origin         i.e = learnpath
30
     * @param string   $current_item   current item from the list of questions
31
     * @param bool     $show_title
32
     * @param bool     $freeze
33
     * @param array    $user_choice
34
     * @param bool     $show_comment
35
     * @param bool     $show_answers
36
     *
37
     * @throws \Exception
38
     *
39
     * @return bool|int
40
     */
41
    public static function showQuestion(
42
        $exercise,
43
        $questionId,
44
        $only_questions = false,
45
        $origin = false,
46
        $current_item = '',
47
        $show_title = true,
48
        $freeze = false,
49
        $user_choice = [],
50
        $show_comment = false,
51
        $show_answers = false,
52
        $show_icon = false
53
    ) {
54
        $course_id = $exercise->course_id;
55
        $exerciseId = $exercise->iId;
56
57
        if (empty($course_id)) {
58
            return '';
59
        }
60
        $course = $exercise->course;
61
62
        // Change false to true in the following line to enable answer hinting
63
        $debug_mark_answer = $show_answers;
64
        // Reads question information
65
        if (!$objQuestionTmp = Question::read($questionId, $course)) {
66
            // Question not found
67
            return false;
68
        }
69
70
        $questionRequireAuth = WhispeakAuthPlugin::questionRequireAuthentify($questionId);
71
72
        if (EXERCISE_FEEDBACK_TYPE_END != $exercise->getFeedbackType()) {
73
            $show_comment = false;
74
        }
75
76
        $answerType = $objQuestionTmp->selectType();
77
        $s = '';
78
        if (HOT_SPOT != $answerType &&
79
            HOT_SPOT_DELINEATION != $answerType &&
80
            ANNOTATION != $answerType
81
        ) {
82
            // Question is not a hotspot
83
            if (!$only_questions) {
84
                $questionDescription = $objQuestionTmp->selectDescription();
85
                if ($show_title) {
86
                    if ($exercise->display_category_name) {
87
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->id);
88
                    }
89
                    $titleToDisplay = $objQuestionTmp->getTitleToDisplay($exercise, $current_item);
90
                    if (READING_COMPREHENSION == $answerType) {
91
                        // In READING_COMPREHENSION, the title of the question
92
                        // contains the question itself, which can only be
93
                        // shown at the end of the given time, so hide for now
94
                        $titleToDisplay = Display::div(
95
                            $current_item.'. '.get_lang('Reading comprehension'),
96
                            ['class' => 'question_title']
97
                        );
98
                    }
99
                    echo $titleToDisplay;
100
                }
101
102
                if ($questionRequireAuth) {
103
                    WhispeakAuthPlugin::quizQuestionAuthentify($questionId, $exercise);
104
105
                    return false;
106
                }
107
108
                if (!empty($questionDescription) && READING_COMPREHENSION != $answerType) {
109
                    echo Display::div(
110
                        $questionDescription,
111
                        ['class' => 'question_description']
112
                    );
113
                }
114
            }
115
116
            if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION]) && $freeze) {
117
                return '';
118
            }
119
120
            echo '<div class="question_options">';
121
            // construction of the Answer object (also gets all answers details)
122
            $objAnswerTmp = new Answer($questionId, $course_id, $exercise);
123
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
124
            $quizQuestionOptions = Question::readQuestionOption($questionId, $course_id);
125
126
            // For "matching" type here, we need something a little bit special
127
            // because the match between the suggestions and the answers cannot be
128
            // done easily (suggestions and answers are in the same table), so we
129
            // have to go through answers first (elems with "correct" value to 0).
130
            $select_items = [];
131
            //This will contain the number of answers on the left side. We call them
132
            // suggestions here, for the sake of comprehensions, while the ones
133
            // on the right side are called answers
134
            $num_suggestions = 0;
135
            switch ($answerType) {
136
                case MATCHING:
137
                case DRAGGABLE:
138
                case MATCHING_DRAGGABLE:
139
                    if (DRAGGABLE == $answerType) {
140
                        $isVertical = 'v' === $objQuestionTmp->extra;
141
                        $s .= '
142
                            <div class="row">
143
                                <div class="col-md-12">
144
                                    <p class="small">'.get_lang('DraggableQuestionIntro').'</p>
145
                                    <ul class="exercise-draggable-answer list-unstyled '
146
                            .($isVertical ? '' : 'list-inline').'" id="question-'.$questionId.'" data-question="'
147
                            .$questionId.'">
148
                        ';
149
                    } else {
150
                        $s .= '<div id="drag'.$questionId.'_question" class="drag_question">
151
                               <table class="table table-hover table-striped data_table">';
152
                    }
153
154
                    // Iterate through answers.
155
                    $x = 1;
156
                    // Mark letters for each answer.
157
                    $letter = 'A';
158
                    $answer_matching = [];
159
                    $cpt1 = [];
160
                    for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
161
                        $answerCorrect = $objAnswerTmp->isCorrect($answerId);
162
                        $numAnswer = $objAnswerTmp->selectAutoId($answerId);
163
                        if (0 == $answerCorrect) {
164
                            // options (A, B, C, ...) that will be put into the list-box
165
                            // have the "correct" field set to 0 because they are answer
166
                            $cpt1[$x] = $letter;
167
                            $answer_matching[$x] = $objAnswerTmp->selectAnswerByAutoId($numAnswer);
168
                            $x++;
169
                            $letter++;
170
                        }
171
                    }
172
173
                    $i = 1;
174
                    $select_items[0]['id'] = 0;
175
                    $select_items[0]['letter'] = '--';
176
                    $select_items[0]['answer'] = '';
177
                    foreach ($answer_matching as $id => $value) {
178
                        $select_items[$i]['id'] = $value['iid'];
179
                        $select_items[$i]['letter'] = $cpt1[$id];
180
                        $select_items[$i]['answer'] = $value['answer'];
181
                        $i++;
182
                    }
183
184
                    $user_choice_array_position = [];
185
                    if (!empty($user_choice)) {
186
                        foreach ($user_choice as $item) {
187
                            $user_choice_array_position[$item['position']] = $item['answer'];
188
                        }
189
                    }
190
                    $num_suggestions = ($nbrAnswers - $x) + 1;
191
                    break;
192
                case FREE_ANSWER:
193
                    $fck_content = isset($user_choice[0]) && !empty($user_choice[0]['answer']) ? $user_choice[0]['answer'] : null;
194
                    $form = new FormValidator('free_choice_'.$questionId);
195
                    $config = [
196
                        'ToolbarSet' => 'TestFreeAnswer',
197
                    ];
198
                    $form->addHtmlEditor(
199
                        'choice['.$questionId.']',
200
                        null,
201
                        false,
202
                        false,
203
                        $config
204
                    );
205
                    $form->setDefaults(["choice[".$questionId."]" => $fck_content]);
206
                    $s .= $form->returnForm();
207
                    break;
208
                case ORAL_EXPRESSION:
209
                    // Add nanog
210
                    if ('true' === api_get_setting('enable_record_audio')) {
211
                        //@todo pass this as a parameter
212
                        global $exercise_stat_info;
213
                        if (!empty($exercise_stat_info)) {
214
                            $objQuestionTmp->initFile(
215
                                api_get_session_id(),
216
                                api_get_user_id(),
217
                                $exercise_stat_info['exe_exo_id'],
218
                                $exercise_stat_info['exe_id']
219
                            );
220
                        } else {
221
                            $objQuestionTmp->initFile(
222
                                api_get_session_id(),
223
                                api_get_user_id(),
224
                                $exerciseId,
225
                                'temp_exe'
226
                            );
227
                        }
228
229
                        echo $objQuestionTmp->returnRecorder();
230
                    }
231
232
                    $form = new FormValidator('free_choice_'.$questionId);
233
                    $config = ['ToolbarSet' => 'TestFreeAnswer'];
234
235
                    $form->addHtml('<div id="'.'hide_description_'.$questionId.'_options" style="display: none;">');
236
                    $form->addHtmlEditor(
237
                        "choice[$questionId]",
238
                        null,
239
                        false,
240
                        false,
241
                        $config
242
                    );
243
                    $form->addHtml('</div>');
244
                    $s .= $form->returnForm();
245
                    break;
246
            }
247
248
            // Now navigate through the possible answers, using the max number of
249
            // answers for the question as a limiter
250
            $lines_count = 1; // a counter for matching-type answers
251
            if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
252
                MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
253
            ) {
254
                $header = Display::tag('th', get_lang('Options'));
255
                foreach ($objQuestionTmp->options as $item) {
256
                    if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
257
                        if (in_array($item, $objQuestionTmp->options)) {
258
                            $header .= Display::tag('th', get_lang($item));
259
                        } else {
260
                            $header .= Display::tag('th', $item);
261
                        }
262
                    } else {
263
                        $header .= Display::tag('th', $item);
264
                    }
265
                }
266
                if ($show_comment) {
267
                    $header .= Display::tag('th', get_lang('Feedback'));
268
                }
269
                $s .= '<table class="table table-hover table-striped">';
270
                $s .= Display::tag(
271
                    'tr',
272
                    $header,
273
                    ['style' => 'text-align:left;']
274
                );
275
            } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
276
                $header = Display::tag('th', get_lang('Options'), ['width' => '50%']);
277
                echo "
278
                <script>
279
                    function RadioValidator(question_id, answer_id)
280
                    {
281
                        var ShowAlert = '';
282
                        var typeRadioB = '';
283
                        var AllFormElements = window.document.getElementById('exercise_form').elements;
284
285
                        for (i = 0; i < AllFormElements.length; i++) {
286
                            if (AllFormElements[i].type == 'radio') {
287
                                var ThisRadio = AllFormElements[i].name;
288
                                var ThisChecked = 'No';
289
                                var AllRadioOptions = document.getElementsByName(ThisRadio);
290
291
                                for (x = 0; x < AllRadioOptions.length; x++) {
292
                                     if (AllRadioOptions[x].checked && ThisChecked == 'No') {
293
                                         ThisChecked = 'Yes';
294
                                         break;
295
                                     }
296
                                }
297
298
                                var AlreadySearched = ShowAlert.indexOf(ThisRadio);
299
                                if (ThisChecked == 'No' && AlreadySearched == -1) {
300
                                    ShowAlert = ShowAlert + ThisRadio;
301
                                }
302
                            }
303
                        }
304
                        if (ShowAlert != '') {
305
306
                        } else {
307
                            $('.question-validate-btn').removeAttr('disabled');
308
                        }
309
                    }
310
311
                    function handleRadioRow(event, question_id, answer_id) {
312
                        var t = event.target;
313
                        if (t && t.tagName == 'INPUT')
314
                            return;
315
                        while (t && t.tagName != 'TD') {
316
                            t = t.parentElement;
317
                        }
318
                        var r = t.getElementsByTagName('INPUT')[0];
319
                        r.click();
320
                        RadioValidator(question_id, answer_id);
321
                    }
322
323
                    $(function() {
324
                        var ShowAlert = '';
325
                        var typeRadioB = '';
326
                        var question_id = $('input[name=question_id]').val();
327
                        var AllFormElements = window.document.getElementById('exercise_form').elements;
328
329
                        for (i = 0; i < AllFormElements.length; i++) {
330
                            if (AllFormElements[i].type == 'radio') {
331
                                var ThisRadio = AllFormElements[i].name;
332
                                var ThisChecked = 'No';
333
                                var AllRadioOptions = document.getElementsByName(ThisRadio);
334
335
                                for (x = 0; x < AllRadioOptions.length; x++) {
336
                                    if (AllRadioOptions[x].checked && ThisChecked == 'No') {
337
                                        ThisChecked = \"Yes\";
338
                                        break;
339
                                    }
340
                                }
341
342
                                var AlreadySearched = ShowAlert.indexOf(ThisRadio);
343
                                if (ThisChecked == 'No' && AlreadySearched == -1) {
344
                                    ShowAlert = ShowAlert + ThisRadio;
345
                                }
346
                            }
347
                        }
348
349
                        if (ShowAlert != '') {
350
                             $('.question-validate-btn').attr('disabled', 'disabled');
351
                        } else {
352
                            $('.question-validate-btn').removeAttr('disabled');
353
                        }
354
                    });
355
                </script>";
356
357
                foreach ($objQuestionTmp->optionsTitle as $item) {
358
                    if (in_array($item, $objQuestionTmp->optionsTitle)) {
359
                        $properties = [];
360
                        if ('Answers' === $item) {
361
                            $properties['colspan'] = 2;
362
                            $properties['style'] = 'background-color: #F56B2A; color: #ffffff;';
363
                        } elseif ('DegreeOfCertaintyThatMyAnswerIsCorrect' === $item) {
364
                            $properties['colspan'] = 6;
365
                            $properties['style'] = 'background-color: #330066; color: #ffffff;';
366
                        }
367
                        $header .= Display::tag('th', get_lang($item), $properties);
368
                    } else {
369
                        $header .= Display::tag('th', $item);
370
                    }
371
                }
372
373
                if ($show_comment) {
374
                    $header .= Display::tag('th', get_lang('Feedback'));
375
                }
376
377
                $s .= '<table class="table table-hover table-striped data_table">';
378
                $s .= Display::tag('tr', $header, ['style' => 'text-align:left;']);
379
380
                // ajout de la 2eme ligne d'entête pour true/falss et les pourcentages de certitude
381
                $header1 = Display::tag('th', '&nbsp;');
382
                $cpt1 = 0;
383
                foreach ($objQuestionTmp->options as $item) {
384
                    $colorBorder1 = ($cpt1 == (count($objQuestionTmp->options) - 1))
385
                        ? '' : 'border-right: solid #FFFFFF 1px;';
386
                    if ('True' === $item || 'False' === $item) {
387
                        $header1 .= Display::tag(
388
                            'th',
389
                            get_lang($item),
390
                            ['style' => 'background-color: #F7C9B4; color: black;'.$colorBorder1]
391
                        );
392
                    } else {
393
                        $header1 .= Display::tag(
394
                            'th',
395
                            $item,
396
                            ['style' => 'background-color: #e6e6ff; color: black;padding:5px; '.$colorBorder1]
397
                        );
398
                    }
399
                    $cpt1++;
400
                }
401
                if ($show_comment) {
402
                    $header1 .= Display::tag('th', '&nbsp;');
403
                }
404
405
                $s .= Display::tag('tr', $header1);
406
407
                // add explanation
408
                $header2 = Display::tag('th', '&nbsp;');
409
                $descriptionList = [
410
                    get_lang('I don\'t know the answer and I\'ve picked at random'),
411
                    get_lang('I am very unsure'),
412
                    get_lang('I am unsure'),
413
                    get_lang('I am pretty sure'),
414
                    get_lang('I am almost 100% sure'),
415
                    get_lang('I am totally sure'),
416
                ];
417
                $counter2 = 0;
418
                foreach ($objQuestionTmp->options as $item) {
419
                    if ('True' === $item || 'False' === $item) {
420
                        $header2 .= Display::tag('td',
421
                            '&nbsp;',
422
                            ['style' => 'background-color: #F7E1D7; color: black;border-right: solid #FFFFFF 1px;']);
423
                    } else {
424
                        $color_border2 = ($counter2 == (count($objQuestionTmp->options) - 1)) ?
425
                            '' : 'border-right: solid #FFFFFF 1px;font-size:11px;';
426
                        $header2 .= Display::tag(
427
                            'td',
428
                            nl2br($descriptionList[$counter2]),
429
                            ['style' => 'background-color: #EFEFFC; color: black; width: 110px; text-align:center;
430
                                vertical-align: top; padding:5px; '.$color_border2]);
431
                        $counter2++;
432
                    }
433
                }
434
                if ($show_comment) {
435
                    $header2 .= Display::tag('th', '&nbsp;');
436
                }
437
                $s .= Display::tag('tr', $header2);
438
            }
439
440
            if ($show_comment) {
441
                if (in_array(
442
                    $answerType,
443
                    [
444
                        MULTIPLE_ANSWER,
445
                        MULTIPLE_ANSWER_COMBINATION,
446
                        UNIQUE_ANSWER,
447
                        UNIQUE_ANSWER_IMAGE,
448
                        UNIQUE_ANSWER_NO_OPTION,
449
                        GLOBAL_MULTIPLE_ANSWER,
450
                    ]
451
                )) {
452
                    $header = Display::tag('th', get_lang('Options'));
453
                    if (EXERCISE_FEEDBACK_TYPE_END == $exercise->getFeedbackType()) {
454
                        $header .= Display::tag('th', get_lang('Feedback'));
455
                    }
456
                    $s .= '<table class="table table-hover table-striped">';
457
                    $s .= Display::tag(
458
                        'tr',
459
                        $header,
460
                        ['style' => 'text-align:left;']
461
                    );
462
                }
463
            }
464
465
            $matching_correct_answer = 0;
466
            $userChoiceList = [];
467
            if (!empty($user_choice)) {
468
                foreach ($user_choice as $item) {
469
                    $userChoiceList[] = $item['answer'];
470
                }
471
            }
472
473
            $hidingClass = '';
474
            if (READING_COMPREHENSION == $answerType) {
475
                /** @var ReadingComprehension */
476
                $objQuestionTmp->setExerciseType($exercise->selectType());
477
                $objQuestionTmp->processText($objQuestionTmp->selectDescription());
478
                $hidingClass = 'hide-reading-answers';
479
                $s .= Display::div(
480
                    $objQuestionTmp->selectTitle(),
481
                    ['class' => 'question_title '.$hidingClass]
482
                );
483
            }
484
485
            for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
486
                $answer = $objAnswerTmp->selectAnswer($answerId);
487
                $answerCorrect = $objAnswerTmp->isCorrect($answerId);
488
                $numAnswer = $objAnswerTmp->selectAutoId($answerId);
489
                $comment = $objAnswerTmp->selectComment($answerId);
490
                $attributes = [];
491
492
                switch ($answerType) {
493
                    case UNIQUE_ANSWER:
494
                    case UNIQUE_ANSWER_NO_OPTION:
495
                    case UNIQUE_ANSWER_IMAGE:
496
                    case READING_COMPREHENSION:
497
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
498
                        if (isset($user_choice[0]['answer']) && $user_choice[0]['answer'] == $numAnswer) {
499
                            $attributes = [
500
                                'id' => $input_id,
501
                                'checked' => 1,
502
                                'selected' => 1,
503
                            ];
504
                        } else {
505
                            $attributes = ['id' => $input_id];
506
                        }
507
508
                        if ($debug_mark_answer) {
509
                            if ($answerCorrect) {
510
                                $attributes['checked'] = 1;
511
                                $attributes['selected'] = 1;
512
                            }
513
                        }
514
515
                        if ($show_comment) {
516
                            $s .= '<tr><td>';
517
                        }
518
519
                        if (UNIQUE_ANSWER_IMAGE == $answerType) {
520
                            if ($show_comment) {
521
                                if (empty($comment)) {
522
                                    $s .= '<div id="answer'.$questionId.$numAnswer.'"
523
                                            class="exercise-unique-answer-image" style="text-align: center">';
524
                                } else {
525
                                    $s .= '<div id="answer'.$questionId.$numAnswer.'"
526
                                            class="exercise-unique-answer-image col-xs-6 col-sm-12"
527
                                            style="text-align: center">';
528
                                }
529
                            } else {
530
                                $s .= '<div id="answer'.$questionId.$numAnswer.'"
531
                                        class="exercise-unique-answer-image col-xs-6 col-md-3"
532
                                        style="text-align: center">';
533
                            }
534
                        }
535
536
                        if (UNIQUE_ANSWER_IMAGE != $answerType) {
537
                            $userStatus = STUDENT;
538
                            // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
539
                            // see BT#18242
540
                            if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
541
                                $userStatus = COURSEMANAGERLOWSECURITY;
542
                            }
543
                            $answer = Security::remove_XSS($answer, $userStatus);
544
                        }
545
                        $s .= Display::input(
546
                            'hidden',
547
                            'choice2['.$questionId.']',
548
                            '0'
549
                        );
550
551
                        $answer_input = null;
552
                        $attributes['class'] = 'checkradios';
553
                        if (UNIQUE_ANSWER_IMAGE == $answerType) {
554
                            $attributes['class'] = '';
555
                            $attributes['style'] = 'display: none;';
556
                            $answer = '<div class="thumbnail">'.$answer.'</div>';
557
                        }
558
559
                        $answer_input .= '<label class="radio '.$hidingClass.'">';
560
                        $answer_input .= Display::input(
561
                            'radio',
562
                            'choice['.$questionId.']',
563
                            $numAnswer,
564
                            $attributes
565
                        );
566
                        $answer_input .= $answer;
567
                        $answer_input .= '</label>';
568
569
                        if (UNIQUE_ANSWER_IMAGE == $answerType) {
570
                            $answer_input .= "</div>";
571
                        }
572
573
                        if ($show_comment) {
574
                            $s .= $answer_input;
575
                            $s .= '</td>';
576
                            $s .= '<td>';
577
                            $s .= $comment;
578
                            $s .= '</td>';
579
                            $s .= '</tr>';
580
                        } else {
581
                            $s .= $answer_input;
582
                        }
583
                        break;
584
                    case MULTIPLE_ANSWER:
585
                    case MULTIPLE_ANSWER_TRUE_FALSE:
586
                    case GLOBAL_MULTIPLE_ANSWER:
587
                    case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
588
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
589
                        $userStatus = STUDENT;
590
                        // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
591
                        // see BT#18242
592
                        if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
593
                            $userStatus = COURSEMANAGERLOWSECURITY;
594
                        }
595
                        $answer = Security::remove_XSS($answer, $userStatus);
596
597
                        if (in_array($numAnswer, $userChoiceList)) {
598
                            $attributes = [
599
                                'id' => $input_id,
600
                                'checked' => 1,
601
                                'selected' => 1,
602
                            ];
603
                        } else {
604
                            $attributes = ['id' => $input_id];
605
                        }
606
607
                        if ($debug_mark_answer) {
608
                            if ($answerCorrect) {
609
                                $attributes['checked'] = 1;
610
                                $attributes['selected'] = 1;
611
                            }
612
                        }
613
614
                        if (MULTIPLE_ANSWER == $answerType || GLOBAL_MULTIPLE_ANSWER == $answerType) {
615
                            $s .= '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
616
                            $attributes['class'] = 'checkradios';
617
                            $answer_input = '<label class="checkbox">';
618
                            $answer_input .= Display::input(
619
                                'checkbox',
620
                                'choice['.$questionId.']['.$numAnswer.']',
621
                                $numAnswer,
622
                                $attributes
623
                            );
624
                            $answer_input .= $answer;
625
                            $answer_input .= '</label>';
626
627
                            if ($show_comment) {
628
                                $s .= '<tr><td>';
629
                                $s .= $answer_input;
630
                                $s .= '</td>';
631
                                $s .= '<td>';
632
                                $s .= $comment;
633
                                $s .= '</td>';
634
                                $s .= '</tr>';
635
                            } else {
636
                                $s .= $answer_input;
637
                            }
638
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
639
                            $myChoice = [];
640
                            if (!empty($userChoiceList)) {
641
                                foreach ($userChoiceList as $item) {
642
                                    $item = explode(':', $item);
643
                                    if (!empty($item)) {
644
                                        $myChoice[$item[0]] = isset($item[1]) ? $item[1] : '';
645
                                    }
646
                                }
647
                            }
648
649
                            $s .= '<tr>';
650
                            $s .= Display::tag('td', $answer);
651
652
                            if (!empty($quizQuestionOptions)) {
653
                                foreach ($quizQuestionOptions as $id => $item) {
654
                                    if (isset($myChoice[$numAnswer]) && $id == $myChoice[$numAnswer]) {
655
                                        $attributes = [
656
                                            'checked' => 1,
657
                                            'selected' => 1,
658
                                        ];
659
                                    } else {
660
                                        $attributes = [];
661
                                    }
662
663
                                    if ($debug_mark_answer) {
664
                                        if ($id == $answerCorrect) {
665
                                            $attributes['checked'] = 1;
666
                                            $attributes['selected'] = 1;
667
                                        }
668
                                    }
669
                                    $s .= Display::tag(
670
                                        'td',
671
                                        Display::input(
672
                                            'radio',
673
                                            'choice['.$questionId.']['.$numAnswer.']',
674
                                            $id,
675
                                            $attributes
676
                                        ),
677
                                        ['style' => '']
678
                                    );
679
                                }
680
                            }
681
682
                            if ($show_comment) {
683
                                $s .= '<td>';
684
                                $s .= $comment;
685
                                $s .= '</td>';
686
                            }
687
                            $s .= '</tr>';
688
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
689
                            $myChoice = [];
690
                            if (!empty($userChoiceList)) {
691
                                foreach ($userChoiceList as $item) {
692
                                    $item = explode(':', $item);
693
                                    $myChoice[$item[0]] = $item[1];
694
                                }
695
                            }
696
                            $myChoiceDegreeCertainty = [];
697
                            if (!empty($userChoiceList)) {
698
                                foreach ($userChoiceList as $item) {
699
                                    $item = explode(':', $item);
700
                                    $myChoiceDegreeCertainty[$item[0]] = $item[2];
701
                                }
702
                            }
703
                            $s .= '<tr>';
704
                            $s .= Display::tag('td', $answer);
705
706
                            if (!empty($quizQuestionOptions)) {
707
                                foreach ($quizQuestionOptions as $id => $item) {
708
                                    if (isset($myChoice[$numAnswer]) && $id == $myChoice[$numAnswer]) {
709
                                        $attributes = ['checked' => 1, 'selected' => 1];
710
                                    } else {
711
                                        $attributes = [];
712
                                    }
713
                                    $attributes['onChange'] = 'RadioValidator('.$questionId.', '.$numAnswer.')';
714
715
                                    // radio button selection
716
                                    if (isset($myChoiceDegreeCertainty[$numAnswer]) &&
717
                                        $id == $myChoiceDegreeCertainty[$numAnswer]
718
                                    ) {
719
                                        $attributes1 = ['checked' => 1, 'selected' => 1];
720
                                    } else {
721
                                        $attributes1 = [];
722
                                    }
723
724
                                    $attributes1['onChange'] = 'RadioValidator('.$questionId.', '.$numAnswer.')';
725
726
                                    if ($debug_mark_answer) {
727
                                        if ($id == $answerCorrect) {
728
                                            $attributes['checked'] = 1;
729
                                            $attributes['selected'] = 1;
730
                                        }
731
                                    }
732
733
                                    if ('True' == $item['name'] || 'False' == $item['name']) {
734
                                        $s .= Display::tag('td',
735
                                            Display::input('radio',
736
                                                'choice['.$questionId.']['.$numAnswer.']',
737
                                                $id,
738
                                                $attributes
739
                                            ),
740
                                            ['style' => 'text-align:center; background-color:#F7E1D7;',
741
                                                'onclick' => 'handleRadioRow(event, '.
742
                                                    $questionId.', '.
743
                                                    $numAnswer.')',
744
                                            ]
745
                                        );
746
                                    } else {
747
                                        $s .= Display::tag('td',
748
                                            Display::input('radio',
749
                                                'choiceDegreeCertainty['.$questionId.']['.$numAnswer.']',
750
                                                $id,
751
                                                $attributes1
752
                                            ),
753
                                            ['style' => 'text-align:center; background-color:#EFEFFC;',
754
                                                'onclick' => 'handleRadioRow(event, '.
755
                                                    $questionId.', '.
756
                                                    $numAnswer.')',
757
                                            ]
758
                                        );
759
                                    }
760
                                }
761
                            }
762
763
                            if ($show_comment) {
764
                                $s .= '<td>';
765
                                $s .= $comment;
766
                                $s .= '</td>';
767
                            }
768
                            $s .= '</tr>';
769
                        }
770
                        break;
771
                    case MULTIPLE_ANSWER_COMBINATION:
772
                        // multiple answers
773
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
774
775
                        if (in_array($numAnswer, $userChoiceList)) {
776
                            $attributes = [
777
                                'id' => $input_id,
778
                                'checked' => 1,
779
                                'selected' => 1,
780
                            ];
781
                        } else {
782
                            $attributes = ['id' => $input_id];
783
                        }
784
785
                        if ($debug_mark_answer) {
786
                            if ($answerCorrect) {
787
                                $attributes['checked'] = 1;
788
                                $attributes['selected'] = 1;
789
                            }
790
                        }
791
792
                        $userStatus = STUDENT;
793
                        // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
794
                        // see BT#18242
795
                        if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
796
                            $userStatus = COURSEMANAGERLOWSECURITY;
797
                        }
798
                        $answer = Security::remove_XSS($answer, $userStatus);
799
                        $answer_input = '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
800
                        $answer_input .= '<label class="checkbox">';
801
                        $answer_input .= Display::input(
802
                            'checkbox',
803
                            'choice['.$questionId.']['.$numAnswer.']',
804
                            1,
805
                            $attributes
806
                        );
807
                        $answer_input .= $answer;
808
                        $answer_input .= '</label>';
809
810
                        if ($show_comment) {
811
                            $s .= '<tr>';
812
                            $s .= '<td>';
813
                            $s .= $answer_input;
814
                            $s .= '</td>';
815
                            $s .= '<td>';
816
                            $s .= $comment;
817
                            $s .= '</td>';
818
                            $s .= '</tr>';
819
                        } else {
820
                            $s .= $answer_input;
821
                        }
822
                        break;
823
                    case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
824
                        $s .= '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
825
                        $myChoice = [];
826
                        if (!empty($userChoiceList)) {
827
                            foreach ($userChoiceList as $item) {
828
                                $item = explode(':', $item);
829
                                if (isset($item[1]) && isset($item[0])) {
830
                                    $myChoice[$item[0]] = $item[1];
831
                                }
832
                            }
833
                        }
834
                        $userStatus = STUDENT;
835
                        // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
836
                        // see BT#18242
837
                        if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
838
                            $userStatus = COURSEMANAGERLOWSECURITY;
839
                        }
840
                        $answer = Security::remove_XSS($answer, $userStatus);
841
                        $s .= '<tr>';
842
                        $s .= Display::tag('td', $answer);
843
                        foreach ($objQuestionTmp->options as $key => $item) {
844
                            if (isset($myChoice[$numAnswer]) && $key == $myChoice[$numAnswer]) {
845
                                $attributes = [
846
                                    'checked' => 1,
847
                                    'selected' => 1,
848
                                ];
849
                            } else {
850
                                $attributes = [];
851
                            }
852
853
                            if ($debug_mark_answer) {
854
                                if ($key == $answerCorrect) {
855
                                    $attributes['checked'] = 1;
856
                                    $attributes['selected'] = 1;
857
                                }
858
                            }
859
                            $s .= Display::tag(
860
                                'td',
861
                                Display::input(
862
                                    'radio',
863
                                    'choice['.$questionId.']['.$numAnswer.']',
864
                                    $key,
865
                                    $attributes
866
                                )
867
                            );
868
                        }
869
870
                        if ($show_comment) {
871
                            $s .= '<td>';
872
                            $s .= $comment;
873
                            $s .= '</td>';
874
                        }
875
                        $s .= '</tr>';
876
                        break;
877
                    case FILL_IN_BLANKS:
878
                        // display the question, with field empty, for student to fill it,
879
                        // or filled to display the answer in the Question preview of the exercise/admin.php page
880
                        $displayForStudent = true;
881
                        $listAnswerInfo = FillBlanks::getAnswerInfo($answer);
882
                        // Correct answers
883
                        $correctAnswerList = $listAnswerInfo['words'];
884
                        // Student's answer
885
                        $studentAnswerList = [];
886
                        if (isset($user_choice[0]['answer'])) {
887
                            $arrayStudentAnswer = FillBlanks::getAnswerInfo(
888
                                $user_choice[0]['answer'],
889
                                true
890
                            );
891
                            $studentAnswerList = $arrayStudentAnswer['student_answer'];
892
                        }
893
894
                        // If the question must be shown with the answer (in page exercise/admin.php)
895
                        // for teacher preview set the student-answer to the correct answer
896
                        if ($debug_mark_answer) {
897
                            $studentAnswerList = $correctAnswerList;
898
                            $displayForStudent = false;
899
                        }
900
901
                        if (!empty($correctAnswerList) && !empty($studentAnswerList)) {
902
                            $answer = '';
903
                            for ($i = 0; $i < count($listAnswerInfo['common_words']) - 1; $i++) {
904
                                // display the common word
905
                                $answer .= $listAnswerInfo['common_words'][$i];
906
                                // display the blank word
907
                                $correctItem = $listAnswerInfo['words'][$i];
908
                                if (isset($studentAnswerList[$i])) {
909
                                    // If student already started this test and answered this question,
910
                                    // fill the blank with his previous answers
911
                                    // may be "" if student viewed the question, but did not fill the blanks
912
                                    $correctItem = $studentAnswerList[$i];
913
                                }
914
                                $attributes['style'] = 'width:'.$listAnswerInfo['input_size'][$i].'px';
915
                                $answer .= FillBlanks::getFillTheBlankHtml(
916
                                    $current_item,
917
                                    $questionId,
918
                                    $correctItem,
919
                                    $attributes,
920
                                    $answer,
921
                                    $listAnswerInfo,
922
                                    $displayForStudent,
923
                                    $i
924
                                );
925
                            }
926
                            // display the last common word
927
                            $answer .= $listAnswerInfo['common_words'][$i];
928
                        } else {
929
                            // display empty [input] with the right width for student to fill it
930
                            $answer = '';
931
                            for ($i = 0; $i < count($listAnswerInfo['common_words']) - 1; $i++) {
932
                                // display the common words
933
                                $answer .= $listAnswerInfo['common_words'][$i];
934
                                // display the blank word
935
                                $attributes['style'] = 'width:'.$listAnswerInfo['input_size'][$i].'px';
936
                                $answer .= FillBlanks::getFillTheBlankHtml(
937
                                    $current_item,
938
                                    $questionId,
939
                                    '',
940
                                    $attributes,
941
                                    $answer,
942
                                    $listAnswerInfo,
943
                                    $displayForStudent,
944
                                    $i
945
                                );
946
                            }
947
                            // display the last common word
948
                            $answer .= $listAnswerInfo['common_words'][$i];
949
                        }
950
                        $s .= $answer;
951
                        break;
952
                    case CALCULATED_ANSWER:
953
                        /*
954
                         * In the CALCULATED_ANSWER test
955
                         * you mustn't have [ and ] in the textarea
956
                         * you mustn't have @@ in the textarea
957
                         * the text to find mustn't be empty or contains only spaces
958
                         * the text to find mustn't contains HTML tags
959
                         * the text to find mustn't contains char "
960
                         */
961
                        if (null !== $origin) {
962
                            global $exe_id;
963
                            $exe_id = (int) $exe_id;
964
                            $trackAttempts = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
965
                            $sql = "SELECT answer FROM $trackAttempts
966
                                    WHERE exe_id = $exe_id AND question_id= $questionId";
967
                            $rsLastAttempt = Database::query($sql);
968
                            $rowLastAttempt = Database::fetch_array($rsLastAttempt);
969
970
                            $answer = null;
971
                            if (isset($rowLastAttempt['answer'])) {
972
                                $answer = $rowLastAttempt['answer'];
973
                            }
974
975
                            if (empty($answer)) {
976
                                $_SESSION['calculatedAnswerId'][$questionId] = mt_rand(
977
                                    1,
978
                                    $nbrAnswers
979
                                );
980
                                $answer = $objAnswerTmp->selectAnswer(
981
                                    $_SESSION['calculatedAnswerId'][$questionId]
982
                                );
983
                            }
984
                        }
985
986
                        [$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($exercise, $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($exercise, $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
        $excludeTestId = (int) $excludeTestId;
1655
1656
        $sql = "SELECT qz.title quiz_title,
1657
                        c.title course_title,
1658
                        s.name session_name,
1659
                        qz.iid as quiz_id,
1660
                        qz.c_id,
1661
                        qz.session_id
1662
                FROM c_quiz qz,
1663
                    c_quiz_rel_question qq,
1664
                    course c,
1665
                    session s
1666
                WHERE qz.c_id = c.id AND
1667
                    (qz.session_id = s.id OR qz.session_id = 0) AND
1668
                    qq.quiz_id = qz.iid AND ";
1669
        if (!empty($excludeTestId)) {
1670
            $sql .= " qz.iid != $excludeTestId AND ";
1671
        }
1672
        $sql .= "     qq.question_id = $questionId
1673
                GROUP BY qq.iid";
1674
1675
        $result = [];
1676
        $html = "";
1677
1678
        $sqlResult = Database::query($sql);
1679
1680
        if (0 != Database::num_rows($sqlResult)) {
1681
            while ($row = Database::fetch_array($sqlResult, 'ASSOC')) {
1682
                $tmp = [];
1683
                $tmp[0] = $row['course_title'];
1684
                $tmp[1] = $row['session_name'];
1685
                $tmp[2] = $row['quiz_title'];
1686
                // Send do other test with r=1 to reset current test session variables
1687
                $urlToQuiz = api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_cidreq().'&exerciseId='.$row['quiz_id'].'&r=1';
1688
                $tmp[3] = '<a href="'.$urlToQuiz.'">'.Display::return_icon('quiz.png', get_lang('Edit')).'</a>';
1689
                if (0 == (int) $row['session_id']) {
1690
                    $tmp[1] = '-';
1691
                }
1692
1693
                $result[] = $tmp;
1694
            }
1695
1696
            $headers = [
1697
                get_lang('Course'),
1698
                get_lang('Session'),
1699
                get_lang('Quiz'),
1700
                get_lang('LinkToTestEdition'),
1701
            ];
1702
1703
            $title = Display::div(
1704
                get_lang('QuestionAlsoUsedInTheFollowingTests'),
1705
                [
1706
                    'class' => 'section-title',
1707
                    'style' => 'margin-top: 25px; border-bottom: none',
1708
                ]
1709
            );
1710
1711
            $html = $title.Display::table($headers, $result);
1712
        }
1713
1714
        echo $html;
1715
    }
1716
1717
    /**
1718
     * @param int $exeId
1719
     *
1720
     * @return array
1721
     */
1722
    public static function get_exercise_track_exercise_info($exeId)
1723
    {
1724
        $quizTable = Database::get_course_table(TABLE_QUIZ_TEST);
1725
        $trackExerciseTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1726
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
1727
        $exeId = (int) $exeId;
1728
        $result = [];
1729
        if (!empty($exeId)) {
1730
            $sql = " SELECT q.*, tee.*
1731
                FROM $quizTable as q
1732
                INNER JOIN $trackExerciseTable as tee
1733
                ON q.iid = tee.exe_exo_id
1734
                WHERE
1735
                    tee.exe_id = $exeId";
1736
1737
            $sqlResult = Database::query($sql);
1738
            if (Database::num_rows($sqlResult)) {
1739
                $result = Database::fetch_array($sqlResult, 'ASSOC');
1740
                $result['duration_formatted'] = '';
1741
                if (!empty($result['exe_duration'])) {
1742
                    $time = api_format_time($result['exe_duration'], 'js');
1743
                    $result['duration_formatted'] = $time;
1744
                }
1745
            }
1746
        }
1747
1748
        return $result;
1749
    }
1750
1751
    /**
1752
     * Validates the time control key.
1753
     *
1754
     * @param int $lp_id
1755
     * @param int $lp_item_id
1756
     *
1757
     * @return bool
1758
     */
1759
    public static function exercise_time_control_is_valid(Exercise $exercise, $lp_id = 0, $lp_item_id = 0)
1760
    {
1761
        $exercise_id = $exercise->getId();
1762
        $expiredTime = $exercise->expired_time;
1763
1764
        if (!empty($expiredTime)) {
1765
            $current_expired_time_key = self::get_time_control_key(
1766
                $exercise_id,
1767
                $lp_id,
1768
                $lp_item_id
1769
            );
1770
            if (isset($_SESSION['expired_time'][$current_expired_time_key])) {
1771
                $current_time = time();
1772
                $expired_time = api_strtotime(
1773
                    $_SESSION['expired_time'][$current_expired_time_key],
1774
                    'UTC'
1775
                );
1776
                $total_time_allowed = $expired_time + 30;
1777
                if ($total_time_allowed < $current_time) {
1778
                    return false;
1779
                }
1780
1781
                return true;
1782
            }
1783
1784
            return false;
1785
        }
1786
1787
        return true;
1788
    }
1789
1790
    /**
1791
     * Deletes the time control token.
1792
     *
1793
     * @param int $exercise_id
1794
     * @param int $lp_id
1795
     * @param int $lp_item_id
1796
     */
1797
    public static function exercise_time_control_delete(
1798
        $exercise_id,
1799
        $lp_id = 0,
1800
        $lp_item_id = 0
1801
    ) {
1802
        $current_expired_time_key = self::get_time_control_key(
1803
            $exercise_id,
1804
            $lp_id,
1805
            $lp_item_id
1806
        );
1807
        unset($_SESSION['expired_time'][$current_expired_time_key]);
1808
    }
1809
1810
    /**
1811
     * Generates the time control key.
1812
     *
1813
     * @param int $exercise_id
1814
     * @param int $lp_id
1815
     * @param int $lp_item_id
1816
     *
1817
     * @return string
1818
     */
1819
    public static function get_time_control_key(
1820
        $exercise_id,
1821
        $lp_id = 0,
1822
        $lp_item_id = 0
1823
    ) {
1824
        $exercise_id = (int) $exercise_id;
1825
        $lp_id = (int) $lp_id;
1826
        $lp_item_id = (int) $lp_item_id;
1827
1828
        return
1829
            api_get_course_int_id().'_'.
1830
            api_get_session_id().'_'.
1831
            $exercise_id.'_'.
1832
            api_get_user_id().'_'.
1833
            $lp_id.'_'.
1834
            $lp_item_id;
1835
    }
1836
1837
    /**
1838
     * Get session time control.
1839
     *
1840
     * @param int $exercise_id
1841
     * @param int $lp_id
1842
     * @param int $lp_item_id
1843
     *
1844
     * @return int
1845
     */
1846
    public static function get_session_time_control_key(
1847
        $exercise_id,
1848
        $lp_id = 0,
1849
        $lp_item_id = 0
1850
    ) {
1851
        $return_value = 0;
1852
        $time_control_key = self::get_time_control_key(
1853
            $exercise_id,
1854
            $lp_id,
1855
            $lp_item_id
1856
        );
1857
        if (isset($_SESSION['expired_time']) && isset($_SESSION['expired_time'][$time_control_key])) {
1858
            $return_value = $_SESSION['expired_time'][$time_control_key];
1859
        }
1860
1861
        return $return_value;
1862
    }
1863
1864
    /**
1865
     * Gets count of exam results.
1866
     *
1867
     * @param int   $exerciseId
1868
     * @param array $conditions
1869
     * @param int   $courseId
1870
     * @param bool  $showSession
1871
     *
1872
     * @return array
1873
     */
1874
    public static function get_count_exam_results($exerciseId, $conditions, $courseId, $showSession = false)
1875
    {
1876
        $count = self::get_exam_results_data(
1877
            null,
1878
            null,
1879
            null,
1880
            null,
1881
            $exerciseId,
1882
            $conditions,
1883
            true,
1884
            $courseId,
1885
            $showSession
1886
        );
1887
1888
        return $count;
1889
    }
1890
1891
    /**
1892
     * Gets the exam'data results.
1893
     *
1894
     * @todo this function should be moved in a library  + no global calls
1895
     *
1896
     * @param int    $from
1897
     * @param int    $number_of_items
1898
     * @param int    $column
1899
     * @param string $direction
1900
     * @param int    $exercise_id
1901
     * @param null   $extra_where_conditions
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $extra_where_conditions is correct as it would always require null to be passed?
Loading history...
1902
     * @param bool   $get_count
1903
     * @param int    $courseId
1904
     * @param bool   $showSessionField
1905
     * @param bool   $showExerciseCategories
1906
     * @param array  $userExtraFieldsToAdd
1907
     * @param bool   $useCommaAsDecimalPoint
1908
     * @param bool   $roundValues
1909
     * @param bool   $getOnyIds
1910
     *
1911
     * @return array
1912
     */
1913
    public static function get_exam_results_data(
1914
        $from,
1915
        $number_of_items,
1916
        $column,
1917
        $direction,
1918
        $exercise_id,
1919
        $extra_where_conditions = null,
1920
        $get_count = false,
1921
        $courseId = null,
1922
        $showSessionField = false,
1923
        $showExerciseCategories = false,
1924
        $userExtraFieldsToAdd = [],
1925
        $useCommaAsDecimalPoint = false,
1926
        $roundValues = false,
1927
        $getOnyIds = false
1928
    ) {
1929
        //@todo replace all this globals
1930
        global $filter;
1931
        $courseId = (int) $courseId;
1932
        $course = api_get_course_entity($courseId);
1933
        if (null === $course) {
1934
            return [];
1935
        }
1936
1937
        $sessionId = api_get_session_id();
1938
        $exercise_id = (int) $exercise_id;
1939
1940
        $is_allowedToEdit =
1941
            api_is_allowed_to_edit(null, true) ||
1942
            api_is_allowed_to_edit(true) ||
1943
            api_is_drh() ||
1944
            api_is_student_boss() ||
1945
            api_is_session_admin();
1946
        $TBL_USER = Database::get_main_table(TABLE_MAIN_USER);
1947
        $TBL_EXERCICES = Database::get_course_table(TABLE_QUIZ_TEST);
1948
        $TBL_GROUP_REL_USER = Database::get_course_table(TABLE_GROUP_USER);
1949
        $TBL_GROUP = Database::get_course_table(TABLE_GROUP);
1950
        $TBL_TRACK_EXERCICES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1951
        $TBL_TRACK_ATTEMPT_RECORDING = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING);
1952
1953
        $session_id_and = '';
1954
        $sessionCondition = '';
1955
        if (!$showSessionField) {
1956
            $session_id_and = " AND te.session_id = $sessionId ";
1957
            $sessionCondition = " AND ttte.session_id = $sessionId";
1958
        }
1959
1960
        $exercise_where = '';
1961
        if (!empty($exercise_id)) {
1962
            $exercise_where .= ' AND te.exe_exo_id = '.$exercise_id.'  ';
1963
        }
1964
1965
        // sql for chamilo-type tests for teacher / tutor view
1966
        $sql_inner_join_tbl_track_exercices = "
1967
        (
1968
            SELECT DISTINCT ttte.*, if(tr.exe_id,1, 0) as revised
1969
            FROM $TBL_TRACK_EXERCICES ttte
1970
            LEFT JOIN $TBL_TRACK_ATTEMPT_RECORDING tr
1971
            ON (ttte.exe_id = tr.exe_id)
1972
            WHERE
1973
                c_id = $courseId AND
1974
                exe_exo_id = $exercise_id
1975
                $sessionCondition
1976
        )";
1977
1978
        if ($is_allowedToEdit) {
1979
            //@todo fix to work with COURSE_RELATION_TYPE_RRHH in both queries
1980
            // Hack in order to filter groups
1981
            $sql_inner_join_tbl_user = '';
1982
            if (strpos($extra_where_conditions, 'group_id')) {
1983
                $sql_inner_join_tbl_user = "
1984
                (
1985
                    SELECT
1986
                        u.id as user_id,
1987
                        firstname,
1988
                        lastname,
1989
                        official_code,
1990
                        email,
1991
                        username,
1992
                        g.name as group_name,
1993
                        g.id as group_id
1994
                    FROM $TBL_USER u
1995
                    INNER JOIN $TBL_GROUP_REL_USER gru
1996
                    ON (gru.user_id = u.id AND gru.c_id= $courseId )
1997
                    INNER JOIN $TBL_GROUP g
1998
                    ON (gru.group_id = g.id AND g.c_id= $courseId )
1999
                )";
2000
            }
2001
2002
            if (strpos($extra_where_conditions, 'group_all')) {
2003
                $extra_where_conditions = str_replace(
2004
                    "AND (  group_id = 'group_all'  )",
2005
                    '',
2006
                    $extra_where_conditions
2007
                );
2008
                $extra_where_conditions = str_replace(
2009
                    "AND group_id = 'group_all'",
2010
                    '',
2011
                    $extra_where_conditions
2012
                );
2013
                $extra_where_conditions = str_replace(
2014
                    "group_id = 'group_all' AND",
2015
                    '',
2016
                    $extra_where_conditions
2017
                );
2018
2019
                $sql_inner_join_tbl_user = "
2020
                (
2021
                    SELECT
2022
                        u.id as user_id,
2023
                        firstname,
2024
                        lastname,
2025
                        official_code,
2026
                        email,
2027
                        username,
2028
                        '' as group_name,
2029
                        '' as group_id
2030
                    FROM $TBL_USER u
2031
                )";
2032
                $sql_inner_join_tbl_user = null;
2033
            }
2034
2035
            if (strpos($extra_where_conditions, 'group_none')) {
2036
                $extra_where_conditions = str_replace(
2037
                    "AND (  group_id = 'group_none'  )",
2038
                    "AND (  group_id is null  )",
2039
                    $extra_where_conditions
2040
                );
2041
                $extra_where_conditions = str_replace(
2042
                    "AND group_id = 'group_none'",
2043
                    "AND (  group_id is null  )",
2044
                    $extra_where_conditions
2045
                );
2046
                $sql_inner_join_tbl_user = "
2047
            (
2048
                SELECT
2049
                    u.id as user_id,
2050
                    firstname,
2051
                    lastname,
2052
                    official_code,
2053
                    email,
2054
                    username,
2055
                    g.name as group_name,
2056
                    g.iid as group_id
2057
                FROM $TBL_USER u
2058
                LEFT OUTER JOIN $TBL_GROUP_REL_USER gru
2059
                ON (gru.user_id = u.id AND gru.c_id= $courseId )
2060
                LEFT OUTER JOIN $TBL_GROUP g
2061
                ON (gru.group_id = g.id AND g.c_id = $courseId )
2062
            )";
2063
            }
2064
2065
            // All
2066
            $is_empty_sql_inner_join_tbl_user = false;
2067
            if (empty($sql_inner_join_tbl_user)) {
2068
                $is_empty_sql_inner_join_tbl_user = true;
2069
                $sql_inner_join_tbl_user = "
2070
            (
2071
                SELECT u.id as user_id, firstname, lastname, email, username, ' ' as group_name, '' as group_id, official_code
2072
                FROM $TBL_USER u
2073
                WHERE u.status NOT IN(".api_get_users_status_ignored_in_reports('string').")
2074
            )";
2075
            }
2076
2077
            $sqlFromOption = " , $TBL_GROUP_REL_USER AS gru ";
2078
            $sqlWhereOption = "  AND gru.c_id = $courseId AND gru.user_id = user.id ";
2079
            $first_and_last_name = api_is_western_name_order() ? "firstname, lastname" : "lastname, firstname";
2080
2081
            if ($get_count) {
2082
                $sql_select = 'SELECT count(te.exe_id) ';
2083
            } else {
2084
                $sql_select = "SELECT DISTINCT
2085
                    user.user_id,
2086
                    $first_and_last_name,
2087
                    official_code,
2088
                    ce.title,
2089
                    username,
2090
                    te.score,
2091
                    te.max_score,
2092
                    te.exe_date,
2093
                    te.exe_id,
2094
                    te.session_id,
2095
                    email as exemail,
2096
                    te.start_date,
2097
                    ce.expired_time,
2098
                    steps_counter,
2099
                    exe_user_id,
2100
                    te.exe_duration,
2101
                    te.status as completion_status,
2102
                    propagate_neg,
2103
                    revised,
2104
                    group_name,
2105
                    group_id,
2106
                    orig_lp_id,
2107
                    te.user_ip";
2108
            }
2109
2110
            $sql = " $sql_select
2111
                FROM $TBL_EXERCICES AS ce
2112
                INNER JOIN $sql_inner_join_tbl_track_exercices AS te
2113
                ON (te.exe_exo_id = ce.iid)
2114
                INNER JOIN $sql_inner_join_tbl_user AS user
2115
                ON (user.user_id = exe_user_id)
2116
                WHERE
2117
                    te.c_id = $courseId $session_id_and AND
2118
                    ce.active <> -1
2119
                    $exercise_where
2120
                    $extra_where_conditions
2121
                ";
2122
        }
2123
2124
        if (empty($sql)) {
2125
            return false;
2126
        }
2127
2128
        if ($get_count) {
2129
            $resx = Database::query($sql);
2130
            $rowx = Database::fetch_row($resx, 'ASSOC');
2131
2132
            return $rowx[0];
2133
        }
2134
2135
        $teacher_list = CourseManager::get_teacher_list_from_course_code($course->getCode());
2136
        $teacher_id_list = [];
2137
        if (!empty($teacher_list)) {
2138
            foreach ($teacher_list as $teacher) {
2139
                $teacher_id_list[] = $teacher['user_id'];
2140
            }
2141
        }
2142
2143
        $scoreDisplay = new ScoreDisplay();
2144
        $decimalSeparator = '.';
2145
        $thousandSeparator = ',';
2146
2147
        if ($useCommaAsDecimalPoint) {
2148
            $decimalSeparator = ',';
2149
            $thousandSeparator = '';
2150
        }
2151
2152
        $listInfo = [];
2153
        $column = !empty($column) ? Database::escape_string($column) : null;
2154
        $from = (int) $from;
2155
        $number_of_items = (int) $number_of_items;
2156
        $direction = !in_array(strtolower(trim($direction)), ['asc', 'desc']) ? 'asc' : $direction;
2157
2158
        if (!empty($column)) {
2159
            $sql .= " ORDER BY `$column` $direction ";
2160
        }
2161
2162
        if (!$getOnyIds) {
2163
            $sql .= " LIMIT $from, $number_of_items";
2164
        }
2165
2166
        $results = [];
2167
        $resx = Database::query($sql);
2168
        while ($rowx = Database::fetch_array($resx, 'ASSOC')) {
2169
            $results[] = $rowx;
2170
        }
2171
2172
        $group_list = GroupManager::get_group_list(null, $course);
2173
        $clean_group_list = [];
2174
        if (!empty($group_list)) {
2175
            foreach ($group_list as $group) {
2176
                $clean_group_list[$group['iid']] = $group['name'];
2177
            }
2178
        }
2179
2180
        $lp_list_obj = new LearnpathList(api_get_user_id());
2181
        $lp_list = $lp_list_obj->get_flat_list();
2182
        $oldIds = array_column($lp_list, 'lp_old_id', 'iid');
2183
2184
        if (is_array($results)) {
2185
            $users_array_id = [];
2186
            $from_gradebook = false;
2187
            if (isset($_GET['gradebook']) && 'view' === $_GET['gradebook']) {
2188
                $from_gradebook = true;
2189
            }
2190
            $sizeof = count($results);
2191
            $locked = api_resource_is_locked_by_gradebook(
2192
                $exercise_id,
2193
                LINK_EXERCISE
2194
            );
2195
2196
            $timeNow = strtotime(api_get_utc_datetime());
2197
            // Looping results
2198
            for ($i = 0; $i < $sizeof; $i++) {
2199
                $revised = $results[$i]['revised'];
2200
                if ('incomplete' === $results[$i]['completion_status']) {
2201
                    // If the exercise was incomplete, we need to determine
2202
                    // if it is still into the time allowed, or if its
2203
                    // allowed time has expired and it can be closed
2204
                    // (it's "unclosed")
2205
                    $minutes = $results[$i]['expired_time'];
2206
                    if (0 == $minutes) {
2207
                        // There's no time limit, so obviously the attempt
2208
                        // can still be "ongoing", but the teacher should
2209
                        // be able to choose to close it, so mark it as
2210
                        // "unclosed" instead of "ongoing"
2211
                        $revised = 2;
2212
                    } else {
2213
                        $allowedSeconds = $minutes * 60;
2214
                        $timeAttemptStarted = strtotime($results[$i]['start_date']);
2215
                        $secondsSinceStart = $timeNow - $timeAttemptStarted;
2216
                        if ($secondsSinceStart > $allowedSeconds) {
2217
                            $revised = 2; // mark as "unclosed"
2218
                        } else {
2219
                            $revised = 3; // mark as "ongoing"
2220
                        }
2221
                    }
2222
                }
2223
2224
                if ($from_gradebook && ($is_allowedToEdit)) {
2225
                    if (in_array(
2226
                        $results[$i]['username'].$results[$i]['firstname'].$results[$i]['lastname'],
2227
                        $users_array_id
2228
                    )) {
2229
                        continue;
2230
                    }
2231
                    $users_array_id[] = $results[$i]['username'].$results[$i]['firstname'].$results[$i]['lastname'];
2232
                }
2233
2234
                $lp_obj = isset($results[$i]['orig_lp_id']) && isset($lp_list[$results[$i]['orig_lp_id']]) ? $lp_list[$results[$i]['orig_lp_id']] : null;
2235
                if (empty($lp_obj)) {
2236
                    // Try to get the old id (id instead of iid)
2237
                    $lpNewId = isset($results[$i]['orig_lp_id']) && isset($oldIds[$results[$i]['orig_lp_id']]) ? $oldIds[$results[$i]['orig_lp_id']] : null;
2238
                    if ($lpNewId) {
2239
                        $lp_obj = isset($lp_list[$lpNewId]) ? $lp_list[$lpNewId] : null;
2240
                    }
2241
                }
2242
                $lp_name = null;
2243
                if ($lp_obj) {
2244
                    $url = api_get_path(WEB_CODE_PATH).
2245
                        'lp/lp_controller.php?'.api_get_cidreq().'&action=view&lp_id='.$results[$i]['orig_lp_id'];
2246
                    $lp_name = Display::url(
2247
                        $lp_obj['lp_name'],
2248
                        $url,
2249
                        ['target' => '_blank']
2250
                    );
2251
                }
2252
2253
                // Add all groups by user
2254
                $group_name_list = '';
2255
                if ($is_empty_sql_inner_join_tbl_user) {
2256
                    $group_list = GroupManager::get_group_ids(
2257
                        api_get_course_int_id(),
2258
                        $results[$i]['user_id']
2259
                    );
2260
2261
                    foreach ($group_list as $id) {
2262
                        if (isset($clean_group_list[$id])) {
2263
                            $group_name_list .= $clean_group_list[$id].'<br/>';
2264
                        }
2265
                    }
2266
                    $results[$i]['group_name'] = $group_name_list;
2267
                }
2268
2269
                $results[$i]['exe_duration'] = !empty($results[$i]['exe_duration']) ? round($results[$i]['exe_duration'] / 60) : 0;
2270
                $id = $results[$i]['exe_id'];
2271
                $dt = api_convert_and_format_date($results[$i]['max_score']);
2272
2273
                // we filter the results if we have the permission to
2274
                $result_disabled = 0;
2275
                if (isset($results[$i]['results_disabled'])) {
2276
                    $result_disabled = (int) $results[$i]['results_disabled'];
2277
                }
2278
                if (0 == $result_disabled) {
2279
                    $my_res = $results[$i]['score'];
2280
                    $my_total = $results[$i]['max_score'];
2281
                    $results[$i]['start_date'] = api_get_local_time($results[$i]['start_date']);
2282
                    $results[$i]['exe_date'] = api_get_local_time($results[$i]['exe_date']);
2283
2284
                    if (!$results[$i]['propagate_neg'] && $my_res < 0) {
2285
                        $my_res = 0;
2286
                    }
2287
2288
                    $score = self::show_score(
2289
                        $my_res,
2290
                        $my_total,
2291
                        true,
2292
                        true,
2293
                        false,
2294
                        false,
2295
                        $decimalSeparator,
2296
                        $thousandSeparator,
2297
                        $roundValues
2298
                    );
2299
2300
                    $actions = '<div class="pull-right">';
2301
                    if ($is_allowedToEdit) {
2302
                        if (isset($teacher_id_list)) {
2303
                            if (in_array(
2304
                                $results[$i]['exe_user_id'],
2305
                                $teacher_id_list
2306
                            )) {
2307
                                $actions .= Display::return_icon('teacher.png', get_lang('Trainer'));
2308
                            }
2309
                        }
2310
                        $revisedLabel = '';
2311
                        switch ($revised) {
2312
                            case 0:
2313
                                $actions .= "<a href='exercise_show.php?".api_get_cidreq()."&action=qualify&id=$id'>".
2314
                                    Display:: return_icon(
2315
                                        'quiz.png',
2316
                                        get_lang('Grade activity')
2317
                                    );
2318
                                $actions .= '</a>';
2319
                                $revisedLabel = Display::label(
2320
                                    get_lang('Not validated'),
2321
                                    'info'
2322
                                );
2323
                                break;
2324
                            case 1:
2325
                                $actions .= "<a href='exercise_show.php?".api_get_cidreq()."&action=edit&id=$id'>".
2326
                                    Display:: return_icon(
2327
                                        'edit.png',
2328
                                        get_lang('Edit'),
2329
                                        [],
2330
                                        ICON_SIZE_SMALL
2331
                                    );
2332
                                $actions .= '</a>';
2333
                                $revisedLabel = Display::label(
2334
                                    get_lang('Validated'),
2335
                                    'success'
2336
                                );
2337
                                break;
2338
                            case 2: //finished but not marked as such
2339
                                $actions .= '<a href="exercise_report.php?'
2340
                                    .api_get_cidreq()
2341
                                    .'&exerciseId='
2342
                                    .$exercise_id
2343
                                    .'&a=close&id='
2344
                                    .$id
2345
                                    .'">'.
2346
                                    Display:: return_icon(
2347
                                        'lock.png',
2348
                                        get_lang('Mark attempt as closed'),
2349
                                        [],
2350
                                        ICON_SIZE_SMALL
2351
                                    );
2352
                                $actions .= '</a>';
2353
                                $revisedLabel = Display::label(
2354
                                    get_lang('Unclosed'),
2355
                                    'warning'
2356
                                );
2357
                                break;
2358
                            case 3: //still ongoing
2359
                                $actions .= Display:: return_icon(
2360
                                    'clock.png',
2361
                                    get_lang('Attempt still going on. Please wait.'),
2362
                                    [],
2363
                                    ICON_SIZE_SMALL
2364
                                );
2365
                                $actions .= '';
2366
                                $revisedLabel = Display::label(
2367
                                    get_lang('Ongoing'),
2368
                                    'danger'
2369
                                );
2370
                                break;
2371
                        }
2372
2373
                        if (2 == $filter) {
2374
                            $actions .= ' <a href="exercise_history.php?'.api_get_cidreq().'&exe_id='.$id.'">'.
2375
                                Display:: return_icon(
2376
                                    'history.png',
2377
                                    get_lang('View changes history')
2378
                                ).'</a>';
2379
                        }
2380
2381
                        // Admin can always delete the attempt
2382
                        if ((false == $locked || api_is_platform_admin()) && !api_is_student_boss()) {
2383
                            $ip = Tracking::get_ip_from_user_event(
2384
                                $results[$i]['exe_user_id'],
2385
                                api_get_utc_datetime(),
2386
                                false
2387
                            );
2388
                            $actions .= '<a href="http://www.whatsmyip.org/ip-geo-location/?ip='.$ip.'" target="_blank">'
2389
                                .Display::return_icon('info.png', $ip)
2390
                                .'</a>';
2391
2392
                            $recalculateUrl = api_get_path(WEB_CODE_PATH).'exercise/recalculate.php?'.
2393
                                api_get_cidreq().'&'.
2394
                                http_build_query([
2395
                                    'id' => $id,
2396
                                    'exercise' => $exercise_id,
2397
                                    'user' => $results[$i]['exe_user_id'],
2398
                                ]);
2399
                            $actions .= Display::url(
2400
                                Display::return_icon('reload.png', get_lang('Recalculate results')),
2401
                                $recalculateUrl,
2402
                                [
2403
                                    'data-exercise' => $exercise_id,
2404
                                    'data-user' => $results[$i]['exe_user_id'],
2405
                                    'data-id' => $id,
2406
                                    'class' => 'exercise-recalculate',
2407
                                ]
2408
                            );
2409
2410
                            $filterByUser = isset($_GET['filter_by_user']) ? (int) $_GET['filter_by_user'] : 0;
2411
                            $delete_link = '<a
2412
                                href="exercise_report.php?'.api_get_cidreq().'&filter_by_user='.$filterByUser.'&filter='.$filter.'&exerciseId='.$exercise_id.'&delete=delete&did='.$id.'"
2413
                                onclick=
2414
                                "javascript:if(!confirm(\''.sprintf(addslashes(get_lang('Delete attempt?')), $results[$i]['username'], $dt).'\')) return false;"
2415
                                >';
2416
                            $delete_link .= Display::return_icon('delete.png', addslashes(get_lang('Delete'))).'</a>';
2417
2418
                            if (api_is_drh() && !api_is_platform_admin()) {
2419
                                $delete_link = null;
2420
                            }
2421
                            if (api_is_session_admin()) {
2422
                                $delete_link = '';
2423
                            }
2424
                            if (3 == $revised) {
2425
                                $delete_link = null;
2426
                            }
2427
                            $actions .= $delete_link;
2428
                        }
2429
                    } else {
2430
                        $attempt_url = api_get_path(WEB_CODE_PATH).'exercise/result.php?'.api_get_cidreq().'&id='.$results[$i]['exe_id'].'&sid='.$sessionId;
2431
                        $attempt_link = Display::url(
2432
                            get_lang('Show'),
2433
                            $attempt_url,
2434
                            [
2435
                                'class' => 'ajax btn btn-default',
2436
                                'data-title' => get_lang('Show'),
2437
                            ]
2438
                        );
2439
                        $actions .= $attempt_link;
2440
                    }
2441
                    $actions .= '</div>';
2442
2443
                    if (!empty($userExtraFieldsToAdd)) {
2444
                        foreach ($userExtraFieldsToAdd as $variable) {
2445
                            $extraFieldValue = new ExtraFieldValue('user');
2446
                            $values = $extraFieldValue->get_values_by_handler_and_field_variable(
2447
                                $results[$i]['user_id'],
2448
                                $variable
2449
                            );
2450
                            if (isset($values['value'])) {
2451
                                $results[$i][$variable] = $values['value'];
2452
                            }
2453
                        }
2454
                    }
2455
2456
                    $exeId = $results[$i]['exe_id'];
2457
                    $results[$i]['id'] = $exeId;
2458
                    $category_list = [];
2459
                    if ($is_allowedToEdit) {
2460
                        $sessionName = '';
2461
                        $sessionStartAccessDate = '';
2462
                        if (!empty($results[$i]['session_id'])) {
2463
                            $sessionInfo = api_get_session_info($results[$i]['session_id']);
2464
                            if (!empty($sessionInfo)) {
2465
                                $sessionName = $sessionInfo['name'];
2466
                                $sessionStartAccessDate = api_get_local_time($sessionInfo['access_start_date']);
2467
                            }
2468
                        }
2469
2470
                        $objExercise = new Exercise($courseId);
2471
                        if ($showExerciseCategories) {
2472
                            // Getting attempt info
2473
                            $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
2474
                            if (!empty($exercise_stat_info['data_tracking'])) {
2475
                                $question_list = explode(',', $exercise_stat_info['data_tracking']);
2476
                                if (!empty($question_list)) {
2477
                                    foreach ($question_list as $questionId) {
2478
                                        $objQuestionTmp = Question::read($questionId, $objExercise->course);
2479
                                        // We're inside *one* question. Go through each possible answer for this question
2480
                                        $result = $objExercise->manage_answer(
2481
                                            $exeId,
2482
                                            $questionId,
2483
                                            null,
2484
                                            'exercise_result',
2485
                                            false,
2486
                                            false,
2487
                                            true,
2488
                                            false,
2489
                                            $objExercise->selectPropagateNeg(),
2490
                                            null,
2491
                                            true
2492
                                        );
2493
2494
                                        $my_total_score = $result['score'];
2495
                                        $my_total_weight = $result['weight'];
2496
2497
                                        // Category report
2498
                                        $category_was_added_for_this_test = false;
2499
                                        if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
2500
                                            if (!isset($category_list[$objQuestionTmp->category]['score'])) {
2501
                                                $category_list[$objQuestionTmp->category]['score'] = 0;
2502
                                            }
2503
                                            if (!isset($category_list[$objQuestionTmp->category]['total'])) {
2504
                                                $category_list[$objQuestionTmp->category]['total'] = 0;
2505
                                            }
2506
                                            $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
2507
                                            $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
2508
                                            $category_was_added_for_this_test = true;
2509
                                        }
2510
2511
                                        if (isset($objQuestionTmp->category_list) &&
2512
                                            !empty($objQuestionTmp->category_list)
2513
                                        ) {
2514
                                            foreach ($objQuestionTmp->category_list as $category_id) {
2515
                                                $category_list[$category_id]['score'] += $my_total_score;
2516
                                                $category_list[$category_id]['total'] += $my_total_weight;
2517
                                                $category_was_added_for_this_test = true;
2518
                                            }
2519
                                        }
2520
2521
                                        // No category for this question!
2522
                                        if (false == $category_was_added_for_this_test) {
2523
                                            if (!isset($category_list['none']['score'])) {
2524
                                                $category_list['none']['score'] = 0;
2525
                                            }
2526
                                            if (!isset($category_list['none']['total'])) {
2527
                                                $category_list['none']['total'] = 0;
2528
                                            }
2529
2530
                                            $category_list['none']['score'] += $my_total_score;
2531
                                            $category_list['none']['total'] += $my_total_weight;
2532
                                        }
2533
                                    }
2534
                                }
2535
                            }
2536
                        }
2537
2538
                        foreach ($category_list as $categoryId => $result) {
2539
                            $scoreToDisplay = self::show_score(
2540
                                $result['score'],
2541
                                $result['total'],
2542
                                true,
2543
                                true,
2544
                                false,
2545
                                false,
2546
                                $decimalSeparator,
2547
                                $thousandSeparator,
2548
                                $roundValues
2549
                            );
2550
                            $results[$i]['category_'.$categoryId] = $scoreToDisplay;
2551
                            $results[$i]['category_'.$categoryId.'_score_percentage'] = self::show_score(
2552
                                $result['score'],
2553
                                $result['total'],
2554
                                true,
2555
                                true,
2556
                                true, // $show_only_percentage = false
2557
                                true, // hide % sign
2558
                                $decimalSeparator,
2559
                                $thousandSeparator,
2560
                                $roundValues
2561
                            );
2562
                            $results[$i]['category_'.$categoryId.'_only_score'] = $result['score'];
2563
                            $results[$i]['category_'.$categoryId.'_total'] = $result['total'];
2564
                        }
2565
                        $results[$i]['session'] = $sessionName;
2566
                        $results[$i]['session_access_start_date'] = $sessionStartAccessDate;
2567
                        $results[$i]['status'] = $revisedLabel;
2568
                        $results[$i]['score'] = $score;
2569
                        $results[$i]['score_percentage'] = self::show_score(
2570
                            $my_res,
2571
                            $my_total,
2572
                            true,
2573
                            true,
2574
                            true,
2575
                            true,
2576
                            $decimalSeparator,
2577
                            $thousandSeparator,
2578
                            $roundValues
2579
                        );
2580
2581
                        if ($roundValues) {
2582
                            $whole = floor($my_res); // 1
2583
                            $fraction = $my_res - $whole; // .25
2584
                            if ($fraction >= 0.5) {
2585
                                $onlyScore = ceil($my_res);
2586
                            } else {
2587
                                $onlyScore = round($my_res);
2588
                            }
2589
                        } else {
2590
                            $onlyScore = $scoreDisplay->format_score(
2591
                                $my_res,
2592
                                false,
2593
                                $decimalSeparator,
2594
                                $thousandSeparator
2595
                            );
2596
                        }
2597
2598
                        $results[$i]['only_score'] = $onlyScore;
2599
2600
                        if ($roundValues) {
2601
                            $whole = floor($my_total); // 1
2602
                            $fraction = $my_total - $whole; // .25
2603
                            if ($fraction >= 0.5) {
2604
                                $onlyTotal = ceil($my_total);
2605
                            } else {
2606
                                $onlyTotal = round($my_total);
2607
                            }
2608
                        } else {
2609
                            $onlyTotal = $scoreDisplay->format_score(
2610
                                $my_total,
2611
                                false,
2612
                                $decimalSeparator,
2613
                                $thousandSeparator
2614
                            );
2615
                        }
2616
                        $results[$i]['total'] = $onlyTotal;
2617
                        $results[$i]['lp'] = $lp_name;
2618
                        $results[$i]['actions'] = $actions;
2619
                        $listInfo[] = $results[$i];
2620
                    } else {
2621
                        $results[$i]['status'] = $revisedLabel;
2622
                        $results[$i]['score'] = $score;
2623
                        $results[$i]['actions'] = $actions;
2624
                        $listInfo[] = $results[$i];
2625
                    }
2626
                }
2627
            }
2628
        }
2629
2630
        return $listInfo;
2631
    }
2632
2633
    /**
2634
     * @param $score
2635
     * @param $weight
2636
     *
2637
     * @return array
2638
     */
2639
    public static function convertScoreToPlatformSetting($score, $weight)
2640
    {
2641
        $maxNote = api_get_setting('exercise_max_score');
2642
        $minNote = api_get_setting('exercise_min_score');
2643
2644
        if ('' != $maxNote && '' != $minNote) {
2645
            if (!empty($weight) && (float) $weight !== (float) 0) {
2646
                $score = $minNote + ($maxNote - $minNote) * $score / $weight;
2647
            } else {
2648
                $score = $minNote;
2649
            }
2650
            $weight = $maxNote;
2651
        }
2652
2653
        return ['score' => $score, 'weight' => $weight];
2654
    }
2655
2656
    /**
2657
     * Converts the score with the exercise_max_note and exercise_min_score
2658
     * the platform settings + formats the results using the float_format function.
2659
     *
2660
     * @param float  $score
2661
     * @param float  $weight
2662
     * @param bool   $show_percentage       show percentage or not
2663
     * @param bool   $use_platform_settings use or not the platform settings
2664
     * @param bool   $show_only_percentage
2665
     * @param bool   $hidePercentageSign    hide "%" sign
2666
     * @param string $decimalSeparator
2667
     * @param string $thousandSeparator
2668
     * @param bool   $roundValues           This option rounds the float values into a int using ceil()
2669
     * @param bool   $removeEmptyDecimals
2670
     *
2671
     * @return string an html with the score modified
2672
     */
2673
    public static function show_score(
2674
        $score,
2675
        $weight,
2676
        $show_percentage = true,
2677
        $use_platform_settings = true,
2678
        $show_only_percentage = false,
2679
        $hidePercentageSign = false,
2680
        $decimalSeparator = '.',
2681
        $thousandSeparator = ',',
2682
        $roundValues = false,
2683
        $removeEmptyDecimals = false
2684
    ) {
2685
        if (is_null($score) && is_null($weight)) {
2686
            return '-';
2687
        }
2688
2689
        $decimalSeparator = empty($decimalSeparator) ? '.' : $decimalSeparator;
2690
        $thousandSeparator = empty($thousandSeparator) ? ',' : $thousandSeparator;
2691
2692
        if ($use_platform_settings) {
2693
            $result = self::convertScoreToPlatformSetting($score, $weight);
2694
            $score = $result['score'];
2695
            $weight = $result['weight'];
2696
        }
2697
2698
        $percentage = (100 * $score) / (0 != $weight ? $weight : 1);
2699
        // Formats values
2700
        $percentage = float_format($percentage, 1);
2701
        $score = float_format($score, 1);
2702
        $weight = float_format($weight, 1);
2703
2704
        if ($roundValues) {
2705
            $whole = floor($percentage); // 1
2706
            $fraction = $percentage - $whole; // .25
2707
2708
            // Formats values
2709
            if ($fraction >= 0.5) {
2710
                $percentage = ceil($percentage);
2711
            } else {
2712
                $percentage = round($percentage);
2713
            }
2714
2715
            $whole = floor($score); // 1
2716
            $fraction = $score - $whole; // .25
2717
            if ($fraction >= 0.5) {
2718
                $score = ceil($score);
2719
            } else {
2720
                $score = round($score);
2721
            }
2722
2723
            $whole = floor($weight); // 1
2724
            $fraction = $weight - $whole; // .25
2725
            if ($fraction >= 0.5) {
2726
                $weight = ceil($weight);
2727
            } else {
2728
                $weight = round($weight);
2729
            }
2730
        } else {
2731
            // Formats values
2732
            $percentage = float_format($percentage, 1, $decimalSeparator, $thousandSeparator);
2733
            $score = float_format($score, 1, $decimalSeparator, $thousandSeparator);
2734
            $weight = float_format($weight, 1, $decimalSeparator, $thousandSeparator);
2735
        }
2736
2737
        if ($show_percentage) {
2738
            $percentageSign = ' %';
2739
            if ($hidePercentageSign) {
2740
                $percentageSign = '';
2741
            }
2742
            $html = $percentage."$percentageSign ($score / $weight)";
2743
            if ($show_only_percentage) {
2744
                $html = $percentage.$percentageSign;
2745
            }
2746
        } else {
2747
            if ($removeEmptyDecimals) {
2748
                if (ScoreDisplay::hasEmptyDecimals($weight)) {
2749
                    $weight = round($weight);
2750
                }
2751
            }
2752
            $html = $score.' / '.$weight;
2753
        }
2754
2755
        // Over write score
2756
        $scoreBasedInModel = self::convertScoreToModel($percentage);
2757
        if (!empty($scoreBasedInModel)) {
2758
            $html = $scoreBasedInModel;
2759
        }
2760
2761
        // Ignore other formats and use the configuration['exercise_score_format'] value
2762
        // But also keep the round values settings.
2763
        $format = api_get_configuration_value('exercise_score_format');
2764
        if (!empty($format)) {
2765
            $html = ScoreDisplay::instance()->display_score([$score, $weight], $format);
2766
        }
2767
2768
        return Display::span($html, ['class' => 'score_exercise']);
2769
    }
2770
2771
    /**
2772
     * @param array $model
2773
     * @param float $percentage
2774
     *
2775
     * @return string
2776
     */
2777
    public static function getModelStyle($model, $percentage)
2778
    {
2779
        return '<span class="'.$model['css_class'].'">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>';
2780
    }
2781
2782
    /**
2783
     * @param float $percentage value between 0 and 100
2784
     *
2785
     * @return string
2786
     */
2787
    public static function convertScoreToModel($percentage)
2788
    {
2789
        $model = self::getCourseScoreModel();
2790
        if (!empty($model)) {
2791
            $scoreWithGrade = [];
2792
            foreach ($model['score_list'] as $item) {
2793
                if ($percentage >= $item['min'] && $percentage <= $item['max']) {
2794
                    $scoreWithGrade = $item;
2795
                    break;
2796
                }
2797
            }
2798
2799
            if (!empty($scoreWithGrade)) {
2800
                return self::getModelStyle($scoreWithGrade, $percentage);
2801
            }
2802
        }
2803
2804
        return '';
2805
    }
2806
2807
    /**
2808
     * @return array
2809
     */
2810
    public static function getCourseScoreModel()
2811
    {
2812
        $modelList = self::getScoreModels();
2813
        if (empty($modelList)) {
2814
            return [];
2815
        }
2816
2817
        $courseInfo = api_get_course_info();
2818
        if (!empty($courseInfo)) {
2819
            $scoreModelId = api_get_course_setting('score_model_id');
2820
            if (-1 != $scoreModelId) {
2821
                $modelIdList = array_column($modelList['models'], 'id');
2822
                if (in_array($scoreModelId, $modelIdList)) {
2823
                    foreach ($modelList['models'] as $item) {
2824
                        if ($item['id'] == $scoreModelId) {
2825
                            return $item;
2826
                        }
2827
                    }
2828
                }
2829
            }
2830
        }
2831
2832
        return [];
2833
    }
2834
2835
    /**
2836
     * @return array
2837
     */
2838
    public static function getScoreModels()
2839
    {
2840
        return api_get_configuration_value('score_grade_model');
2841
    }
2842
2843
    /**
2844
     * @param float  $score
2845
     * @param float  $weight
2846
     * @param string $passPercentage
2847
     *
2848
     * @return bool
2849
     */
2850
    public static function isSuccessExerciseResult($score, $weight, $passPercentage)
2851
    {
2852
        $percentage = float_format(
2853
            ($score / (0 != $weight ? $weight : 1)) * 100,
2854
            1
2855
        );
2856
        if (isset($passPercentage) && !empty($passPercentage)) {
2857
            if ($percentage >= $passPercentage) {
2858
                return true;
2859
            }
2860
        }
2861
2862
        return false;
2863
    }
2864
2865
    /**
2866
     * @param string $name
2867
     * @param $weight
2868
     * @param $selected
2869
     *
2870
     * @return bool
2871
     */
2872
    public static function addScoreModelInput(
2873
        FormValidator $form,
2874
        $name,
2875
        $weight,
2876
        $selected
2877
    ) {
2878
        $model = self::getCourseScoreModel();
2879
        if (empty($model)) {
2880
            return false;
2881
        }
2882
2883
        /** @var HTML_QuickForm_select $element */
2884
        $element = $form->createElement(
2885
            'select',
2886
            $name,
2887
            get_lang('Score'),
2888
            [],
2889
            ['class' => 'exercise_mark_select']
2890
        );
2891
2892
        foreach ($model['score_list'] as $item) {
2893
            $i = api_number_format($item['score_to_qualify'] / 100 * $weight, 2);
2894
            $label = self::getModelStyle($item, $i);
2895
            $attributes = [
2896
                'class' => $item['css_class'],
2897
            ];
2898
            if ($selected == $i) {
2899
                $attributes['selected'] = 'selected';
2900
            }
2901
            $element->addOption($label, $i, $attributes);
2902
        }
2903
        $form->addElement($element);
2904
    }
2905
2906
    /**
2907
     * @return string
2908
     */
2909
    public static function getJsCode()
2910
    {
2911
        // Filling the scores with the right colors.
2912
        $models = self::getCourseScoreModel();
2913
        $cssListToString = '';
2914
        if (!empty($models)) {
2915
            $cssList = array_column($models['score_list'], 'css_class');
2916
            $cssListToString = implode(' ', $cssList);
2917
        }
2918
2919
        if (empty($cssListToString)) {
2920
            return '';
2921
        }
2922
        $js = <<<EOT
2923
2924
        function updateSelect(element) {
2925
            var spanTag = element.parent().find('span.filter-option');
2926
            var value = element.val();
2927
            var selectId = element.attr('id');
2928
            var optionClass = $('#' + selectId + ' option[value="'+value+'"]').attr('class');
2929
            spanTag.removeClass('$cssListToString');
2930
            spanTag.addClass(optionClass);
2931
        }
2932
2933
        $(function() {
2934
            // Loading values
2935
            $('.exercise_mark_select').on('loaded.bs.select', function() {
2936
                updateSelect($(this));
2937
            });
2938
            // On change
2939
            $('.exercise_mark_select').on('changed.bs.select', function() {
2940
                updateSelect($(this));
2941
            });
2942
        });
2943
EOT;
2944
2945
        return $js;
2946
    }
2947
2948
    /**
2949
     * @param float  $score
2950
     * @param float  $weight
2951
     * @param string $pass_percentage
2952
     *
2953
     * @return string
2954
     */
2955
    public static function showSuccessMessage($score, $weight, $pass_percentage)
2956
    {
2957
        $res = '';
2958
        if (self::isPassPercentageEnabled($pass_percentage)) {
2959
            $isSuccess = self::isSuccessExerciseResult(
2960
                $score,
2961
                $weight,
2962
                $pass_percentage
2963
            );
2964
2965
            if ($isSuccess) {
2966
                $html = get_lang('Congratulations you passed the test!');
2967
                $icon = Display::return_icon(
2968
                    'completed.png',
2969
                    get_lang('Correct'),
2970
                    [],
2971
                    ICON_SIZE_MEDIUM
2972
                );
2973
            } else {
2974
                $html = get_lang('You didn\'t reach the minimum score');
2975
                $icon = Display::return_icon(
2976
                    'warning.png',
2977
                    get_lang('Wrong'),
2978
                    [],
2979
                    ICON_SIZE_MEDIUM
2980
                );
2981
            }
2982
            $html = Display::tag('h4', $html);
2983
            $html .= Display::tag(
2984
                'h5',
2985
                $icon,
2986
                ['style' => 'width:40px; padding:2px 10px 0px 0px']
2987
            );
2988
            $res = $html;
2989
        }
2990
2991
        return $res;
2992
    }
2993
2994
    /**
2995
     * Return true if pass_pourcentage activated (we use the pass pourcentage feature
2996
     * return false if pass_percentage = 0 (we don't use the pass pourcentage feature.
2997
     *
2998
     * @param $value
2999
     *
3000
     * @return bool
3001
     *              In this version, pass_percentage and show_success_message are disabled if
3002
     *              pass_percentage is set to 0
3003
     */
3004
    public static function isPassPercentageEnabled($value)
3005
    {
3006
        return $value > 0;
3007
    }
3008
3009
    /**
3010
     * Converts a numeric value in a percentage example 0.66666 to 66.67 %.
3011
     *
3012
     * @param $value
3013
     *
3014
     * @return float Converted number
3015
     */
3016
    public static function convert_to_percentage($value)
3017
    {
3018
        $return = '-';
3019
        if ('' != $value) {
3020
            $return = float_format($value * 100, 1).' %';
3021
        }
3022
3023
        return $return;
3024
    }
3025
3026
    /**
3027
     * Getting all active exercises from a course from a session
3028
     * (if a session_id is provided we will show all the exercises in the course +
3029
     * all exercises in the session).
3030
     *
3031
     * @param array  $course_info
3032
     * @param int    $session_id
3033
     * @param bool   $check_publication_dates
3034
     * @param string $search                  Search exercise name
3035
     * @param bool   $search_all_sessions     Search exercises in all sessions
3036
     * @param   int     0 = only inactive exercises
0 ignored issues
show
Documentation Bug introduced by
The doc comment 0 at position 0 could not be parsed: Unknown type name '0' at position 0 in 0.
Loading history...
3037
     *                  1 = only active exercises,
3038
     *                  2 = all exercises
3039
     *                  3 = active <> -1
3040
     *
3041
     * @return CQuiz[]
3042
     */
3043
    public static function get_all_exercises(
3044
        $course_info = null,
3045
        $session_id = 0,
3046
        $check_publication_dates = false,
3047
        $search = '',
3048
        $search_all_sessions = false,
3049
        $active = 2
3050
    ) {
3051
        $course_id = api_get_course_int_id();
3052
        if (!empty($course_info) && !empty($course_info['real_id'])) {
3053
            $course_id = $course_info['real_id'];
3054
        }
3055
3056
        if (-1 == $session_id) {
3057
            $session_id = 0;
3058
        }
3059
        $course = api_get_course_entity($course_id);
3060
        $session = api_get_session_entity($session_id);
3061
3062
        if (null === $course) {
3063
            return [];
3064
        }
3065
3066
        $repo = Container::getQuizRepository();
3067
3068
        return $repo->findAllByCourse($course, $session, (string) $search, $active);
3069
3070
        // Show courses by active status
3071
        /*if (true == $search_all_sessions) {
3072
            $conditions = [
3073
                'where' => [
3074
                    $active_sql.' c_id = ? '.$needle_where.$timeConditions => [
3075
                        $course_id,
3076
                        $needle,
3077
                    ],
3078
                ],
3079
                'order' => 'title',
3080
            ];
3081
        } else {
3082
            if (empty($session_id)) {
3083
                $conditions = [
3084
                    'where' => [
3085
                        $active_sql.' (session_id = 0 OR session_id IS NULL) AND c_id = ? '.$needle_where.$timeConditions => [
3086
                            $course_id,
3087
                            $needle,
3088
                        ],
3089
                    ],
3090
                    'order' => 'title',
3091
                ];
3092
            } else {
3093
                $conditions = [
3094
                    'where' => [
3095
                        $active_sql.' (session_id = 0 OR session_id IS NULL OR session_id = ? ) AND c_id = ? '.$needle_where.$timeConditions => [
3096
                            $session_id,
3097
                            $course_id,
3098
                            $needle,
3099
                        ],
3100
                    ],
3101
                    'order' => 'title',
3102
                ];
3103
            }
3104
        }
3105
3106
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
3107
3108
        return Database::select('*', $table, $conditions);*/
3109
    }
3110
3111
    /**
3112
     * Getting all exercises (active only or all)
3113
     * from a course from a session
3114
     * (if a session_id is provided we will show all the exercises in the
3115
     * course + all exercises in the session).
3116
     *
3117
     * @param   array   course data
3118
     * @param   int     session id
3119
     * @param    int        course c_id
3120
     * @param bool $only_active_exercises
3121
     *
3122
     * @return array array with exercise data
3123
     *               modified by Hubert Borderiou
3124
     */
3125
    public static function get_all_exercises_for_course_id(
3126
        $course_info = null,
3127
        $session_id = 0,
3128
        $course_id = 0,
3129
        $only_active_exercises = true
3130
    ) {
3131
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
3132
3133
        if ($only_active_exercises) {
3134
            // Only active exercises.
3135
            $sql_active_exercises = "active = 1 AND ";
3136
        } else {
3137
            // Not only active means visible and invisible NOT deleted (-2)
3138
            $sql_active_exercises = "active IN (1, 0) AND ";
3139
        }
3140
3141
        if (-1 == $session_id) {
3142
            $session_id = 0;
3143
        }
3144
3145
        $params = [
3146
            $session_id,
3147
            $course_id,
3148
        ];
3149
3150
        if (empty($session_id)) {
3151
            $conditions = [
3152
                'where' => ["$sql_active_exercises (session_id = 0 OR session_id IS NULL) AND c_id = ?" => [$course_id]],
3153
                'order' => 'title',
3154
            ];
3155
        } else {
3156
            // All exercises
3157
            $conditions = [
3158
                'where' => ["$sql_active_exercises (session_id = 0 OR session_id IS NULL OR session_id = ? ) AND c_id=?" => $params],
3159
                'order' => 'title',
3160
            ];
3161
        }
3162
3163
        return Database::select('*', $table, $conditions);
3164
    }
3165
3166
    /**
3167
     * Gets the position of the score based in a given score (result/weight)
3168
     * and the exe_id based in the user list
3169
     * (NO Exercises in LPs ).
3170
     *
3171
     * @param float  $my_score      user score to be compared *attention*
3172
     *                              $my_score = score/weight and not just the score
3173
     * @param int    $my_exe_id     exe id of the exercise
3174
     *                              (this is necessary because if 2 students have the same score the one
3175
     *                              with the minor exe_id will have a best position, just to be fair and FIFO)
3176
     * @param int    $exercise_id
3177
     * @param string $course_code
3178
     * @param int    $session_id
3179
     * @param array  $user_list
3180
     * @param bool   $return_string
3181
     *
3182
     * @return int the position of the user between his friends in a course
3183
     *             (or course within a session)
3184
     */
3185
    public static function get_exercise_result_ranking(
3186
        $my_score,
3187
        $my_exe_id,
3188
        $exercise_id,
3189
        $course_code,
3190
        $session_id = 0,
3191
        $user_list = [],
3192
        $return_string = true
3193
    ) {
3194
        //No score given we return
3195
        if (is_null($my_score)) {
3196
            return '-';
3197
        }
3198
        if (empty($user_list)) {
3199
            return '-';
3200
        }
3201
3202
        $best_attempts = [];
3203
        foreach ($user_list as $user_data) {
3204
            $user_id = $user_data['user_id'];
3205
            $best_attempts[$user_id] = self::get_best_attempt_by_user(
3206
                $user_id,
3207
                $exercise_id,
3208
                $course_code,
3209
                $session_id
3210
            );
3211
        }
3212
3213
        if (empty($best_attempts)) {
3214
            return 1;
3215
        } else {
3216
            $position = 1;
3217
            $my_ranking = [];
3218
            foreach ($best_attempts as $user_id => $result) {
3219
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3220
                    $my_ranking[$user_id] = $result['score'] / $result['max_score'];
3221
                } else {
3222
                    $my_ranking[$user_id] = 0;
3223
                }
3224
            }
3225
            //if (!empty($my_ranking)) {
3226
            asort($my_ranking);
3227
            $position = count($my_ranking);
3228
            if (!empty($my_ranking)) {
3229
                foreach ($my_ranking as $user_id => $ranking) {
3230
                    if ($my_score >= $ranking) {
3231
                        if ($my_score == $ranking && isset($best_attempts[$user_id]['exe_id'])) {
3232
                            $exe_id = $best_attempts[$user_id]['exe_id'];
3233
                            if ($my_exe_id < $exe_id) {
3234
                                $position--;
3235
                            }
3236
                        } else {
3237
                            $position--;
3238
                        }
3239
                    }
3240
                }
3241
            }
3242
            //}
3243
            $return_value = [
3244
                'position' => $position,
3245
                'count' => count($my_ranking),
3246
            ];
3247
3248
            if ($return_string) {
3249
                if (!empty($position) && !empty($my_ranking)) {
3250
                    $return_value = $position.'/'.count($my_ranking);
3251
                } else {
3252
                    $return_value = '-';
3253
                }
3254
            }
3255
3256
            return $return_value;
3257
        }
3258
    }
3259
3260
    /**
3261
     * Gets the position of the score based in a given score (result/weight) and the exe_id based in all attempts
3262
     * (NO Exercises in LPs ) old functionality by attempt.
3263
     *
3264
     * @param   float   user score to be compared attention => score/weight
3265
     * @param   int     exe id of the exercise
3266
     * (this is necessary because if 2 students have the same score the one
3267
     * with the minor exe_id will have a best position, just to be fair and FIFO)
3268
     * @param   int     exercise id
3269
     * @param   string  course code
3270
     * @param   int     session id
3271
     * @param bool $return_string
3272
     *
3273
     * @return int the position of the user between his friends in a course (or course within a session)
3274
     */
3275
    public static function get_exercise_result_ranking_by_attempt(
3276
        $my_score,
3277
        $my_exe_id,
3278
        $exercise_id,
3279
        $courseId,
3280
        $session_id = 0,
3281
        $return_string = true
3282
    ) {
3283
        if (empty($session_id)) {
3284
            $session_id = 0;
3285
        }
3286
        if (is_null($my_score)) {
3287
            return '-';
3288
        }
3289
        $user_results = Event::get_all_exercise_results(
3290
            $exercise_id,
3291
            $courseId,
3292
            $session_id,
3293
            false
3294
        );
3295
        $position_data = [];
3296
        if (empty($user_results)) {
3297
            return 1;
3298
        } else {
3299
            $position = 1;
3300
            $my_ranking = [];
3301
            foreach ($user_results as $result) {
3302
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3303
                    $my_ranking[$result['exe_id']] = $result['score'] / $result['max_score'];
3304
                } else {
3305
                    $my_ranking[$result['exe_id']] = 0;
3306
                }
3307
            }
3308
            asort($my_ranking);
3309
            $position = count($my_ranking);
3310
            if (!empty($my_ranking)) {
3311
                foreach ($my_ranking as $exe_id => $ranking) {
3312
                    if ($my_score >= $ranking) {
3313
                        if ($my_score == $ranking) {
3314
                            if ($my_exe_id < $exe_id) {
3315
                                $position--;
3316
                            }
3317
                        } else {
3318
                            $position--;
3319
                        }
3320
                    }
3321
                }
3322
            }
3323
            $return_value = [
3324
                'position' => $position,
3325
                'count' => count($my_ranking),
3326
            ];
3327
3328
            if ($return_string) {
3329
                if (!empty($position) && !empty($my_ranking)) {
3330
                    return $position.'/'.count($my_ranking);
3331
                }
3332
            }
3333
3334
            return $return_value;
3335
        }
3336
    }
3337
3338
    /**
3339
     * Get the best attempt in a exercise (NO Exercises in LPs ).
3340
     *
3341
     * @param int $exercise_id
3342
     * @param int $courseId
3343
     * @param int $session_id
3344
     *
3345
     * @return array
3346
     */
3347
    public static function get_best_attempt_in_course($exercise_id, $courseId, $session_id)
3348
    {
3349
        $user_results = Event::get_all_exercise_results(
3350
            $exercise_id,
3351
            $courseId,
3352
            $session_id,
3353
            false
3354
        );
3355
3356
        $best_score_data = [];
3357
        $best_score = 0;
3358
        if (!empty($user_results)) {
3359
            foreach ($user_results as $result) {
3360
                if (!empty($result['max_score']) &&
3361
                    0 != intval($result['max_score'])
3362
                ) {
3363
                    $score = $result['score'] / $result['max_score'];
3364
                    if ($score >= $best_score) {
3365
                        $best_score = $score;
3366
                        $best_score_data = $result;
3367
                    }
3368
                }
3369
            }
3370
        }
3371
3372
        return $best_score_data;
3373
    }
3374
3375
    /**
3376
     * Get the best score in a exercise (NO Exercises in LPs ).
3377
     *
3378
     * @param int $user_id
3379
     * @param int $exercise_id
3380
     * @param int $courseId
3381
     * @param int $session_id
3382
     *
3383
     * @return array
3384
     */
3385
    public static function get_best_attempt_by_user(
3386
        $user_id,
3387
        $exercise_id,
3388
        $courseId,
3389
        $session_id
3390
    ) {
3391
        $user_results = Event::get_all_exercise_results(
3392
            $exercise_id,
3393
            $courseId,
3394
            $session_id,
3395
            false,
3396
            $user_id
3397
        );
3398
        $best_score_data = [];
3399
        $best_score = 0;
3400
        if (!empty($user_results)) {
3401
            foreach ($user_results as $result) {
3402
                if (!empty($result['max_score']) && 0 != (float) $result['max_score']) {
3403
                    $score = $result['score'] / $result['max_score'];
3404
                    if ($score >= $best_score) {
3405
                        $best_score = $score;
3406
                        $best_score_data = $result;
3407
                    }
3408
                }
3409
            }
3410
        }
3411
3412
        return $best_score_data;
3413
    }
3414
3415
    /**
3416
     * Get average score (NO Exercises in LPs ).
3417
     *
3418
     * @param    int    exercise id
3419
     * @param int $courseId
3420
     * @param    int    session id
3421
     *
3422
     * @return float Average score
3423
     */
3424
    public static function get_average_score($exercise_id, $courseId, $session_id)
3425
    {
3426
        $user_results = Event::get_all_exercise_results(
3427
            $exercise_id,
3428
            $courseId,
3429
            $session_id
3430
        );
3431
        $avg_score = 0;
3432
        if (!empty($user_results)) {
3433
            foreach ($user_results as $result) {
3434
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3435
                    $score = $result['score'] / $result['max_score'];
3436
                    $avg_score += $score;
3437
                }
3438
            }
3439
            $avg_score = float_format($avg_score / count($user_results), 1);
3440
        }
3441
3442
        return $avg_score;
3443
    }
3444
3445
    /**
3446
     * Get average score by score (NO Exercises in LPs ).
3447
     *
3448
     * @param int $courseId
3449
     * @param    int    session id
3450
     *
3451
     * @return float Average score
3452
     */
3453
    public static function get_average_score_by_course($courseId, $session_id)
3454
    {
3455
        $user_results = Event::get_all_exercise_results_by_course(
3456
            $courseId,
3457
            $session_id,
3458
            false
3459
        );
3460
        $avg_score = 0;
3461
        if (!empty($user_results)) {
3462
            foreach ($user_results as $result) {
3463
                if (!empty($result['max_score']) && 0 != intval(
3464
                        $result['max_score']
3465
                    )
3466
                ) {
3467
                    $score = $result['score'] / $result['max_score'];
3468
                    $avg_score += $score;
3469
                }
3470
            }
3471
            // We assume that all max_score
3472
            $avg_score = $avg_score / count($user_results);
3473
        }
3474
3475
        return $avg_score;
3476
    }
3477
3478
    /**
3479
     * @param int $user_id
3480
     * @param int $courseId
3481
     * @param int $session_id
3482
     *
3483
     * @return float|int
3484
     */
3485
    public static function get_average_score_by_course_by_user(
3486
        $user_id,
3487
        $courseId,
3488
        $session_id
3489
    ) {
3490
        $user_results = Event::get_all_exercise_results_by_user(
3491
            $user_id,
3492
            $courseId,
3493
            $session_id
3494
        );
3495
        $avg_score = 0;
3496
        if (!empty($user_results)) {
3497
            foreach ($user_results as $result) {
3498
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3499
                    $score = $result['score'] / $result['max_score'];
3500
                    $avg_score += $score;
3501
                }
3502
            }
3503
            // We assume that all max_score
3504
            $avg_score = ($avg_score / count($user_results));
3505
        }
3506
3507
        return $avg_score;
3508
    }
3509
3510
    /**
3511
     * Get average score by score (NO Exercises in LPs ).
3512
     *
3513
     * @param int $exercise_id
3514
     * @param int $courseId
3515
     * @param int $session_id
3516
     * @param int $user_count
3517
     *
3518
     * @return float Best average score
3519
     */
3520
    public static function get_best_average_score_by_exercise(
3521
        $exercise_id,
3522
        $courseId,
3523
        $session_id,
3524
        $user_count
3525
    ) {
3526
        $user_results = Event::get_best_exercise_results_by_user(
3527
            $exercise_id,
3528
            $courseId,
3529
            $session_id
3530
        );
3531
        $avg_score = 0;
3532
        if (!empty($user_results)) {
3533
            foreach ($user_results as $result) {
3534
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3535
                    $score = $result['score'] / $result['max_score'];
3536
                    $avg_score += $score;
3537
                }
3538
            }
3539
            // We asumme that all max_score
3540
            if (!empty($user_count)) {
3541
                $avg_score = float_format($avg_score / $user_count, 1) * 100;
3542
            } else {
3543
                $avg_score = 0;
3544
            }
3545
        }
3546
3547
        return $avg_score;
3548
    }
3549
3550
    /**
3551
     * Get average score by score (NO Exercises in LPs ).
3552
     *
3553
     * @param int $exercise_id
3554
     * @param int $courseId
3555
     * @param int $session_id
3556
     *
3557
     * @return float Best average score
3558
     */
3559
    public static function getBestScoreByExercise(
3560
        $exercise_id,
3561
        $courseId,
3562
        $session_id
3563
    ) {
3564
        $user_results = Event::get_best_exercise_results_by_user(
3565
            $exercise_id,
3566
            $courseId,
3567
            $session_id
3568
        );
3569
        $avg_score = 0;
3570
        if (!empty($user_results)) {
3571
            foreach ($user_results as $result) {
3572
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3573
                    $score = $result['score'] / $result['max_score'];
3574
                    $avg_score += $score;
3575
                }
3576
            }
3577
        }
3578
3579
        return $avg_score;
3580
    }
3581
3582
    /**
3583
     * Get student results (only in completed exercises) stats by question.
3584
     *
3585
     * @param int    $question_id
3586
     * @param int    $exercise_id
3587
     * @param string $course_code
3588
     * @param int    $session_id
3589
     * @param bool   $onlyStudent Filter only enrolled students
3590
     *
3591
     * @return array
3592
     */
3593
    public static function get_student_stats_by_question(
3594
        $question_id,
3595
        $exercise_id,
3596
        $course_code,
3597
        $session_id,
3598
        $onlyStudent = false
3599
    ) {
3600
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3601
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3602
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3603
3604
        $question_id = (int) $question_id;
3605
        $exercise_id = (int) $exercise_id;
3606
        $course_code = Database::escape_string($course_code);
3607
        $session_id = (int) $session_id;
3608
        $courseId = api_get_course_int_id($course_code);
3609
3610
        $sql = "SELECT MAX(marks) as max, MIN(marks) as min, AVG(marks) as average
3611
                FROM $track_exercises e ";
3612
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
3613
        if (true == $onlyStudent) {
3614
            $courseCondition = '';
3615
            if (empty($session_id)) {
3616
                $courseCondition = "
3617
                INNER JOIN $courseUser c
3618
                ON (
3619
                    e.exe_user_id = c.user_id AND
3620
                    e.c_id = c.c_id AND
3621
                    c.status = ".STUDENT." AND
3622
                    relation_type <> 2
3623
                )";
3624
            } else {
3625
                $sessionRelCourse = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3626
                $courseCondition = "
3627
            INNER JOIN $sessionRelCourse sc
3628
            ON (
3629
                        e.exe_user_id = sc.user_id AND
3630
                        e.c_id = sc.c_id AND
3631
                        e.session_id = sc.session_id AND
3632
                        sc.status = ".SessionEntity::STUDENT."
3633
                )";
3634
            }
3635
            $sql .= $courseCondition;
3636
        }
3637
        $sql .= "
3638
    		INNER JOIN $track_attempt a
3639
    		ON (
3640
    		    a.exe_id = e.exe_id
3641
            )
3642
    		WHERE
3643
    		    exe_exo_id 	= $exercise_id AND
3644
                a.c_id = $courseId AND
3645
                question_id = $question_id AND
3646
                e.status = ''
3647
                $sessionCondition
3648
            LIMIT 1";
3649
        $result = Database::query($sql);
3650
        $return = [];
3651
        if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
3652
            $return = Database::fetch_array($result, 'ASSOC');
3653
        }
3654
3655
        return $return;
3656
    }
3657
3658
    /**
3659
     * Get the correct answer count for a fill blanks question.
3660
     *
3661
     * @param int $question_id
3662
     * @param int $exercise_id
3663
     *
3664
     * @return array
3665
     */
3666
    public static function getNumberStudentsFillBlanksAnswerCount(
3667
        $question_id,
3668
        $exercise_id
3669
    ) {
3670
        $listStudentsId = [];
3671
        $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
3672
            api_get_course_id(),
3673
            true
3674
        );
3675
        foreach ($listAllStudentInfo as $i => $listStudentInfo) {
3676
            $listStudentsId[] = $listStudentInfo['user_id'];
3677
        }
3678
3679
        $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3680
            $exercise_id,
3681
            $question_id,
3682
            $listStudentsId,
3683
            '1970-01-01',
3684
            '3000-01-01'
3685
        );
3686
3687
        $arrayCount = [];
3688
3689
        foreach ($listFillTheBlankResult as $resultCount) {
3690
            foreach ($resultCount as $index => $count) {
3691
                //this is only for declare the array index per answer
3692
                $arrayCount[$index] = 0;
3693
            }
3694
        }
3695
3696
        foreach ($listFillTheBlankResult as $resultCount) {
3697
            foreach ($resultCount as $index => $count) {
3698
                $count = (0 === $count) ? 1 : 0;
3699
                $arrayCount[$index] += $count;
3700
            }
3701
        }
3702
3703
        return $arrayCount;
3704
    }
3705
3706
    /**
3707
     * Get the number of questions with answers.
3708
     *
3709
     * @param int    $question_id
3710
     * @param int    $exercise_id
3711
     * @param string $course_code
3712
     * @param int    $session_id
3713
     * @param string $questionType
3714
     *
3715
     * @return int
3716
     */
3717
    public static function get_number_students_question_with_answer_count(
3718
        $question_id,
3719
        $exercise_id,
3720
        $course_code,
3721
        $session_id,
3722
        $questionType = ''
3723
    ) {
3724
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3725
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3726
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3727
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
3728
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3729
3730
        $question_id = intval($question_id);
3731
        $exercise_id = intval($exercise_id);
3732
        $courseId = api_get_course_int_id($course_code);
3733
        $session_id = intval($session_id);
3734
3735
        if (FILL_IN_BLANKS == $questionType) {
3736
            $listStudentsId = [];
3737
            $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
3738
                api_get_course_id(),
3739
                true
3740
            );
3741
            foreach ($listAllStudentInfo as $i => $listStudentInfo) {
3742
                $listStudentsId[] = $listStudentInfo['user_id'];
3743
            }
3744
3745
            $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3746
                $exercise_id,
3747
                $question_id,
3748
                $listStudentsId,
3749
                '1970-01-01',
3750
                '3000-01-01'
3751
            );
3752
3753
            return FillBlanks::getNbResultFillBlankAll($listFillTheBlankResult);
3754
        }
3755
3756
        if (empty($session_id)) {
3757
            $courseCondition = "
3758
            INNER JOIN $courseUser cu
3759
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
3760
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3761
        } else {
3762
            $courseCondition = "
3763
            INNER JOIN $courseUserSession cu
3764
            ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
3765
            $courseConditionWhere = " AND cu.status = ".SessionEntity::STUDENT;
3766
        }
3767
3768
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
3769
        $sql = "SELECT DISTINCT exe_user_id
3770
                FROM $track_exercises e
3771
                INNER JOIN $track_attempt a
3772
                ON (
3773
                    a.exe_id = e.exe_id
3774
                )
3775
                INNER JOIN $courseTable c
3776
                ON (c.id = e.c_id)
3777
                $courseCondition
3778
                WHERE
3779
                    exe_exo_id = $exercise_id AND
3780
                    e.c_id = $courseId AND
3781
                    question_id = $question_id AND
3782
                    answer <> '0' AND
3783
                    e.status = ''
3784
                    $courseConditionWhere
3785
                    $sessionCondition
3786
            ";
3787
        $result = Database::query($sql);
3788
        $return = 0;
3789
        if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
3790
            $return = Database::num_rows($result);
3791
        }
3792
3793
        return $return;
3794
    }
3795
3796
    /**
3797
     * Get number of answers to hotspot questions.
3798
     *
3799
     * @param int    $answer_id
3800
     * @param int    $question_id
3801
     * @param int    $exercise_id
3802
     * @param string $courseId
3803
     * @param int    $session_id
3804
     *
3805
     * @return int
3806
     */
3807
    public static function get_number_students_answer_hotspot_count(
3808
        $answer_id,
3809
        $question_id,
3810
        $exercise_id,
3811
        $courseId,
3812
        $session_id
3813
    ) {
3814
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3815
        $track_hotspot = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
3816
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3817
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
3818
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3819
3820
        $question_id = (int) $question_id;
3821
        $answer_id = (int) $answer_id;
3822
        $exercise_id = (int) $exercise_id;
3823
        $courseId = (int) $courseId;
3824
        $session_id = (int) $session_id;
3825
3826
        if (empty($session_id)) {
3827
            $courseCondition = "
3828
            INNER JOIN $courseUser cu
3829
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
3830
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3831
        } else {
3832
            $courseCondition = "
3833
            INNER JOIN $courseUserSession cu
3834
            ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
3835
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
3836
        }
3837
3838
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
3839
        $sql = "SELECT DISTINCT exe_user_id
3840
                FROM $track_exercises e
3841
                INNER JOIN $track_hotspot a
3842
                ON (a.hotspot_exe_id = e.exe_id)
3843
                INNER JOIN $courseTable c
3844
                ON (a.c_id = c.id)
3845
                $courseCondition
3846
                WHERE
3847
                    exe_exo_id              = $exercise_id AND
3848
                    a.c_id 	= $courseId AND
3849
                    hotspot_answer_id       = $answer_id AND
3850
                    hotspot_question_id     = $question_id AND
3851
                    hotspot_correct         =  1 AND
3852
                    e.status                = ''
3853
                    $courseConditionWhere
3854
                    $sessionCondition
3855
            ";
3856
3857
        $result = Database::query($sql);
3858
        $return = 0;
3859
        if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
3860
            $return = Database::num_rows($result);
3861
        }
3862
3863
        return $return;
3864
    }
3865
3866
    /**
3867
     * @param int    $answer_id
3868
     * @param int    $question_id
3869
     * @param int    $exercise_id
3870
     * @param string $course_code
3871
     * @param int    $session_id
3872
     * @param string $question_type
3873
     * @param string $correct_answer
3874
     * @param string $current_answer
3875
     *
3876
     * @return int
3877
     */
3878
    public static function get_number_students_answer_count(
3879
        $answer_id,
3880
        $question_id,
3881
        $exercise_id,
3882
        $course_code,
3883
        $session_id,
3884
        $question_type = null,
3885
        $correct_answer = null,
3886
        $current_answer = null
3887
    ) {
3888
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3889
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3890
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
3891
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3892
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3893
3894
        $question_id = (int) $question_id;
3895
        $answer_id = (int) $answer_id;
3896
        $exercise_id = (int) $exercise_id;
3897
        $courseId = api_get_course_int_id($course_code);
3898
        $session_id = (int) $session_id;
3899
3900
        switch ($question_type) {
3901
            case FILL_IN_BLANKS:
3902
                $answer_condition = '';
3903
                $select_condition = ' e.exe_id, answer ';
3904
                break;
3905
            case MATCHING:
3906
            case MATCHING_DRAGGABLE:
3907
            default:
3908
                $answer_condition = " answer = $answer_id AND ";
3909
                $select_condition = ' DISTINCT exe_user_id ';
3910
        }
3911
3912
        if (empty($session_id)) {
3913
            $courseCondition = "
3914
            INNER JOIN $courseUser cu
3915
            ON cu.c_id = c.id AND cu.user_id = exe_user_id";
3916
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3917
        } else {
3918
            $courseCondition = "
3919
            INNER JOIN $courseUserSession cu
3920
            ON (cu.c_id = a.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
3921
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
3922
        }
3923
3924
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
3925
        $sql = "SELECT $select_condition
3926
                FROM $track_exercises e
3927
                INNER JOIN $track_attempt a
3928
                ON (
3929
                    a.exe_id = e.exe_id
3930
                )
3931
                INNER JOIN $courseTable c
3932
                ON c.id = a.c_id
3933
                $courseCondition
3934
                WHERE
3935
                    exe_exo_id = $exercise_id AND
3936
                    e.c_id = $courseId AND
3937
                    $answer_condition
3938
                    question_id = $question_id AND
3939
                    e.status = ''
3940
                    $courseConditionWhere
3941
                    $sessionCondition
3942
            ";
3943
        $result = Database::query($sql);
3944
        $return = 0;
3945
        if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
3946
            $good_answers = 0;
3947
            switch ($question_type) {
3948
                case FILL_IN_BLANKS:
3949
                    while ($row = Database::fetch_array($result, 'ASSOC')) {
3950
                        $fill_blank = self::check_fill_in_blanks(
3951
                            $correct_answer,
3952
                            $row['answer'],
3953
                            $current_answer
3954
                        );
3955
                        if (isset($fill_blank[$current_answer]) && 1 == $fill_blank[$current_answer]) {
3956
                            $good_answers++;
3957
                        }
3958
                    }
3959
3960
                    return $good_answers;
3961
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

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

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

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

Loading history...
3962
                case MATCHING:
3963
                case MATCHING_DRAGGABLE:
3964
                default:
3965
                    $return = Database::num_rows($result);
3966
            }
3967
        }
3968
3969
        return $return;
3970
    }
3971
3972
    /**
3973
     * @param array  $answer
3974
     * @param string $user_answer
3975
     *
3976
     * @return array
3977
     */
3978
    public static function check_fill_in_blanks($answer, $user_answer, $current_answer)
3979
    {
3980
        // the question is encoded like this
3981
        // [A] B [C] D [E] F::10,10,10@1
3982
        // number 1 before the "@" means that is a switchable fill in blank question
3983
        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
3984
        // means that is a normal fill blank question
3985
        // first we explode the "::"
3986
        $pre_array = explode('::', $answer);
3987
        // is switchable fill blank or not
3988
        $last = count($pre_array) - 1;
3989
        $is_set_switchable = explode('@', $pre_array[$last]);
3990
        $switchable_answer_set = false;
3991
        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
3992
            $switchable_answer_set = true;
3993
        }
3994
        $answer = '';
3995
        for ($k = 0; $k < $last; $k++) {
3996
            $answer .= $pre_array[$k];
3997
        }
3998
        // splits weightings that are joined with a comma
3999
        $answerWeighting = explode(',', $is_set_switchable[0]);
4000
4001
        // we save the answer because it will be modified
4002
        //$temp = $answer;
4003
        $temp = $answer;
4004
4005
        $answer = '';
4006
        $j = 0;
4007
        //initialise answer tags
4008
        $user_tags = $correct_tags = $real_text = [];
4009
        // the loop will stop at the end of the text
4010
        while (1) {
4011
            // quits the loop if there are no more blanks (detect '[')
4012
            if (false === ($pos = api_strpos($temp, '['))) {
4013
                // adds the end of the text
4014
                $answer = $temp;
4015
                $real_text[] = $answer;
4016
                break; //no more "blanks", quit the loop
4017
            }
4018
            // adds the piece of text that is before the blank
4019
            //and ends with '[' into a general storage array
4020
            $real_text[] = api_substr($temp, 0, $pos + 1);
4021
            $answer .= api_substr($temp, 0, $pos + 1);
4022
            //take the string remaining (after the last "[" we found)
4023
            $temp = api_substr($temp, $pos + 1);
4024
            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4025
            if (false === ($pos = api_strpos($temp, ']'))) {
4026
                // adds the end of the text
4027
                $answer .= $temp;
4028
                break;
4029
            }
4030
4031
            $str = $user_answer;
4032
4033
            preg_match_all('#\[([^[]*)\]#', $str, $arr);
4034
            $str = str_replace('\r\n', '', $str);
4035
            $choices = $arr[1];
4036
            $choice = [];
4037
            $check = false;
4038
            $i = 0;
4039
            foreach ($choices as $item) {
4040
                if ($current_answer === $item) {
4041
                    $check = true;
4042
                }
4043
                if ($check) {
4044
                    $choice[] = $item;
4045
                    $i++;
4046
                }
4047
                if (3 == $i) {
4048
                    break;
4049
                }
4050
            }
4051
            $tmp = api_strrpos($choice[$j], ' / ');
4052
4053
            if (false !== $tmp) {
4054
                $choice[$j] = api_substr($choice[$j], 0, $tmp);
4055
            }
4056
4057
            $choice[$j] = trim($choice[$j]);
4058
4059
            //Needed to let characters ' and " to work as part of an answer
4060
            $choice[$j] = stripslashes($choice[$j]);
4061
4062
            $user_tags[] = api_strtolower($choice[$j]);
4063
            //put the contents of the [] answer tag into correct_tags[]
4064
            $correct_tags[] = api_strtolower(api_substr($temp, 0, $pos));
4065
            $j++;
4066
            $temp = api_substr($temp, $pos + 1);
4067
        }
4068
4069
        $answer = '';
4070
        $real_correct_tags = $correct_tags;
4071
        $chosen_list = [];
4072
        $good_answer = [];
4073
4074
        for ($i = 0; $i < count($real_correct_tags); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4075
            if (!$switchable_answer_set) {
4076
                //needed to parse ' and " characters
4077
                $user_tags[$i] = stripslashes($user_tags[$i]);
4078
                if ($correct_tags[$i] == $user_tags[$i]) {
4079
                    $good_answer[$correct_tags[$i]] = 1;
4080
                } elseif (!empty($user_tags[$i])) {
4081
                    $good_answer[$correct_tags[$i]] = 0;
4082
                } else {
4083
                    $good_answer[$correct_tags[$i]] = 0;
4084
                }
4085
            } else {
4086
                // switchable fill in the blanks
4087
                if (in_array($user_tags[$i], $correct_tags)) {
4088
                    $correct_tags = array_diff($correct_tags, $chosen_list);
4089
                    $good_answer[$correct_tags[$i]] = 1;
4090
                } elseif (!empty($user_tags[$i])) {
4091
                    $good_answer[$correct_tags[$i]] = 0;
4092
                } else {
4093
                    $good_answer[$correct_tags[$i]] = 0;
4094
                }
4095
            }
4096
            // adds the correct word, followed by ] to close the blank
4097
            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4098
            if (isset($real_text[$i + 1])) {
4099
                $answer .= $real_text[$i + 1];
4100
            }
4101
        }
4102
4103
        return $good_answer;
4104
    }
4105
4106
    /**
4107
     * Return an HTML select menu with the student groups.
4108
     *
4109
     * @param string $name     is the name and the id of the <select>
4110
     * @param string $default  default value for option
4111
     * @param string $onchange
4112
     *
4113
     * @return string the html code of the <select>
4114
     */
4115
    public static function displayGroupMenu($name, $default, $onchange = "")
4116
    {
4117
        // check the default value of option
4118
        $tabSelected = [$default => " selected='selected' "];
4119
        $res = "<select name='$name' id='$name' onchange='".$onchange."' >";
4120
        $res .= "<option value='-1'".$tabSelected["-1"].">-- ".get_lang('AllGroups')." --</option>";
4121
        $res .= "<option value='0'".$tabSelected["0"].">- ".get_lang('NotInAGroup')." -</option>";
4122
        $groups = GroupManager::get_group_list();
4123
        $currentCatId = 0;
4124
        $countGroups = count($groups);
4125
        for ($i = 0; $i < $countGroups; $i++) {
4126
            $category = GroupManager::get_category_from_group($groups[$i]['iid']);
4127
            if ($category['id'] != $currentCatId) {
4128
                $res .= "<option value='-1' disabled='disabled'>".$category['title']."</option>";
4129
                $currentCatId = $category['id'];
4130
            }
4131
            $res .= "<option ".$tabSelected[$groups[$i]['id']]."style='margin-left:40px' value='".
4132
                $groups[$i]["iid"]."'>".
4133
                $groups[$i]["name"].
4134
                "</option>";
4135
        }
4136
        $res .= "</select>";
4137
4138
        return $res;
4139
    }
4140
4141
    /**
4142
     * @param int $exe_id
4143
     */
4144
    public static function create_chat_exercise_session($exe_id)
4145
    {
4146
        if (!isset($_SESSION['current_exercises'])) {
4147
            $_SESSION['current_exercises'] = [];
4148
        }
4149
        $_SESSION['current_exercises'][$exe_id] = true;
4150
    }
4151
4152
    /**
4153
     * @param int $exe_id
4154
     */
4155
    public static function delete_chat_exercise_session($exe_id)
4156
    {
4157
        if (isset($_SESSION['current_exercises'])) {
4158
            $_SESSION['current_exercises'][$exe_id] = false;
4159
        }
4160
    }
4161
4162
    /**
4163
     * Display the exercise results.
4164
     *
4165
     * @param Exercise $objExercise
4166
     * @param int      $exeId
4167
     * @param bool     $save_user_result save users results (true) or just show the results (false)
4168
     * @param string   $remainingMessage
4169
     * @param bool     $allowSignature
4170
     * @param bool     $allowExportPdf
4171
     * @param bool     $isExport
4172
     */
4173
    public static function displayQuestionListByAttempt(
4174
        $objExercise,
4175
        $exeId,
4176
        $save_user_result = false,
4177
        $remainingMessage = '',
4178
        $allowSignature = false,
4179
        $allowExportPdf = false,
4180
        $isExport = false
4181
    ) {
4182
        $origin = api_get_origin();
4183
        $courseId = api_get_course_int_id();
4184
        $courseCode = api_get_course_id();
4185
        $sessionId = api_get_session_id();
4186
4187
        // Getting attempt info
4188
        $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
4189
4190
        // Getting question list
4191
        $question_list = [];
4192
        $studentInfo = [];
4193
        if (!empty($exercise_stat_info['data_tracking'])) {
4194
            $studentInfo = api_get_user_info($exercise_stat_info['exe_user_id']);
4195
            $question_list = explode(',', $exercise_stat_info['data_tracking']);
4196
        } else {
4197
            // Try getting the question list only if save result is off
4198
            if (false == $save_user_result) {
4199
                $question_list = $objExercise->get_validated_question_list();
4200
            }
4201
            if (in_array(
4202
                $objExercise->getFeedbackType(),
4203
                [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4204
            )) {
4205
                $question_list = $objExercise->get_validated_question_list();
4206
            }
4207
        }
4208
4209
        if ($objExercise->getResultAccess()) {
4210
            if (false === $objExercise->hasResultsAccess($exercise_stat_info)) {
4211
                echo Display::return_message(
4212
                    sprintf(get_lang('YouPassedTheLimitOfXMinutesToSeeTheResults'), $objExercise->getResultsAccess())
4213
                );
4214
4215
                return false;
4216
            }
4217
4218
            if (!empty($objExercise->getResultAccess())) {
4219
                $url = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$objExercise->id;
4220
                echo $objExercise->returnTimeLeftDiv();
4221
                echo $objExercise->showSimpleTimeControl(
4222
                    $objExercise->getResultAccessTimeDiff($exercise_stat_info),
4223
                    $url
4224
                );
4225
            }
4226
        }
4227
4228
        $counter = 1;
4229
        $total_score = $total_weight = 0;
4230
        $exercise_content = null;
4231
4232
        // Hide results
4233
        $show_results = false;
4234
        $show_only_score = false;
4235
        if (in_array($objExercise->results_disabled,
4236
            [
4237
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4238
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
4239
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4240
            ]
4241
        )) {
4242
            $show_results = true;
4243
        }
4244
4245
        if (in_array(
4246
            $objExercise->results_disabled,
4247
            [
4248
                RESULT_DISABLE_SHOW_SCORE_ONLY,
4249
                RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES,
4250
                RESULT_DISABLE_RANKING,
4251
            ]
4252
        )
4253
        ) {
4254
            $show_only_score = true;
4255
        }
4256
4257
        // Not display expected answer, but score, and feedback
4258
        $show_all_but_expected_answer = false;
4259
        if (RESULT_DISABLE_SHOW_SCORE_ONLY == $objExercise->results_disabled &&
4260
            EXERCISE_FEEDBACK_TYPE_END == $objExercise->getFeedbackType()
4261
        ) {
4262
            $show_all_but_expected_answer = true;
4263
            $show_results = true;
4264
            $show_only_score = false;
4265
        }
4266
4267
        $showTotalScoreAndUserChoicesInLastAttempt = true;
4268
        $showTotalScore = true;
4269
        $showQuestionScore = true;
4270
        $attemptResult = [];
4271
4272
        if (in_array(
4273
            $objExercise->results_disabled,
4274
            [
4275
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
4276
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
4277
                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
4278
            ])
4279
        ) {
4280
            $show_only_score = true;
4281
            $show_results = true;
4282
            $numberAttempts = 0;
4283
            if ($objExercise->attempts > 0) {
4284
                $attempts = Event::getExerciseResultsByUser(
4285
                    api_get_user_id(),
4286
                    $objExercise->id,
4287
                    $courseId,
4288
                    $sessionId,
4289
                    $exercise_stat_info['orig_lp_id'],
4290
                    $exercise_stat_info['orig_lp_item_id'],
4291
                    'desc'
4292
                );
4293
                if ($attempts) {
4294
                    $numberAttempts = count($attempts);
4295
                }
4296
4297
                if ($save_user_result) {
4298
                    $numberAttempts++;
4299
                }
4300
4301
                $showTotalScore = false;
4302
                if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT == $objExercise->results_disabled) {
4303
                    $showTotalScore = true;
4304
                }
4305
                $showTotalScoreAndUserChoicesInLastAttempt = false;
4306
                if ($numberAttempts >= $objExercise->attempts) {
4307
                    $showTotalScore = true;
4308
                    $show_results = true;
4309
                    $show_only_score = false;
4310
                    $showTotalScoreAndUserChoicesInLastAttempt = true;
4311
                }
4312
4313
                if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $objExercise->results_disabled) {
4314
                    $showTotalScore = true;
4315
                    $show_results = true;
4316
                    $show_only_score = false;
4317
                    $showTotalScoreAndUserChoicesInLastAttempt = false;
4318
                    if ($numberAttempts >= $objExercise->attempts) {
4319
                        $showTotalScoreAndUserChoicesInLastAttempt = true;
4320
                    }
4321
4322
                    // Check if the current attempt is the last.
4323
                    if (false === $save_user_result && !empty($attempts)) {
4324
                        $showTotalScoreAndUserChoicesInLastAttempt = false;
4325
                        $position = 1;
4326
                        foreach ($attempts as $attempt) {
4327
                            if ($exeId == $attempt['exe_id']) {
4328
                                break;
4329
                            }
4330
                            $position++;
4331
                        }
4332
4333
                        if ($position == $objExercise->attempts) {
4334
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
4335
                        }
4336
                    }
4337
                }
4338
            }
4339
4340
            if (RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK ==
4341
                $objExercise->results_disabled
4342
            ) {
4343
                $show_only_score = false;
4344
                $show_results = true;
4345
                $show_all_but_expected_answer = false;
4346
                $showTotalScore = false;
4347
                $showQuestionScore = false;
4348
                if ($numberAttempts >= $objExercise->attempts) {
4349
                    $showTotalScore = true;
4350
                    $showQuestionScore = true;
4351
                }
4352
            }
4353
        }
4354
4355
        // When exporting to PDF hide feedback/comment/score show warning in hotspot.
4356
        if ($allowExportPdf && $isExport) {
4357
            $showTotalScore = false;
4358
            $showQuestionScore = false;
4359
            $objExercise->feedback_type = 2;
4360
            $objExercise->hideComment = true;
4361
            $objExercise->hideNoAnswer = true;
4362
            $objExercise->results_disabled = 0;
4363
            $objExercise->hideExpectedAnswer = true;
4364
            $show_results = true;
4365
        }
4366
4367
        if ('embeddable' !== $origin &&
4368
            !empty($exercise_stat_info['exe_user_id']) &&
4369
            !empty($studentInfo)
4370
        ) {
4371
            // Shows exercise header.
4372
            echo $objExercise->showExerciseResultHeader(
4373
                $studentInfo,
4374
                $exercise_stat_info,
4375
                $save_user_result,
4376
                $allowSignature,
4377
                $allowExportPdf
4378
            );
4379
        }
4380
4381
        // Display text when test is finished #4074 and for LP #4227
4382
        $endOfMessage = $objExercise->getTextWhenFinished();
4383
        if (!empty($endOfMessage)) {
4384
            echo Display::div(
4385
                $endOfMessage,
4386
                ['id' => 'quiz_end_message']
4387
            );
4388
        }
4389
4390
        $question_list_answers = [];
4391
        $category_list = [];
4392
        $loadChoiceFromSession = false;
4393
        $fromDatabase = true;
4394
        $exerciseResult = null;
4395
        $exerciseResultCoordinates = null;
4396
        $delineationResults = null;
4397
        if (true === $save_user_result && in_array(
4398
            $objExercise->getFeedbackType(),
4399
            [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4400
        )) {
4401
            $loadChoiceFromSession = true;
4402
            $fromDatabase = false;
4403
            $exerciseResult = Session::read('exerciseResult');
4404
            $exerciseResultCoordinates = Session::read('exerciseResultCoordinates');
4405
            $delineationResults = Session::read('hotspot_delineation_result');
4406
            $delineationResults = isset($delineationResults[$objExercise->id]) ? $delineationResults[$objExercise->id] : null;
4407
        }
4408
4409
        $countPendingQuestions = 0;
4410
        $result = [];
4411
        // Loop over all question to show results for each of them, one by one
4412
        if (!empty($question_list)) {
4413
            foreach ($question_list as $questionId) {
4414
                // Creates a temporary Question object
4415
                $objQuestionTmp = Question::read($questionId, $objExercise->course);
4416
                // This variable came from exercise_submit_modal.php
4417
                ob_start();
4418
                $choice = null;
4419
                $delineationChoice = null;
4420
                if ($loadChoiceFromSession) {
4421
                    $choice = isset($exerciseResult[$questionId]) ? $exerciseResult[$questionId] : null;
4422
                    $delineationChoice = isset($delineationResults[$questionId]) ? $delineationResults[$questionId] : null;
4423
                }
4424
4425
                // We're inside *one* question. Go through each possible answer for this question
4426
                $result = $objExercise->manage_answer(
4427
                    $exeId,
4428
                    $questionId,
4429
                    $choice,
4430
                    'exercise_result',
4431
                    $exerciseResultCoordinates,
4432
                    $save_user_result,
4433
                    $fromDatabase,
4434
                    $show_results,
4435
                    $objExercise->selectPropagateNeg(),
4436
                    $delineationChoice,
4437
                    $showTotalScoreAndUserChoicesInLastAttempt
4438
                );
4439
4440
                if (empty($result)) {
4441
                    continue;
4442
                }
4443
4444
                $total_score += $result['score'];
4445
                $total_weight += $result['weight'];
4446
4447
                $question_list_answers[] = [
4448
                    'question' => $result['open_question'],
4449
                    'answer' => $result['open_answer'],
4450
                    'answer_type' => $result['answer_type'],
4451
                    'generated_oral_file' => $result['generated_oral_file'],
4452
                ];
4453
4454
                $my_total_score = $result['score'];
4455
                $my_total_weight = $result['weight'];
4456
                $scorePassed = self::scorePassed($my_total_score, $my_total_weight);
4457
4458
                // Category report
4459
                $category_was_added_for_this_test = false;
4460
                if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
4461
                    if (!isset($category_list[$objQuestionTmp->category]['score'])) {
4462
                        $category_list[$objQuestionTmp->category]['score'] = 0;
4463
                    }
4464
                    if (!isset($category_list[$objQuestionTmp->category]['total'])) {
4465
                        $category_list[$objQuestionTmp->category]['total'] = 0;
4466
                    }
4467
                    if (!isset($category_list[$objQuestionTmp->category]['total_questions'])) {
4468
                        $category_list[$objQuestionTmp->category]['total_questions'] = 0;
4469
                    }
4470
                    if (!isset($category_list[$objQuestionTmp->category]['passed'])) {
4471
                        $category_list[$objQuestionTmp->category]['passed'] = 0;
4472
                    }
4473
                    if (!isset($category_list[$objQuestionTmp->category]['wrong'])) {
4474
                        $category_list[$objQuestionTmp->category]['wrong'] = 0;
4475
                    }
4476
                    if (!isset($category_list[$objQuestionTmp->category]['no_answer'])) {
4477
                        $category_list[$objQuestionTmp->category]['no_answer'] = 0;
4478
                    }
4479
4480
                    $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
4481
                    $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
4482
                    if ($scorePassed) {
4483
                        // Only count passed if score is not empty
4484
                        if (!empty($my_total_score)) {
4485
                            $category_list[$objQuestionTmp->category]['passed']++;
4486
                        }
4487
                    } else {
4488
                        if ($result['user_answered']) {
4489
                            $category_list[$objQuestionTmp->category]['wrong']++;
4490
                        } else {
4491
                            $category_list[$objQuestionTmp->category]['no_answer']++;
4492
                        }
4493
                    }
4494
4495
                    $category_list[$objQuestionTmp->category]['total_questions']++;
4496
                    $category_was_added_for_this_test = true;
4497
                }
4498
                if (isset($objQuestionTmp->category_list) && !empty($objQuestionTmp->category_list)) {
4499
                    foreach ($objQuestionTmp->category_list as $category_id) {
4500
                        $category_list[$category_id]['score'] += $my_total_score;
4501
                        $category_list[$category_id]['total'] += $my_total_weight;
4502
                        $category_was_added_for_this_test = true;
4503
                    }
4504
                }
4505
4506
                // No category for this question!
4507
                if (false == $category_was_added_for_this_test) {
4508
                    if (!isset($category_list['none']['score'])) {
4509
                        $category_list['none']['score'] = 0;
4510
                    }
4511
                    if (!isset($category_list['none']['total'])) {
4512
                        $category_list['none']['total'] = 0;
4513
                    }
4514
4515
                    $category_list['none']['score'] += $my_total_score;
4516
                    $category_list['none']['total'] += $my_total_weight;
4517
                }
4518
4519
                if (0 == $objExercise->selectPropagateNeg() && $my_total_score < 0) {
4520
                    $my_total_score = 0;
4521
                }
4522
4523
                $comnt = null;
4524
                if ($show_results) {
4525
                    $comnt = Event::get_comments($exeId, $questionId);
4526
                    $teacherAudio = self::getOralFeedbackAudio(
4527
                        $exeId,
4528
                        $questionId,
4529
                        api_get_user_id()
4530
                    );
4531
4532
                    if (!empty($comnt) || $teacherAudio) {
4533
                        echo '<b>'.get_lang('Feedback').'</b>';
4534
                    }
4535
4536
                    if (!empty($comnt)) {
4537
                        echo self::getFeedbackText($comnt);
4538
                    }
4539
4540
                    if ($teacherAudio) {
4541
                        echo $teacherAudio;
4542
                    }
4543
                }
4544
4545
                $calculatedScore = [
4546
                    'result' => self::show_score(
4547
                        $my_total_score,
4548
                        $my_total_weight,
4549
                        false
4550
                    ),
4551
                    'pass' => $scorePassed,
4552
                    'score' => $my_total_score,
4553
                    'weight' => $my_total_weight,
4554
                    'comments' => $comnt,
4555
                    'user_answered' => $result['user_answered'],
4556
                ];
4557
4558
                $score = [];
4559
                if ($show_results) {
4560
                    $score = $calculatedScore;
4561
                }
4562
                if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION])) {
4563
                    $reviewScore = [
4564
                        'score' => $my_total_score,
4565
                        'comments' => Event::get_comments($exeId, $questionId),
4566
                    ];
4567
                    $check = $objQuestionTmp->isQuestionWaitingReview($reviewScore);
4568
                    if (false === $check) {
4569
                        $countPendingQuestions++;
4570
                    }
4571
                }
4572
4573
                $contents = ob_get_clean();
4574
                $question_content = '';
4575
                if ($show_results) {
4576
                    $question_content = '<div class="question_row_answer">';
4577
                    if (false === $showQuestionScore) {
4578
                        $score = [];
4579
                    }
4580
4581
                    // Shows question title an description
4582
                    $question_content .= $objQuestionTmp->return_header(
4583
                        $objExercise,
4584
                        $counter,
4585
                        $score
4586
                    );
4587
                }
4588
                $counter++;
4589
                $question_content .= $contents;
4590
                if ($show_results) {
4591
                    $question_content .= '</div>';
4592
                }
4593
4594
                $calculatedScore['question_content'] = $question_content;
4595
                $attemptResult[] = $calculatedScore;
4596
4597
                if ($objExercise->showExpectedChoice()) {
4598
                    $exercise_content .= Display::div(
4599
                        Display::panel($question_content),
4600
                        ['class' => 'question-panel']
4601
                    );
4602
                } else {
4603
                    // $show_all_but_expected_answer should not happen at
4604
                    // the same time as $show_results
4605
                    if ($show_results && !$show_only_score) {
4606
                        $exercise_content .= Display::div(
4607
                            Display::panel($question_content),
4608
                            ['class' => 'question-panel']
4609
                        );
4610
                    }
4611
                }
4612
            }
4613
        }
4614
4615
        $totalScoreText = null;
4616
        $certificateBlock = '';
4617
        if (($show_results || $show_only_score) && $showTotalScore) {
4618
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4619
                echo '<h1 style="text-align : center; margin : 20px 0;">'.get_lang('Your results').'</h1><br />';
4620
            }
4621
            $totalScoreText .= '<div class="question_row_score">';
4622
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4623
                $totalScoreText .= self::getQuestionDiagnosisRibbon(
4624
                    $objExercise,
4625
                    $total_score,
4626
                    $total_weight,
4627
                    true
4628
                );
4629
            } else {
4630
                $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
4631
                if ('true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)) {
4632
                    $formula = $pluginEvaluation->getFormulaForExercise($objExercise->getId());
4633
4634
                    if (!empty($formula)) {
4635
                        $total_score = $pluginEvaluation->getResultWithFormula($exeId, $formula);
4636
                        $total_weight = $pluginEvaluation->getMaxScore();
4637
                    }
4638
                }
4639
4640
                $totalScoreText .= self::getTotalScoreRibbon(
4641
                    $objExercise,
4642
                    $total_score,
4643
                    $total_weight,
4644
                    true,
4645
                    $countPendingQuestions
4646
                );
4647
            }
4648
            $totalScoreText .= '</div>';
4649
4650
            if (!empty($studentInfo)) {
4651
                $certificateBlock = self::generateAndShowCertificateBlock(
4652
                    $total_score,
4653
                    $total_weight,
4654
                    $objExercise,
4655
                    $studentInfo['id'],
4656
                    $courseId,
4657
                    $sessionId
4658
                );
4659
            }
4660
        }
4661
4662
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4663
            $chartMultiAnswer = MultipleAnswerTrueFalseDegreeCertainty::displayStudentsChartResults(
4664
                $exeId,
4665
                $objExercise
4666
            );
4667
            echo $chartMultiAnswer;
4668
        }
4669
4670
        if (!empty($category_list) &&
4671
            ($show_results || $show_only_score || RESULT_DISABLE_RADAR == $objExercise->results_disabled)
4672
        ) {
4673
            // Adding total
4674
            $category_list['total'] = [
4675
                'score' => $total_score,
4676
                'total' => $total_weight,
4677
            ];
4678
            echo TestCategory::get_stats_table_by_attempt($objExercise, $category_list);
4679
        }
4680
4681
        if ($show_all_but_expected_answer) {
4682
            $exercise_content .= Display::return_message(get_lang('Note: This test has been setup to hide the expected answers.'));
4683
        }
4684
4685
        // Remove audio auto play from questions on results page - refs BT#7939
4686
        $exercise_content = preg_replace(
4687
            ['/autoplay[\=\".+\"]+/', '/autostart[\=\".+\"]+/'],
4688
            '',
4689
            $exercise_content
4690
        );
4691
4692
        echo $totalScoreText;
4693
        echo $certificateBlock;
4694
4695
        // Ofaj change BT#11784
4696
        if (api_get_configuration_value('quiz_show_description_on_results_page') &&
4697
            !empty($objExercise->description)
4698
        ) {
4699
            echo Display::div($objExercise->description, ['class' => 'exercise_description']);
4700
        }
4701
4702
        echo $exercise_content;
4703
        if (!$show_only_score) {
4704
            echo $totalScoreText;
4705
        }
4706
4707
        if ($save_user_result) {
4708
            // Tracking of results
4709
            if ($exercise_stat_info) {
4710
                $learnpath_id = $exercise_stat_info['orig_lp_id'];
4711
                $learnpath_item_id = $exercise_stat_info['orig_lp_item_id'];
4712
                $learnpath_item_view_id = $exercise_stat_info['orig_lp_item_view_id'];
4713
4714
                if (api_is_allowed_to_session_edit()) {
4715
                    Event::updateEventExercise(
4716
                        $exercise_stat_info['exe_id'],
4717
                        $objExercise->getId(),
4718
                        $total_score,
4719
                        $total_weight,
4720
                        $sessionId,
4721
                        $learnpath_id,
4722
                        $learnpath_item_id,
4723
                        $learnpath_item_view_id,
4724
                        $exercise_stat_info['exe_duration'],
4725
                        $question_list
4726
                    );
4727
4728
                    $allowStats = api_get_configuration_value('allow_gradebook_stats');
4729
                    if ($allowStats) {
4730
                        $objExercise->generateStats(
4731
                            $objExercise->getId(),
4732
                            api_get_course_info(),
4733
                            $sessionId
4734
                        );
4735
                    }
4736
                }
4737
            }
4738
4739
            // Send notification at the end
4740
            if (!api_is_allowed_to_edit(null, true) &&
4741
                !api_is_excluded_user_type()
4742
            ) {
4743
                $objExercise->send_mail_notification_for_exam(
4744
                    'end',
4745
                    $question_list_answers,
4746
                    $origin,
4747
                    $exeId,
4748
                    $total_score,
4749
                    $total_weight
4750
                );
4751
            }
4752
        }
4753
4754
        if (in_array(
4755
            $objExercise->selectResultsDisabled(),
4756
            [RESULT_DISABLE_RANKING, RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING]
4757
        )) {
4758
            echo Display::page_header(get_lang('Ranking'), null, 'h4');
4759
            echo self::displayResultsInRanking(
4760
                $objExercise,
4761
                api_get_user_id(),
4762
                $courseId,
4763
                $sessionId
4764
            );
4765
        }
4766
4767
        if (!empty($remainingMessage)) {
4768
            echo Display::return_message($remainingMessage, 'normal', false);
4769
        }
4770
4771
        $failedAnswersCount = 0;
4772
        $wrongQuestionHtml = '';
4773
        $all = '';
4774
        foreach ($attemptResult as $item) {
4775
            if (false === $item['pass']) {
4776
                $failedAnswersCount++;
4777
                $wrongQuestionHtml .= $item['question_content'].'<br />';
4778
            }
4779
            $all .= $item['question_content'].'<br />';
4780
        }
4781
4782
        $passed = self::isPassPercentageAttemptPassed(
4783
            $objExercise,
4784
            $total_score,
4785
            $total_weight
4786
        );
4787
4788
        $percentage = 0;
4789
        if (!empty($total_weight)) {
4790
            $percentage = ($total_score / $total_weight) * 100;
4791
        }
4792
4793
        return [
4794
            'category_list' => $category_list,
4795
            'attempts_result_list' => $attemptResult, // array of results
4796
            'exercise_passed' => $passed, // boolean
4797
            'total_answers_count' => count($attemptResult), // int
4798
            'failed_answers_count' => $failedAnswersCount, // int
4799
            'failed_answers_html' => $wrongQuestionHtml,
4800
            'all_answers_html' => $all,
4801
            'total_score' => $total_score,
4802
            'total_weight' => $total_weight,
4803
            'total_percentage' => $percentage,
4804
            'count_pending_questions' => $countPendingQuestions,
4805
        ];
4806
    }
4807
4808
    /**
4809
     * Display the ranking of results in a exercise.
4810
     *
4811
     * @param Exercise $exercise
4812
     * @param int      $currentUserId
4813
     * @param int      $courseId
4814
     * @param int      $sessionId
4815
     *
4816
     * @return string
4817
     */
4818
    public static function displayResultsInRanking($exercise, $currentUserId, $courseId, $sessionId = 0)
4819
    {
4820
        $exerciseId = $exercise->iId;
4821
        $data = self::exerciseResultsInRanking($exerciseId, $courseId, $sessionId);
4822
4823
        $table = new HTML_Table(['class' => 'table table-hover table-striped table-bordered']);
4824
        $table->setHeaderContents(0, 0, get_lang('Position'), ['class' => 'text-right']);
4825
        $table->setHeaderContents(0, 1, get_lang('Username'));
4826
        $table->setHeaderContents(0, 2, get_lang('Score'), ['class' => 'text-right']);
4827
        $table->setHeaderContents(0, 3, get_lang('Date'), ['class' => 'text-center']);
4828
4829
        foreach ($data as $r => $item) {
4830
            if (!isset($item[1])) {
4831
                continue;
4832
            }
4833
            $selected = $item[1]->getId() == $currentUserId;
4834
4835
            foreach ($item as $c => $value) {
4836
                $table->setCellContents($r + 1, $c, $value);
4837
4838
                $attrClass = '';
4839
4840
                if (in_array($c, [0, 2])) {
4841
                    $attrClass = 'text-right';
4842
                } elseif (3 == $c) {
4843
                    $attrClass = 'text-center';
4844
                }
4845
4846
                if ($selected) {
4847
                    $attrClass .= ' warning';
4848
                }
4849
4850
                $table->setCellAttributes($r + 1, $c, ['class' => $attrClass]);
4851
            }
4852
        }
4853
4854
        return $table->toHtml();
4855
    }
4856
4857
    /**
4858
     * Get the ranking for results in a exercise.
4859
     * Function used internally by ExerciseLib::displayResultsInRanking.
4860
     *
4861
     * @param int $exerciseId
4862
     * @param int $courseId
4863
     * @param int $sessionId
4864
     *
4865
     * @return array
4866
     */
4867
    public static function exerciseResultsInRanking($exerciseId, $courseId, $sessionId = 0)
4868
    {
4869
        $em = Database::getManager();
4870
4871
        $dql = 'SELECT DISTINCT te.exeUserId FROM ChamiloCoreBundle:TrackExercise te WHERE te.exeExoId = :id AND te.course = :cId';
4872
        $dql .= api_get_session_condition($sessionId, true, false, 'te.sessionId');
4873
4874
        $result = $em
4875
            ->createQuery($dql)
4876
            ->setParameters(['id' => $exerciseId, 'cId' => $courseId])
4877
            ->getScalarResult();
4878
4879
        $data = [];
4880
4881
        foreach ($result as $item) {
4882
            $data[] = self::get_best_attempt_by_user($item['exeUserId'], $exerciseId, $courseId, $sessionId);
4883
        }
4884
4885
        usort(
4886
            $data,
4887
            function ($a, $b) {
4888
                if ($a['score'] != $b['score']) {
4889
                    return $a['score'] > $b['score'] ? -1 : 1;
4890
                }
4891
4892
                if ($a['exe_date'] != $b['exe_date']) {
4893
                    return $a['exe_date'] < $b['exe_date'] ? -1 : 1;
4894
                }
4895
4896
                return 0;
4897
            }
4898
        );
4899
4900
        // flags to display the same position in case of tie
4901
        $lastScore = $data[0]['score'];
4902
        $position = 1;
4903
        $data = array_map(
4904
            function ($item) use (&$lastScore, &$position) {
4905
                if ($item['score'] < $lastScore) {
4906
                    $position++;
4907
                }
4908
4909
                $lastScore = $item['score'];
4910
4911
                return [
4912
                    $position,
4913
                    api_get_user_entity($item['exe_user_id']),
4914
                    self::show_score($item['score'], $item['max_score'], true, true, true),
4915
                    api_convert_and_format_date($item['exe_date'], DATE_TIME_FORMAT_SHORT),
4916
                ];
4917
            },
4918
            $data
4919
        );
4920
4921
        return $data;
4922
    }
4923
4924
    /**
4925
     * Get a special ribbon on top of "degree of certainty" questions (
4926
     * variation from getTotalScoreRibbon() for other question types).
4927
     *
4928
     * @param Exercise $objExercise
4929
     * @param float    $score
4930
     * @param float    $weight
4931
     * @param bool     $checkPassPercentage
4932
     *
4933
     * @return string
4934
     */
4935
    public static function getQuestionDiagnosisRibbon($objExercise, $score, $weight, $checkPassPercentage = false)
4936
    {
4937
        $displayChartDegree = true;
4938
        $ribbon = $displayChartDegree ? '<div class="ribbon">' : '';
4939
4940
        if ($checkPassPercentage) {
4941
            $passPercentage = $objExercise->selectPassPercentage();
4942
            $isSuccess = self::isSuccessExerciseResult($score, $weight, $passPercentage);
4943
            // Color the final test score if pass_percentage activated
4944
            $ribbonTotalSuccessOrError = '';
4945
            if (self::isPassPercentageEnabled($passPercentage)) {
4946
                if ($isSuccess) {
4947
                    $ribbonTotalSuccessOrError = ' ribbon-total-success';
4948
                } else {
4949
                    $ribbonTotalSuccessOrError = ' ribbon-total-error';
4950
                }
4951
            }
4952
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total '.$ribbonTotalSuccessOrError.'">' : '';
4953
        } else {
4954
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total">' : '';
4955
        }
4956
4957
        if ($displayChartDegree) {
4958
            $ribbon .= '<h3>'.get_lang('Score for the test').':&nbsp;';
4959
            $ribbon .= self::show_score($score, $weight, false, true);
4960
            $ribbon .= '</h3>';
4961
            $ribbon .= '</div>';
4962
        }
4963
4964
        if ($checkPassPercentage) {
4965
            $ribbon .= self::showSuccessMessage(
4966
                $score,
4967
                $weight,
4968
                $objExercise->selectPassPercentage()
4969
            );
4970
        }
4971
4972
        $ribbon .= $displayChartDegree ? '</div>' : '';
4973
4974
        return $ribbon;
4975
    }
4976
4977
    public static function isPassPercentageAttemptPassed($objExercise, $score, $weight)
4978
    {
4979
        $passPercentage = $objExercise->selectPassPercentage();
4980
4981
        return self::isSuccessExerciseResult($score, $weight, $passPercentage);
4982
    }
4983
4984
    /**
4985
     * @param float $score
4986
     * @param float $weight
4987
     * @param bool  $checkPassPercentage
4988
     * @param int   $countPendingQuestions
4989
     *
4990
     * @return string
4991
     */
4992
    public static function getTotalScoreRibbon(
4993
        Exercise $objExercise,
4994
        $score,
4995
        $weight,
4996
        $checkPassPercentage = false,
4997
        $countPendingQuestions = 0
4998
    ) {
4999
        $hide = (int) $objExercise->getPageConfigurationAttribute('hide_total_score');
5000
        if (1 === $hide) {
5001
            return '';
5002
        }
5003
5004
        $passPercentage = $objExercise->selectPassPercentage();
5005
        $ribbon = '<div class="title-score">';
5006
        if ($checkPassPercentage) {
5007
            $isSuccess = self::isSuccessExerciseResult(
5008
                $score,
5009
                $weight,
5010
                $passPercentage
5011
            );
5012
            // Color the final test score if pass_percentage activated
5013
            $class = '';
5014
            if (self::isPassPercentageEnabled($passPercentage)) {
5015
                if ($isSuccess) {
5016
                    $class = ' ribbon-total-success';
5017
                } else {
5018
                    $class = ' ribbon-total-error';
5019
                }
5020
            }
5021
            $ribbon .= '<div class="total '.$class.'">';
5022
        } else {
5023
            $ribbon .= '<div class="total">';
5024
        }
5025
        $ribbon .= '<h3>'.get_lang('Score for the test').':&nbsp;';
5026
        $ribbon .= self::show_score($score, $weight, false, true);
5027
        $ribbon .= '</h3>';
5028
        $ribbon .= '</div>';
5029
        if ($checkPassPercentage) {
5030
            $ribbon .= self::showSuccessMessage(
5031
                $score,
5032
                $weight,
5033
                $passPercentage
5034
            );
5035
        }
5036
        $ribbon .= '</div>';
5037
5038
        if (!empty($countPendingQuestions)) {
5039
            $ribbon .= '<br />';
5040
            $ribbon .= Display::return_message(
5041
                sprintf(
5042
                    get_lang('Temporary score: %s open question(s) not corrected yet.'),
5043
                    $countPendingQuestions
5044
                ),
5045
                'warning'
5046
            );
5047
        }
5048
5049
        return $ribbon;
5050
    }
5051
5052
    /**
5053
     * @param int $countLetter
5054
     *
5055
     * @return mixed
5056
     */
5057
    public static function detectInputAppropriateClass($countLetter)
5058
    {
5059
        $limits = [
5060
            0 => 'input-mini',
5061
            10 => 'input-mini',
5062
            15 => 'input-medium',
5063
            20 => 'input-xlarge',
5064
            40 => 'input-xlarge',
5065
            60 => 'input-xxlarge',
5066
            100 => 'input-xxlarge',
5067
            200 => 'input-xxlarge',
5068
        ];
5069
5070
        foreach ($limits as $size => $item) {
5071
            if ($countLetter <= $size) {
5072
                return $item;
5073
            }
5074
        }
5075
5076
        return $limits[0];
5077
    }
5078
5079
    /**
5080
     * @param int    $senderId
5081
     * @param array  $course_info
5082
     * @param string $test
5083
     * @param string $url
5084
     *
5085
     * @return string
5086
     */
5087
    public static function getEmailNotification($senderId, $course_info, $test, $url)
5088
    {
5089
        $teacher_info = api_get_user_info($senderId);
5090
        $fromName = api_get_person_name(
5091
            $teacher_info['firstname'],
5092
            $teacher_info['lastname'],
5093
            null,
5094
            PERSON_NAME_EMAIL_ADDRESS
5095
        );
5096
5097
        $params = [
5098
            'course_title' => Security::remove_XSS($course_info['name']),
5099
            'test_title' => Security::remove_XSS($test),
5100
            'url' => $url,
5101
            'teacher_name' => $fromName,
5102
        ];
5103
5104
        return Container::getTwig()->render(
5105
            '@ChamiloCore/Mailer/Exercise/result_alert_body.html.twig',
5106
            $params
5107
        );
5108
    }
5109
5110
    /**
5111
     * @return string
5112
     */
5113
    public static function getNotCorrectedYetText()
5114
    {
5115
        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');
5116
    }
5117
5118
    /**
5119
     * @param string $message
5120
     *
5121
     * @return string
5122
     */
5123
    public static function getFeedbackText($message)
5124
    {
5125
        return Display::return_message($message, 'warning', false);
5126
    }
5127
5128
    /**
5129
     * Get the recorder audio component for save a teacher audio feedback.
5130
     *
5131
     * @param int $attemptId
5132
     * @param int $questionId
5133
     * @param int $userId
5134
     *
5135
     * @return string
5136
     */
5137
    public static function getOralFeedbackForm($attemptId, $questionId, $userId)
5138
    {
5139
        $view = new Template('', false, false, false, false, false, false);
5140
        $view->assign('user_id', $userId);
5141
        $view->assign('question_id', $questionId);
5142
        $view->assign('directory', "/../exercises/teacher_audio/$attemptId/");
5143
        $view->assign('file_name', "{$questionId}_{$userId}");
5144
        $template = $view->get_template('exercise/oral_expression.tpl');
5145
5146
        return $view->fetch($template);
5147
    }
5148
5149
    /**
5150
     * Get the audio componen for a teacher audio feedback.
5151
     *
5152
     * @param int $attemptId
5153
     * @param int $questionId
5154
     * @param int $userId
5155
     *
5156
     * @return string
5157
     */
5158
    public static function getOralFeedbackAudio($attemptId, $questionId, $userId)
5159
    {
5160
        return;
5161
        $courseInfo = api_get_course_info();
0 ignored issues
show
Unused Code introduced by
$courseInfo = api_get_course_info() is not reachable.

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

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

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

    return false;
}

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

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