Passed
Pull Request — 1.11.x (#4360)
by Angel Fernando Quiroz
08:44
created

ExerciseLib::showQuestion()   F

Complexity

Conditions 244
Paths > 20000

Size

Total Lines 1734
Code Lines 1004

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1004
c 1
b 0
f 0
dl 0
loc 1734
rs 0
cc 244
nc 13660802
nop 11

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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