Passed
Push — 1.11.x ( fdee22...a6a7a3 )
by Julito
14:59
created

ExerciseLib::get_exercises_to_be_taken()   A

Complexity

Conditions 5

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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