Passed
Push — 1.11.x ( 7871bd...376377 )
by Julito
14:09
created

ExerciseLib::show_score()   D

Complexity

Conditions 18

Size

Total Lines 97
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 18
eloc 54
c 0
b 0
f 0
nop 10
dl 0
loc 97
rs 4.8666

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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