Passed
Push — 1.11.x ( 9ffdbf...6f13d0 )
by Yannick
08:16 queued 14s
created

ExerciseLib::exportExerciseAllResultsZip()   B

Complexity

Conditions 7

Size

Total Lines 46
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 26
c 0
b 0
f 0
dl 0
loc 46
rs 8.5706
cc 7
nop 3
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_COMBINATION &&
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 = [];
126
127
            for ($i = 1; $i <= $objAnswerTmp->nbrAnswers; $i++) {
128
                $selectableOptions[$objAnswerTmp->iid[$i]] = $objAnswerTmp->answer[$i];
129
            }
130
131
            // For "matching" type here, we need something a little bit special
132
            // because the match between the suggestions and the answers cannot be
133
            // done easily (suggestions and answers are in the same table), so we
134
            // have to go through answers first (elems with "correct" value to 0).
135
            $select_items = [];
136
            //This will contain the number of answers on the left side. We call them
137
            // suggestions here, for the sake of comprehensions, while the ones
138
            // on the right side are called answers
139
            $num_suggestions = 0;
140
            switch ($answerType) {
141
                case MATCHING:
142
                case MATCHING_COMBINATION:
143
                case DRAGGABLE:
144
                case MATCHING_DRAGGABLE_COMBINATION:
145
                case MATCHING_DRAGGABLE:
146
                    if ($answerType == DRAGGABLE) {
147
                        $isVertical = $objQuestionTmp->extra == 'v';
148
                        $s .= '
149
                            <div class="row">
150
                                <div class="col-md-12">
151
                                    <p class="small">'.get_lang('DraggableQuestionIntro').'</p>
152
                                    <ul class="exercise-draggable-answer list-unstyled '
153
                            .($isVertical ? '' : 'list-inline').'" id="question-'.$questionId.'" data-question="'
154
                            .$questionId.'">
155
                        ';
156
                    } else {
157
                        $s .= '<div id="drag'.$questionId.'_question" class="drag_question">
158
                               <table class="table table-hover table-striped data_table">';
159
                    }
160
161
                    // Iterate through answers.
162
                    $x = 1;
163
                    // Mark letters for each answer.
164
                    $letter = 'A';
165
                    $answer_matching = [];
166
                    $cpt1 = [];
167
                    for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
168
                        $answerCorrect = $objAnswerTmp->isCorrect($answerId);
169
                        $numAnswer = $objAnswerTmp->selectId($answerId);
170
                        if ($answerCorrect == 0) {
171
                            // options (A, B, C, ...) that will be put into the list-box
172
                            // have the "correct" field set to 0 because they are answer
173
                            $cpt1[$x] = $letter;
174
                            $answer_matching[$x] = $objAnswerTmp->selectAnswerById($numAnswer);
175
                            $x++;
176
                            $letter++;
177
                        }
178
                    }
179
180
                    $i = 1;
181
                    $select_items[0]['id'] = 0;
182
                    $select_items[0]['letter'] = '--';
183
                    $select_items[0]['answer'] = '';
184
                    foreach ($answer_matching as $id => $value) {
185
                        $select_items[$i]['id'] = $value['iid'];
186
                        $select_items[$i]['letter'] = $cpt1[$id];
187
                        $select_items[$i]['answer'] = $value['answer'];
188
                        $i++;
189
                    }
190
191
                    $user_choice_array_position = [];
192
                    if (!empty($user_choice)) {
193
                        foreach ($user_choice as $item) {
194
                            $user_choice_array_position[$item['position']] = $item['answer'];
195
                        }
196
                    }
197
                    $num_suggestions = ($nbrAnswers - $x) + 1;
198
                    break;
199
                case FREE_ANSWER:
200
                    $fck_content = isset($user_choice[0]) && !empty($user_choice[0]['answer']) ? $user_choice[0]['answer'] : null;
201
                    $form = new FormValidator('free_choice_'.$questionId);
202
                    $config = [
203
                        'ToolbarSet' => 'TestFreeAnswer',
204
                        'id' => 'choice['.$questionId.']',
205
                    ];
206
                    $form->addHtmlEditor(
207
                        'choice['.$questionId.']',
208
                        null,
209
                        false,
210
                        false,
211
                        $config
212
                    );
213
                    $form->setDefaults(["choice[".$questionId."]" => $fck_content]);
214
                    $s .= $form->returnForm();
215
                    break;
216
                case UPLOAD_ANSWER:
217
                    global $exe_id;
218
                    $answer = isset($user_choice[0]) && !empty($user_choice[0]['answer']) ? $user_choice[0]['answer'] : null;
219
                    $path = '/upload_answer/'.$exe_id.'/'.$questionId.'/';
220
                    $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=upload_answer&curdirpath='.$path;
221
                    $multipleForm = new FormValidator(
222
                        'drag_drop',
223
                        'post',
224
                        '#',
225
                        ['enctype' => 'multipart/form-data']
226
                    );
227
                    $iconDelete = Display::return_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_SMALL);
228
                    $multipleForm->addMultipleUpload($url);
229
                    $s .= '<script>
230
                        function setRemoveLink(dataContext) {
231
                            var removeLink = $("<a>", {
232
                                html: "&nbsp;'.addslashes($iconDelete).'",
233
                                href: "#",
234
                                click: function(e) {
235
                                  e.preventDefault();
236
                                  dataContext.parent().remove();
237
                                }
238
                            });
239
                            dataContext.append(removeLink);
240
                        }
241
                        $(function() {
242
                            $("#input_file_upload").bind("fileuploaddone", function (e, data) {
243
                                $.each(data.result.files, function (index, file) {
244
                                    if (file.name) {
245
                                        var input = $("<input>", {
246
                                            type: "hidden",
247
                                            name: "uploadChoice['.$questionId.'][]",
248
                                            value: file.name
249
                                        });
250
                                        $(data.context.children()[index]).parent().append(input);
251
                                        // set the remove link
252
                                        setRemoveLink($(data.context.children()[index]).parent());
253
                                    }
254
                                });
255
                            });
256
                        });
257
                    </script>';
258
                    // Set default values
259
                    if (!empty($answer)) {
260
                        $userWebpath = UserManager::getUserPathById(api_get_user_id(), 'web').'my_files'.'/upload_answer/'.$exe_id.'/'.$questionId.'/';
261
                        $userSyspath = UserManager::getUserPathById(api_get_user_id(), 'system').'my_files'.'/upload_answer/'.$exe_id.'/'.$questionId.'/';
262
                        $filesNames = explode('|', $answer);
263
                        $icon = Display::return_icon('file_txt.gif');
264
                        $default = '';
265
                        foreach ($filesNames as $fileName) {
266
                            $fileName = Security::remove_XSS($fileName);
267
                            if (file_exists($userSyspath.$fileName)) {
268
                                $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>';
269
                            }
270
                        }
271
                        $s .= '<script>
272
                            $(function() {
273
                                if ($("#files").length > 0) {
274
                                    $("#files").html("'.addslashes($default).'");
275
                                    var links = $("#files").children();
276
                                    links.each(function(index) {
277
                                        var dataContext = $(links[index]).find(".row");
278
                                        setRemoveLink(dataContext);
279
                                    });
280
                                }
281
                            });
282
                        </script>';
283
                    }
284
                    $s .= $multipleForm->returnForm();
285
                    break;
286
                case ORAL_EXPRESSION:
287
                    // Add nanog
288
                    if (api_get_setting('enable_record_audio') === 'true') {
289
                        //@todo pass this as a parameter
290
                        global $exercise_stat_info;
291
                        if (!empty($exercise_stat_info)) {
292
                            $objQuestionTmp->initFile(
293
                                api_get_session_id(),
294
                                api_get_user_id(),
295
                                $exercise_stat_info['exe_exo_id'],
296
                                $exercise_stat_info['exe_id']
297
                            );
298
                        } else {
299
                            $objQuestionTmp->initFile(
300
                                api_get_session_id(),
301
                                api_get_user_id(),
302
                                $exerciseId,
303
                                'temp_exe'
304
                            );
305
                        }
306
307
                        echo $objQuestionTmp->returnRecorder();
308
                    }
309
                    $fileUrl = $objQuestionTmp->getFileUrl();
310
                    if (isset($fileUrl)) {
311
                        $s .= '
312
                            <div class="col-sm-4 col-sm-offset-4">
313
                                <div class="form-group text-center">
314
                                    <audio src="'.$fileUrl.'" controls id="record-preview-'.$questionId.'-previous"></audio>
315
                                </div>
316
                            </div>
317
                        ';
318
                    }
319
                    $s .= '<script>
320
                        // The buttons are blocked waiting for the audio file to be uploaded
321
                        $(document).ajaxStart(function() {
322
                            $("button").attr("disabled", true);
323
                            $("button").attr("title", "'.get_lang('WaitingForTheAudioFileToBeUploaded').'");
324
                        });
325
                        $(document).ajaxComplete(function() {
326
                            $("button").attr("disabled", false);
327
                            $("button").removeAttr("title");
328
                        });
329
                    </script>';
330
                    $form = new FormValidator('free_choice_'.$questionId);
331
                    $config = ['ToolbarSet' => 'TestFreeAnswer'];
332
333
                    $form->addHtml('<div id="'.'hide_description_'.$questionId.'_options" style="display: none;">');
334
                    $form->addHtmlEditor(
335
                        "choice[$questionId]",
336
                        null,
337
                        false,
338
                        false,
339
                        $config
340
                    );
341
                    $form->addHtml('</div>');
342
                    $s .= $form->returnForm();
343
                    break;
344
                case MULTIPLE_ANSWER_DROPDOWN:
345
                case MULTIPLE_ANSWER_DROPDOWN_COMBINATION:
346
                    if ($debug_mark_answer) {
347
                        $s .= '<p><strong>'
348
                            .(
349
                                MULTIPLE_ANSWER_DROPDOWN == $answerType
350
                                    ? '<span class="pull-right">'.get_lang('Weighting').'</span>'
351
                                    : ''
352
                            )
353
                            .get_lang('CorrectAnswer').'</strong></p>';
354
                    }
355
                    break;
356
            }
357
358
            // Now navigate through the possible answers, using the max number of
359
            // answers for the question as a limiter
360
            $lines_count = 1; // a counter for matching-type answers
361
            if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE ||
362
                $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE
363
            ) {
364
                $header = Display::tag('th', get_lang('Options'));
365
                foreach ($objQuestionTmp->options as $item) {
366
                    if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE) {
367
                        if (in_array($item, $objQuestionTmp->options)) {
368
                            $header .= Display::tag('th', get_lang($item));
369
                        } else {
370
                            $header .= Display::tag('th', $item);
371
                        }
372
                    } else {
373
                        $header .= Display::tag('th', $item);
374
                    }
375
                }
376
                if ($show_comment) {
377
                    $header .= Display::tag('th', get_lang('Feedback'));
378
                }
379
                $s .= '<table class="table table-hover table-striped">';
380
                $s .= Display::tag(
381
                    'tr',
382
                    $header,
383
                    ['style' => 'text-align:left;']
384
                );
385
            } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
386
                $header = Display::tag('th', get_lang('Options'), ['width' => '50%']);
387
                echo "
388
                <script>
389
                    function RadioValidator(question_id, answer_id)
390
                    {
391
                        var ShowAlert = '';
392
                        var typeRadioB = '';
393
                        var AllFormElements = window.document.getElementById('exercise_form').elements;
394
395
                        for (i = 0; i < AllFormElements.length; i++) {
396
                            if (AllFormElements[i].type == 'radio') {
397
                                var ThisRadio = AllFormElements[i].name;
398
                                var ThisChecked = 'No';
399
                                var AllRadioOptions = document.getElementsByName(ThisRadio);
400
401
                                for (x = 0; x < AllRadioOptions.length; x++) {
402
                                     if (AllRadioOptions[x].checked && ThisChecked == 'No') {
403
                                         ThisChecked = 'Yes';
404
                                         break;
405
                                     }
406
                                }
407
408
                                var AlreadySearched = ShowAlert.indexOf(ThisRadio);
409
                                if (ThisChecked == 'No' && AlreadySearched == -1) {
410
                                    ShowAlert = ShowAlert + ThisRadio;
411
                                }
412
                            }
413
                        }
414
                        if (ShowAlert != '') {
415
416
                        } else {
417
                            $('.question-validate-btn').removeAttr('disabled');
418
                        }
419
                    }
420
421
                    function handleRadioRow(event, question_id, answer_id) {
422
                        var t = event.target;
423
                        if (t && t.tagName == 'INPUT')
424
                            return;
425
                        while (t && t.tagName != 'TD') {
426
                            t = t.parentElement;
427
                        }
428
                        var r = t.getElementsByTagName('INPUT')[0];
429
                        r.click();
430
                        RadioValidator(question_id, answer_id);
431
                    }
432
433
                    $(function() {
434
                        var ShowAlert = '';
435
                        var typeRadioB = '';
436
                        var question_id = $('input[name=question_id]').val();
437
                        var AllFormElements = window.document.getElementById('exercise_form').elements;
438
439
                        for (i = 0; i < AllFormElements.length; i++) {
440
                            if (AllFormElements[i].type == 'radio') {
441
                                var ThisRadio = AllFormElements[i].name;
442
                                var ThisChecked = 'No';
443
                                var AllRadioOptions = document.getElementsByName(ThisRadio);
444
445
                                for (x = 0; x < AllRadioOptions.length; x++) {
446
                                    if (AllRadioOptions[x].checked && ThisChecked == 'No') {
447
                                        ThisChecked = \"Yes\";
448
                                        break;
449
                                    }
450
                                }
451
452
                                var AlreadySearched = ShowAlert.indexOf(ThisRadio);
453
                                if (ThisChecked == 'No' && AlreadySearched == -1) {
454
                                    ShowAlert = ShowAlert + ThisRadio;
455
                                }
456
                            }
457
                        }
458
459
                        if (ShowAlert != '') {
460
                             $('.question-validate-btn').attr('disabled', 'disabled');
461
                        } else {
462
                            $('.question-validate-btn').removeAttr('disabled');
463
                        }
464
                    });
465
                </script>";
466
467
                foreach ($objQuestionTmp->optionsTitle as $item) {
468
                    if (in_array($item, $objQuestionTmp->optionsTitle)) {
469
                        $properties = [];
470
                        if ($item === 'Answers') {
471
                            $properties['colspan'] = 2;
472
                            $properties['style'] = 'background-color: #F56B2A; color: #ffffff;';
473
                        } elseif ($item == 'DegreeOfCertaintyThatMyAnswerIsCorrect') {
474
                            $properties['colspan'] = 6;
475
                            $properties['style'] = 'background-color: #330066; color: #ffffff;';
476
                        }
477
                        $header .= Display::tag('th', get_lang($item), $properties);
478
                    } else {
479
                        $header .= Display::tag('th', $item);
480
                    }
481
                }
482
483
                if ($show_comment) {
484
                    $header .= Display::tag('th', get_lang('Feedback'));
485
                }
486
487
                $s .= '<table class="table table-hover table-striped data_table">';
488
                $s .= Display::tag('tr', $header, ['style' => 'text-align:left;']);
489
490
                // ajout de la 2eme ligne d'entête pour true/falss et les pourcentages de certitude
491
                $header1 = Display::tag('th', '&nbsp;');
492
                $cpt1 = 0;
493
                foreach ($objQuestionTmp->options as $item) {
494
                    $colorBorder1 = $cpt1 == (count($objQuestionTmp->options) - 1)
495
                        ? '' : 'border-right: solid #FFFFFF 1px;';
496
                    if ($item === 'True' || $item === 'False') {
497
                        $header1 .= Display::tag(
498
                            'th',
499
                            get_lang($item),
500
                            ['style' => 'background-color: #F7C9B4; color: black;'.$colorBorder1]
501
                        );
502
                    } else {
503
                        $header1 .= Display::tag(
504
                            'th',
505
                            $item,
506
                            ['style' => 'background-color: #e6e6ff; color: black;padding:5px; '.$colorBorder1]
507
                        );
508
                    }
509
                    $cpt1++;
510
                }
511
                if ($show_comment) {
512
                    $header1 .= Display::tag('th', '&nbsp;');
513
                }
514
515
                $s .= Display::tag('tr', $header1);
516
517
                // add explanation
518
                $header2 = Display::tag('th', '&nbsp;');
519
                $descriptionList = [
520
                    get_lang('DegreeOfCertaintyIDeclareMyIgnorance'),
521
                    get_lang('DegreeOfCertaintyIAmVeryUnsure'),
522
                    get_lang('DegreeOfCertaintyIAmUnsure'),
523
                    get_lang('DegreeOfCertaintyIAmPrettySure'),
524
                    get_lang('DegreeOfCertaintyIAmSure'),
525
                    get_lang('DegreeOfCertaintyIAmVerySure'),
526
                ];
527
                $counter2 = 0;
528
                foreach ($objQuestionTmp->options as $item) {
529
                    if ($item === 'True' || $item === 'False') {
530
                        $header2 .= Display::tag('td',
531
                            '&nbsp;',
532
                            ['style' => 'background-color: #F7E1D7; color: black;border-right: solid #FFFFFF 1px;']);
533
                    } else {
534
                        $color_border2 = ($counter2 == (count($objQuestionTmp->options) - 1)) ?
535
                            '' : 'border-right: solid #FFFFFF 1px;font-size:11px;';
536
                        $header2 .= Display::tag(
537
                            'td',
538
                            nl2br($descriptionList[$counter2]),
539
                            ['style' => 'background-color: #EFEFFC; color: black; width: 110px; text-align:center;
540
                                vertical-align: top; padding:5px; '.$color_border2]);
541
                        $counter2++;
542
                    }
543
                }
544
                if ($show_comment) {
545
                    $header2 .= Display::tag('th', '&nbsp;');
546
                }
547
                $s .= Display::tag('tr', $header2);
548
            }
549
550
            if ($show_comment) {
551
                if (in_array(
552
                    $answerType,
553
                    [
554
                        MULTIPLE_ANSWER,
555
                        MULTIPLE_ANSWER_COMBINATION,
556
                        UNIQUE_ANSWER,
557
                        UNIQUE_ANSWER_IMAGE,
558
                        UNIQUE_ANSWER_NO_OPTION,
559
                        GLOBAL_MULTIPLE_ANSWER,
560
                    ]
561
                )) {
562
                    $header = Display::tag('th', get_lang('Options'));
563
                    if ($exercise->getFeedbackType() == EXERCISE_FEEDBACK_TYPE_END) {
564
                        $header .= Display::tag('th', get_lang('Feedback'));
565
                    }
566
                    $s .= '<table class="table table-hover table-striped">';
567
                    $s .= Display::tag(
568
                        'tr',
569
                        $header,
570
                        ['style' => 'text-align:left;']
571
                    );
572
                }
573
            }
574
575
            $matching_correct_answer = 0;
576
            $userChoiceList = [];
577
            if (!empty($user_choice)) {
578
                foreach ($user_choice as $item) {
579
                    $userChoiceList[] = $item['answer'];
580
                }
581
            }
582
583
            $hidingClass = '';
584
            if ($answerType == READING_COMPREHENSION) {
585
                $objQuestionTmp->setExerciseType($exercise->selectType());
586
                $objQuestionTmp->processText($objQuestionTmp->selectDescription());
587
                $hidingClass = 'hide-reading-answers';
588
                $s .= Display::div(
589
                    $objQuestionTmp->selectTitle(),
590
                    ['class' => 'question_title '.$hidingClass]
591
                );
592
            }
593
594
            $userStatus = STUDENT;
595
            // Allows to do a remove_XSS in question of exercise with user status COURSEMANAGER
596
            // see BT#18242
597
            if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
598
                $userStatus = COURSEMANAGERLOWSECURITY;
599
            }
600
601
            for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
602
                $answer = $objAnswerTmp->selectAnswer($answerId);
603
                $answerCorrect = $objAnswerTmp->isCorrect($answerId);
604
                $numAnswer = $objAnswerTmp->selectId($answerId);
605
                $comment = Security::remove_XSS($objAnswerTmp->selectComment($answerId));
606
                $attributes = [];
607
608
                switch ($answerType) {
609
                    case UNIQUE_ANSWER:
610
                    case UNIQUE_ANSWER_NO_OPTION:
611
                    case UNIQUE_ANSWER_IMAGE:
612
                    case READING_COMPREHENSION:
613
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
614
                        if (isset($user_choice[0]['answer']) && $user_choice[0]['answer'] == $numAnswer) {
615
                            $attributes = [
616
                                'id' => $input_id,
617
                                'checked' => 1,
618
                                'selected' => 1,
619
                            ];
620
                        } else {
621
                            $attributes = ['id' => $input_id];
622
                        }
623
624
                        if ($debug_mark_answer) {
625
                            if ($answerCorrect) {
626
                                $attributes['checked'] = 1;
627
                                $attributes['selected'] = 1;
628
                            }
629
                        }
630
631
                        if ($show_comment) {
632
                            $s .= '<tr><td>';
633
                        }
634
635
                        if ($answerType == UNIQUE_ANSWER_IMAGE) {
636
                            if ($show_comment) {
637
                                if (empty($comment)) {
638
                                    $s .= '<div id="answer'.$questionId.$numAnswer.'"
639
                                            class="exercise-unique-answer-image" style="text-align: center">';
640
                                } else {
641
                                    $s .= '<div id="answer'.$questionId.$numAnswer.'"
642
                                            class="exercise-unique-answer-image col-xs-6 col-sm-12"
643
                                            style="text-align: center">';
644
                                }
645
                            } else {
646
                                $s .= '<div id="answer'.$questionId.$numAnswer.'"
647
                                        class="exercise-unique-answer-image col-xs-6 col-md-3"
648
                                        style="text-align: center">';
649
                            }
650
                        }
651
652
                        if ($answerType != UNIQUE_ANSWER_IMAGE) {
653
                            $answer = Security::remove_XSS($answer, $userStatus);
654
                        }
655
                        $s .= Display::input(
656
                            'hidden',
657
                            'choice2['.$questionId.']',
658
                            '0'
659
                        );
660
661
                        $answer_input = null;
662
                        $attributes['class'] = 'checkradios';
663
                        if ($answerType == UNIQUE_ANSWER_IMAGE) {
664
                            $attributes['class'] = '';
665
                            $attributes['style'] = 'display: none;';
666
                            $answer = '<div class="thumbnail">'.$answer.'</div>';
667
                        }
668
669
                        $answer_input .= '<label class="radio '.$hidingClass.'">';
670
                        $answer_input .= Display::input(
671
                            'radio',
672
                            'choice['.$questionId.']',
673
                            $numAnswer,
674
                            $attributes
675
                        );
676
                        $answer_input .= $answer;
677
                        $answer_input .= '</label>';
678
679
                        if ($answerType == UNIQUE_ANSWER_IMAGE) {
680
                            $answer_input .= "</div>";
681
                        }
682
683
                        if ($show_comment) {
684
                            $s .= $answer_input;
685
                            $s .= '</td>';
686
                            $s .= '<td>';
687
                            $s .= $comment;
688
                            $s .= '</td>';
689
                            $s .= '</tr>';
690
                        } else {
691
                            $s .= $answer_input;
692
                        }
693
                        break;
694
                    case MULTIPLE_ANSWER:
695
                    case MULTIPLE_ANSWER_TRUE_FALSE:
696
                    case GLOBAL_MULTIPLE_ANSWER:
697
                    case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
698
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
699
                        $answer = Security::remove_XSS($answer, $userStatus);
700
701
                        if (in_array($numAnswer, $userChoiceList)) {
702
                            $attributes = [
703
                                'id' => $input_id,
704
                                'checked' => 1,
705
                                'selected' => 1,
706
                            ];
707
                        } else {
708
                            $attributes = ['id' => $input_id];
709
                        }
710
711
                        if ($debug_mark_answer) {
712
                            if ($answerCorrect) {
713
                                $attributes['checked'] = 1;
714
                                $attributes['selected'] = 1;
715
                            }
716
                        }
717
718
                        if ($answerType == MULTIPLE_ANSWER || $answerType == GLOBAL_MULTIPLE_ANSWER) {
719
                            $s .= '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
720
                            $attributes['class'] = 'checkradios';
721
                            $answer_input = '<label class="checkbox">';
722
                            $answer_input .= Display::input(
723
                                'checkbox',
724
                                'choice['.$questionId.']['.$numAnswer.']',
725
                                $numAnswer,
726
                                $attributes
727
                            );
728
                            $answer_input .= $answer;
729
                            $answer_input .= '</label>';
730
731
                            if ($show_comment) {
732
                                $s .= '<tr><td>';
733
                                $s .= $answer_input;
734
                                $s .= '</td>';
735
                                $s .= '<td>';
736
                                $s .= $comment;
737
                                $s .= '</td>';
738
                                $s .= '</tr>';
739
                            } else {
740
                                $s .= $answer_input;
741
                            }
742
                        } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE) {
743
                            $myChoice = [];
744
                            if (!empty($userChoiceList)) {
745
                                foreach ($userChoiceList as $item) {
746
                                    $item = explode(':', $item);
747
                                    if (!empty($item)) {
748
                                        $myChoice[$item[0]] = isset($item[1]) ? $item[1] : '';
749
                                    }
750
                                }
751
                            }
752
753
                            $s .= '<tr>';
754
                            $s .= Display::tag('td', $answer);
755
756
                            if (!empty($quizQuestionOptions)) {
757
                                foreach ($quizQuestionOptions as $id => $item) {
758
                                    if (isset($myChoice[$numAnswer]) && $id == $myChoice[$numAnswer]) {
759
                                        $attributes = [
760
                                            'checked' => 1,
761
                                            'selected' => 1,
762
                                        ];
763
                                    } else {
764
                                        $attributes = [];
765
                                    }
766
767
                                    if ($debug_mark_answer) {
768
                                        if ($id == $answerCorrect) {
769
                                            $attributes['checked'] = 1;
770
                                            $attributes['selected'] = 1;
771
                                        }
772
                                    }
773
                                    $s .= Display::tag(
774
                                        'td',
775
                                        Display::input(
776
                                            'radio',
777
                                            'choice['.$questionId.']['.$numAnswer.']',
778
                                            $id,
779
                                            $attributes
780
                                        ),
781
                                        ['style' => '']
782
                                    );
783
                                }
784
                            }
785
786
                            if ($show_comment) {
787
                                $s .= '<td>';
788
                                $s .= $comment;
789
                                $s .= '</td>';
790
                            }
791
                            $s .= '</tr>';
792
                        } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
793
                            $myChoice = [];
794
                            if (!empty($userChoiceList)) {
795
                                foreach ($userChoiceList as $item) {
796
                                    $item = explode(':', $item);
797
                                    $myChoice[$item[0]] = $item[1];
798
                                }
799
                            }
800
                            $myChoiceDegreeCertainty = [];
801
                            if (!empty($userChoiceList)) {
802
                                foreach ($userChoiceList as $item) {
803
                                    $item = explode(':', $item);
804
                                    $myChoiceDegreeCertainty[$item[0]] = $item[2];
805
                                }
806
                            }
807
                            $s .= '<tr>';
808
                            $s .= Display::tag('td', $answer);
809
810
                            if (!empty($quizQuestionOptions)) {
811
                                foreach ($quizQuestionOptions as $id => $item) {
812
                                    if (isset($myChoice[$numAnswer]) && $id == $myChoice[$numAnswer]) {
813
                                        $attributes = ['checked' => 1, 'selected' => 1];
814
                                    } else {
815
                                        $attributes = [];
816
                                    }
817
                                    $attributes['onChange'] = 'RadioValidator('.$questionId.', '.$numAnswer.')';
818
819
                                    // radio button selection
820
                                    if (isset($myChoiceDegreeCertainty[$numAnswer]) &&
821
                                        $id == $myChoiceDegreeCertainty[$numAnswer]
822
                                    ) {
823
                                        $attributes1 = ['checked' => 1, 'selected' => 1];
824
                                    } else {
825
                                        $attributes1 = [];
826
                                    }
827
828
                                    $attributes1['onChange'] = 'RadioValidator('.$questionId.', '.$numAnswer.')';
829
830
                                    if ($debug_mark_answer) {
831
                                        if ($id == $answerCorrect) {
832
                                            $attributes['checked'] = 1;
833
                                            $attributes['selected'] = 1;
834
                                        }
835
                                    }
836
837
                                    if ($item['name'] == 'True' || $item['name'] == 'False') {
838
                                        $s .= Display::tag('td',
839
                                            Display::input('radio',
840
                                                'choice['.$questionId.']['.$numAnswer.']',
841
                                                $id,
842
                                                $attributes
843
                                            ),
844
                                            ['style' => 'text-align:center; background-color:#F7E1D7;',
845
                                                'onclick' => 'handleRadioRow(event, '.
846
                                                    $questionId.', '.
847
                                                    $numAnswer.')',
848
                                            ]
849
                                        );
850
                                    } else {
851
                                        $s .= Display::tag('td',
852
                                            Display::input('radio',
853
                                                'choiceDegreeCertainty['.$questionId.']['.$numAnswer.']',
854
                                                $id,
855
                                                $attributes1
856
                                            ),
857
                                            ['style' => 'text-align:center; background-color:#EFEFFC;',
858
                                                'onclick' => 'handleRadioRow(event, '.
859
                                                    $questionId.', '.
860
                                                    $numAnswer.')',
861
                                            ]
862
                                        );
863
                                    }
864
                                }
865
                            }
866
867
                            if ($show_comment) {
868
                                $s .= '<td>';
869
                                $s .= $comment;
870
                                $s .= '</td>';
871
                            }
872
                            $s .= '</tr>';
873
                        }
874
                        break;
875
                    case MULTIPLE_ANSWER_COMBINATION:
876
                        // multiple answers
877
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
878
879
                        if (in_array($numAnswer, $userChoiceList)) {
880
                            $attributes = [
881
                                'id' => $input_id,
882
                                'checked' => 1,
883
                                'selected' => 1,
884
                            ];
885
                        } else {
886
                            $attributes = ['id' => $input_id];
887
                        }
888
889
                        if ($debug_mark_answer) {
890
                            if ($answerCorrect) {
891
                                $attributes['checked'] = 1;
892
                                $attributes['selected'] = 1;
893
                            }
894
                        }
895
                        $answer = Security::remove_XSS($answer, $userStatus);
896
                        $answer_input = '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
897
                        $answer_input .= '<label class="checkbox">';
898
                        $answer_input .= Display::input(
899
                            'checkbox',
900
                            'choice['.$questionId.']['.$numAnswer.']',
901
                            1,
902
                            $attributes
903
                        );
904
                        $answer_input .= $answer;
905
                        $answer_input .= '</label>';
906
907
                        if ($show_comment) {
908
                            $s .= '<tr>';
909
                            $s .= '<td>';
910
                            $s .= $answer_input;
911
                            $s .= '</td>';
912
                            $s .= '<td>';
913
                            $s .= $comment;
914
                            $s .= '</td>';
915
                            $s .= '</tr>';
916
                        } else {
917
                            $s .= $answer_input;
918
                        }
919
                        break;
920
                    case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
921
                        $s .= '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
922
                        $myChoice = [];
923
                        if (!empty($userChoiceList)) {
924
                            foreach ($userChoiceList as $item) {
925
                                $item = explode(':', $item);
926
                                if (isset($item[1]) && isset($item[0])) {
927
                                    $myChoice[$item[0]] = $item[1];
928
                                }
929
                            }
930
                        }
931
932
                        $answer = Security::remove_XSS($answer, $userStatus);
933
                        $s .= '<tr>';
934
                        $s .= Display::tag('td', $answer);
935
                        foreach ($objQuestionTmp->options as $key => $item) {
936
                            if (isset($myChoice[$numAnswer]) && $key == $myChoice[$numAnswer]) {
937
                                $attributes = [
938
                                    'checked' => 1,
939
                                    'selected' => 1,
940
                                ];
941
                            } else {
942
                                $attributes = [];
943
                            }
944
945
                            if ($debug_mark_answer) {
946
                                if ($key == $answerCorrect) {
947
                                    $attributes['checked'] = 1;
948
                                    $attributes['selected'] = 1;
949
                                }
950
                            }
951
                            $s .= Display::tag(
952
                                'td',
953
                                Display::input(
954
                                    'radio',
955
                                    'choice['.$questionId.']['.$numAnswer.']',
956
                                    $key,
957
                                    $attributes
958
                                )
959
                            );
960
                        }
961
962
                        if ($show_comment) {
963
                            $s .= '<td>';
964
                            $s .= $comment;
965
                            $s .= '</td>';
966
                        }
967
                        $s .= '</tr>';
968
                        break;
969
                    case FILL_IN_BLANKS:
970
                    case FILL_IN_BLANKS_COMBINATION:
971
                        // display the question, with field empty, for student to fill it,
972
                        // or filled to display the answer in the Question preview of the exercise/admin.php page
973
                        $displayForStudent = true;
974
                        $listAnswerInfo = FillBlanks::getAnswerInfo($answer);
975
                        // Correct answers
976
                        $correctAnswerList = $listAnswerInfo['words'];
977
                        // Student's answer
978
                        $studentAnswerList = [];
979
                        if (isset($user_choice[0]['answer'])) {
980
                            $arrayStudentAnswer = FillBlanks::getAnswerInfo(
981
                                $user_choice[0]['answer'],
982
                                true
983
                            );
984
                            $studentAnswerList = $arrayStudentAnswer['student_answer'];
985
                        }
986
987
                        // If the question must be shown with the answer (in page exercise/admin.php)
988
                        // for teacher preview set the student-answer to the correct answer
989
                        if ($debug_mark_answer) {
990
                            $studentAnswerList = $correctAnswerList;
991
                            $displayForStudent = false;
992
                        }
993
994
                        if (!empty($correctAnswerList) && !empty($studentAnswerList)) {
995
                            $answer = '';
996
                            for ($i = 0; $i < count($listAnswerInfo['common_words']) - 1; $i++) {
997
                                // display the common word
998
                                $answer .= $listAnswerInfo['common_words'][$i];
999
                                // display the blank word
1000
                                $correctItem = $listAnswerInfo['words'][$i];
1001
                                if (isset($studentAnswerList[$i])) {
1002
                                    // If student already started this test and answered this question,
1003
                                    // fill the blank with his previous answers
1004
                                    // may be "" if student viewed the question, but did not fill the blanks
1005
                                    $correctItem = $studentAnswerList[$i];
1006
                                }
1007
                                $attributes['style'] = 'width:'.$listAnswerInfo['input_size'][$i].'px';
1008
                                $answer .= FillBlanks::getFillTheBlankHtml(
1009
                                    $current_item,
1010
                                    $questionId,
1011
                                    $correctItem,
1012
                                    $attributes,
1013
                                    $answer,
1014
                                    $listAnswerInfo,
1015
                                    $displayForStudent,
1016
                                    $i
1017
                                );
1018
                            }
1019
                            // display the last common word
1020
                            $answer .= $listAnswerInfo['common_words'][$i];
1021
                        } else {
1022
                            // display empty [input] with the right width for student to fill it
1023
                            $answer = '';
1024
                            for ($i = 0; $i < count($listAnswerInfo['common_words']) - 1; $i++) {
1025
                                // display the common words
1026
                                $answer .= $listAnswerInfo['common_words'][$i];
1027
                                // display the blank word
1028
                                $attributes['style'] = 'width:'.$listAnswerInfo['input_size'][$i].'px';
1029
                                $answer .= FillBlanks::getFillTheBlankHtml(
1030
                                    $current_item,
1031
                                    $questionId,
1032
                                    '',
1033
                                    $attributes,
1034
                                    $answer,
1035
                                    $listAnswerInfo,
1036
                                    $displayForStudent,
1037
                                    $i
1038
                                );
1039
                            }
1040
                            // display the last common word
1041
                            $answer .= $listAnswerInfo['common_words'][$i];
1042
                        }
1043
                        $s .= $answer;
1044
                        break;
1045
                    case CALCULATED_ANSWER:
1046
                        /*
1047
                         * In the CALCULATED_ANSWER test
1048
                         * you mustn't have [ and ] in the textarea
1049
                         * you mustn't have @@ in the textarea
1050
                         * the text to find mustn't be empty or contains only spaces
1051
                         * the text to find mustn't contains HTML tags
1052
                         * the text to find mustn't contains char "
1053
                         */
1054
                        if (null !== $origin) {
1055
                            global $exe_id;
1056
                            $exe_id = (int) $exe_id;
1057
                            $trackAttempts = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1058
                            $sql = "SELECT answer FROM $trackAttempts
1059
                                    WHERE exe_id = $exe_id AND question_id = $questionId";
1060
                            $rsLastAttempt = Database::query($sql);
1061
                            $rowLastAttempt = Database::fetch_array($rsLastAttempt);
1062
1063
                            $answer = null;
1064
                            if (isset($rowLastAttempt['answer'])) {
1065
                                $answer = $rowLastAttempt['answer'];
1066
                                $answerParts = explode(':::', $answer);
1067
                                if (isset($answerParts[1])) {
1068
                                    $answer = $answerParts[0];
1069
                                    $calculatedAnswerList[$questionId] = $answerParts[1];
1070
                                    Session::write('calculatedAnswerId', $calculatedAnswerList);
1071
                                }
1072
                            } else {
1073
                                $calculatedAnswerList = Session::read('calculatedAnswerId');
1074
                                if (!isset($calculatedAnswerList[$questionId])) {
1075
                                    $calculatedAnswerList[$questionId] = mt_rand(1, $nbrAnswers);
1076
                                    Session::write('calculatedAnswerId', $calculatedAnswerList);
1077
                                }
1078
                                $answer = $objAnswerTmp->selectAnswer($calculatedAnswerList[$questionId]);
1079
                            }
1080
                        }
1081
1082
                        [$answer] = explode('@@', $answer);
1083
                        // $correctAnswerList array of array with correct answers 0=> [0=>[\p] 1=>[plop]]
1084
                        api_preg_match_all(
1085
                            '/\[[^]]+\]/',
1086
                            $answer,
1087
                            $correctAnswerList
1088
                        );
1089
1090
                        // get student answer to display it if student go back
1091
                        // to previous calculated answer question in a test
1092
                        if (isset($user_choice[0]['answer'])) {
1093
                            api_preg_match_all(
1094
                                '/\[[^]]+\]/',
1095
                                $answer,
1096
                                $studentAnswerList
1097
                            );
1098
                            $studentAnswerListToClean = $studentAnswerList[0];
1099
                            $studentAnswerList = [];
1100
1101
                            $maxStudents = count($studentAnswerListToClean);
1102
                            for ($i = 0; $i < $maxStudents; $i++) {
1103
                                $answerCorrected = $studentAnswerListToClean[$i];
1104
                                $answerCorrected = api_preg_replace(
1105
                                    '| / <font color="green"><b>.*$|',
1106
                                    '',
1107
                                    $answerCorrected
1108
                                );
1109
                                $answerCorrected = api_preg_replace(
1110
                                    '/^\[/',
1111
                                    '',
1112
                                    $answerCorrected
1113
                                );
1114
                                $answerCorrected = api_preg_replace(
1115
                                    '|^<font color="red"><s>|',
1116
                                    '',
1117
                                    $answerCorrected
1118
                                );
1119
                                $answerCorrected = api_preg_replace(
1120
                                    '|</s></font>$|',
1121
                                    '',
1122
                                    $answerCorrected
1123
                                );
1124
                                $answerCorrected = '['.$answerCorrected.']';
1125
                                $studentAnswerList[] = $answerCorrected;
1126
                            }
1127
                        }
1128
1129
                        // If display preview of answer in test view for exemple,
1130
                        // set the student answer to the correct answers
1131
                        if ($debug_mark_answer) {
1132
                            // contain the rights answers surronded with brackets
1133
                            $studentAnswerList = $correctAnswerList[0];
1134
                        }
1135
1136
                        /*
1137
                        Split the response by bracket
1138
                        tabComments is an array with text surrounding the text to find
1139
                        we add a space before and after the answerQuestion to be sure to
1140
                        have a block of text before and after [xxx] patterns
1141
                        so we have n text to find ([xxx]) and n+1 block of texts before,
1142
                        between and after the text to find
1143
                        */
1144
                        $tabComments = api_preg_split(
1145
                            '/\[[^]]+\]/',
1146
                            ' '.$answer.' '
1147
                        );
1148
                        if (!empty($correctAnswerList) && !empty($studentAnswerList)) {
1149
                            $answer = '';
1150
                            $i = 0;
1151
                            foreach ($studentAnswerList as $studentItem) {
1152
                                // Remove surrounding brackets
1153
                                $studentResponse = api_substr(
1154
                                    $studentItem,
1155
                                    1,
1156
                                    api_strlen($studentItem) - 2
1157
                                );
1158
                                $size = strlen($studentItem);
1159
                                $attributes['class'] = self::detectInputAppropriateClass($size);
1160
                                $answer .= $tabComments[$i].
1161
                                    Display::input(
1162
                                        'text',
1163
                                        "choice[$questionId][]",
1164
                                        $studentResponse,
1165
                                        $attributes
1166
                                    );
1167
                                $i++;
1168
                            }
1169
                            $answer .= $tabComments[$i];
1170
                        } else {
1171
                            // display exercise with empty input fields
1172
                            // every [xxx] are replaced with an empty input field
1173
                            foreach ($correctAnswerList[0] as $item) {
1174
                                $size = strlen($item);
1175
                                $attributes['class'] = self::detectInputAppropriateClass($size);
1176
                                if (EXERCISE_FEEDBACK_TYPE_POPUP == $exercise->getFeedbackType()) {
1177
                                    $attributes['id'] = "question_$questionId";
1178
                                    $attributes['class'] .= ' checkCalculatedQuestionOnEnter ';
1179
                                }
1180
1181
                                $answer = str_replace(
1182
                                    $item,
1183
                                    Display::input(
1184
                                        'text',
1185
                                        "choice[$questionId][]",
1186
                                        '',
1187
                                        $attributes
1188
                                    ),
1189
                                    $answer
1190
                                );
1191
                            }
1192
                        }
1193
                        if (null !== $origin) {
1194
                            $s = $answer;
1195
                            break;
1196
                        } else {
1197
                            $s .= $answer;
1198
                        }
1199
                        break;
1200
                    case MATCHING:
1201
                    case MATCHING_COMBINATION:
1202
                        // matching type, showing suggestions and answers
1203
                        // TODO: replace $answerId by $numAnswer
1204
                        if ($answerCorrect != 0) {
1205
                            // only show elements to be answered (not the contents of
1206
                            // the select boxes, who are correct = 0)
1207
                            $s .= '<tr><td width="45%" valign="top">';
1208
                            $parsed_answer = $answer;
1209
                            // Left part questions
1210
                            $s .= '<p class="indent">'.$lines_count.'.&nbsp;'.$parsed_answer.'</p></td>';
1211
                            // Middle part (matches selects)
1212
                            // Id of select is # question + # of option
1213
                            $s .= '<td width="10%" valign="top" align="center">
1214
                                <div class="select-matching">
1215
                                <select
1216
                                    id="choice_id_'.$current_item.'_'.$lines_count.'"
1217
                                    name="choice['.$questionId.']['.$numAnswer.']">';
1218
1219
                            // fills the list-box
1220
                            foreach ($select_items as $key => $val) {
1221
                                // set $debug_mark_answer to true at function start to
1222
                                // show the correct answer with a suffix '-x'
1223
                                $selected = '';
1224
                                if ($debug_mark_answer) {
1225
                                    if ($val['id'] == $answerCorrect) {
1226
                                        $selected = 'selected="selected"';
1227
                                    }
1228
                                }
1229
                                //$user_choice_array_position
1230
                                if (isset($user_choice_array_position[$numAnswer]) &&
1231
                                    $val['id'] == $user_choice_array_position[$numAnswer]
1232
                                ) {
1233
                                    $selected = 'selected="selected"';
1234
                                }
1235
                                $s .= '<option value="'.$val['id'].'" '.$selected.'>'.$val['letter'].'</option>';
1236
                            }
1237
1238
                            $s .= '</select></div></td><td width="5%" class="separate">&nbsp;</td>';
1239
                            $s .= '<td width="40%" valign="top" >';
1240
                            if (isset($select_items[$lines_count])) {
1241
                                $s .= '<div class="text-right">
1242
                                        <p class="indent">'.
1243
                                    $select_items[$lines_count]['letter'].'.&nbsp; '.
1244
                                    $select_items[$lines_count]['answer'].'
1245
                                        </p>
1246
                                        </div>';
1247
                            } else {
1248
                                $s .= '&nbsp;';
1249
                            }
1250
                            $s .= '</td>';
1251
                            $s .= '</tr>';
1252
                            $lines_count++;
1253
                            // If the left side of the "matching" has been completely
1254
                            // shown but the right side still has values to show...
1255
                            if (($lines_count - 1) == $num_suggestions) {
1256
                                // if it remains answers to shown at the right side
1257
                                while (isset($select_items[$lines_count])) {
1258
                                    $s .= '<tr>
1259
                                      <td colspan="2"></td>
1260
                                      <td valign="top">';
1261
                                    $s .= '<b>'.$select_items[$lines_count]['letter'].'.</b> '.
1262
                                        $select_items[$lines_count]['answer'];
1263
                                    $s .= "</td>
1264
                                </tr>";
1265
                                    $lines_count++;
1266
                                }
1267
                            }
1268
                            $matching_correct_answer++;
1269
                        }
1270
                        break;
1271
                    case DRAGGABLE:
1272
                        if ($answerCorrect) {
1273
                            $windowId = $questionId.'_'.$lines_count;
1274
                            $s .= '<li class="touch-items" id="'.$windowId.'">';
1275
                            $s .= Display::div(
1276
                                $answer,
1277
                                [
1278
                                    'id' => "window_$windowId",
1279
                                    'class' => "window{$questionId}_question_draggable exercise-draggable-answer-option",
1280
                                ]
1281
                            );
1282
1283
                            $draggableSelectOptions = [];
1284
                            $selectedValue = 0;
1285
                            $selectedIndex = 0;
1286
                            if ($user_choice) {
1287
                                foreach ($user_choice as $userChoiceKey => $chosen) {
1288
                                    $userChoiceKey++;
1289
                                    if ($lines_count != $userChoiceKey) {
1290
                                        continue;
1291
                                    }
1292
                                    /*if ($answerCorrect != $chosen['answer']) {
1293
                                        continue;
1294
                                    }*/
1295
                                    $selectedValue = $chosen['answer'];
1296
                                }
1297
                            }
1298
                            foreach ($select_items as $key => $select_item) {
1299
                                $draggableSelectOptions[$select_item['id']] = $select_item['letter'];
1300
                            }
1301
1302
                            foreach ($draggableSelectOptions as $value => $text) {
1303
                                if ($value == $selectedValue) {
1304
                                    break;
1305
                                }
1306
                                $selectedIndex++;
1307
                            }
1308
1309
                            $s .= Display::select(
1310
                                "choice[$questionId][$numAnswer]",
1311
                                $draggableSelectOptions,
1312
                                $selectedValue,
1313
                                [
1314
                                    'id' => "window_{$windowId}_select",
1315
                                    'class' => 'select_option hidden',
1316
                                ],
1317
                                false
1318
                            );
1319
1320
                            if ($selectedValue && $selectedIndex) {
1321
                                $s .= "
1322
                                    <script>
1323
                                        $(function() {
1324
                                            DraggableAnswer.deleteItem(
1325
                                                $('#{$questionId}_$lines_count'),
1326
                                                $('#drop_{$questionId}_{$selectedIndex}')
1327
                                            );
1328
                                        });
1329
                                    </script>
1330
                                ";
1331
                            }
1332
1333
                            if (isset($select_items[$lines_count])) {
1334
                                $s .= Display::div(
1335
                                    Display::tag(
1336
                                        'b',
1337
                                        $select_items[$lines_count]['letter']
1338
                                    ).$select_items[$lines_count]['answer'],
1339
                                    [
1340
                                        'id' => "window_{$windowId}_answer",
1341
                                        'class' => 'hidden',
1342
                                    ]
1343
                                );
1344
                            } else {
1345
                                $s .= '&nbsp;';
1346
                            }
1347
1348
                            $lines_count++;
1349
                            if (($lines_count - 1) == $num_suggestions) {
1350
                                while (isset($select_items[$lines_count])) {
1351
                                    $s .= Display::tag('b', $select_items[$lines_count]['letter']);
1352
                                    $s .= $select_items[$lines_count]['answer'];
1353
                                    $lines_count++;
1354
                                }
1355
                            }
1356
1357
                            $matching_correct_answer++;
1358
                            $s .= '</li>';
1359
                        }
1360
                        break;
1361
                    case MATCHING_DRAGGABLE_COMBINATION:
1362
                    case MATCHING_DRAGGABLE:
1363
                        if ($answerId == 1) {
1364
                            echo $objAnswerTmp->getJs();
1365
                        }
1366
                        if ($answerCorrect != 0) {
1367
                            $windowId = "{$questionId}_{$lines_count}";
1368
                            $s .= <<<HTML
1369
                            <tr>
1370
                                <td width="45%">
1371
                                    <div id="window_{$windowId}"
1372
                                        class="window window_left_question window{$questionId}_question">
1373
                                        <strong>$lines_count.</strong>
1374
                                        $answer
1375
                                    </div>
1376
                                </td>
1377
                                <td width="10%">
1378
HTML;
1379
1380
                            $draggableSelectOptions = [];
1381
                            $selectedValue = 0;
1382
                            $selectedIndex = 0;
1383
1384
                            if ($user_choice) {
1385
                                foreach ($user_choice as $chosen) {
1386
                                    if ($numAnswer == $chosen['position']) {
1387
                                        $selectedValue = $chosen['answer'];
1388
                                        break;
1389
                                    }
1390
                                }
1391
                            }
1392
1393
                            foreach ($select_items as $key => $selectItem) {
1394
                                $draggableSelectOptions[$selectItem['id']] = $selectItem['letter'];
1395
                            }
1396
1397
                            foreach ($draggableSelectOptions as $value => $text) {
1398
                                if ($value == $selectedValue) {
1399
                                    break;
1400
                                }
1401
                                $selectedIndex++;
1402
                            }
1403
1404
                            $s .= Display::select(
1405
                                "choice[$questionId][$numAnswer]",
1406
                                $draggableSelectOptions,
1407
                                $selectedValue,
1408
                                [
1409
                                    'id' => "window_{$windowId}_select",
1410
                                    'class' => 'hidden',
1411
                                ],
1412
                                false
1413
                            );
1414
1415
                            if (!empty($answerCorrect) && !empty($selectedValue)) {
1416
                                // Show connect if is not freeze (question preview)
1417
                                if (!$freeze) {
1418
                                    $s .= "
1419
                                        <script>
1420
                                            $(function() {
1421
                                                $(window).on('load', function() {
1422
                                                    jsPlumb.connect({
1423
                                                        source: 'window_$windowId',
1424
                                                        target: 'window_{$questionId}_{$selectedIndex}_answer',
1425
                                                        endpoint: ['Blank', {radius: 15}],
1426
                                                        anchors: ['RightMiddle', 'LeftMiddle'],
1427
                                                        paintStyle: {strokeStyle: '#8A8888', lineWidth: 8},
1428
                                                        connector: [
1429
                                                            MatchingDraggable.connectorType,
1430
                                                            {curvines: MatchingDraggable.curviness}
1431
                                                        ]
1432
                                                    });
1433
                                                });
1434
                                            });
1435
                                        </script>
1436
                                    ";
1437
                                }
1438
                            }
1439
1440
                            $s .= '</td><td width="45%">';
1441
                            if (isset($select_items[$lines_count])) {
1442
                                $s .= <<<HTML
1443
                                <div id="window_{$windowId}_answer" class="window window_right_question">
1444
                                    <strong>{$select_items[$lines_count]['letter']}.</strong>
1445
                                    {$select_items[$lines_count]['answer']}
1446
                                </div>
1447
HTML;
1448
                            } else {
1449
                                $s .= '&nbsp;';
1450
                            }
1451
1452
                            $s .= '</td></tr>';
1453
                            $lines_count++;
1454
                            if (($lines_count - 1) == $num_suggestions) {
1455
                                while (isset($select_items[$lines_count])) {
1456
                                    $s .= <<<HTML
1457
                                    <tr>
1458
                                        <td colspan="2"></td>
1459
                                        <td>
1460
                                            <strong>{$select_items[$lines_count]['letter']}</strong>
1461
                                            {$select_items[$lines_count]['answer']}
1462
                                        </td>
1463
                                    </tr>
1464
HTML;
1465
                                    $lines_count++;
1466
                                }
1467
                            }
1468
                            $matching_correct_answer++;
1469
                        }
1470
                        break;
1471
                    case MULTIPLE_ANSWER_DROPDOWN:
1472
                    case MULTIPLE_ANSWER_DROPDOWN_COMBINATION:
1473
                        if ($debug_mark_answer && $answerCorrect) {
1474
                            $s .= '<p>'
1475
                                .(
1476
                                    MULTIPLE_ANSWER_DROPDOWN == $answerType
1477
                                        ? '<span class="pull-right">'.$objAnswerTmp->weighting[$answerId].'</span>'
1478
                                        : ''
1479
                                )
1480
                                .Display::returnFontAwesomeIcon('check-square-o', '', true);
1481
                            $s .= Security::remove_XSS($objAnswerTmp->answer[$answerId]).'</p>';
1482
                        }
1483
                        break;
1484
                }
1485
            }
1486
1487
            if (in_array($answerType, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION])
1488
                && !$debug_mark_answer
1489
            ) {
1490
                $userChoiceList = array_unique($userChoiceList);
1491
                $input_id = "choice-$questionId";
1492
1493
                $s .= Display::input('hidden', "choice2[$questionId]", '0')
1494
                    .'<p>'
1495
                    .Display::select(
1496
                        "choice[$questionId][]",
1497
                        $selectableOptions,
1498
                        $userChoiceList,
1499
                        [
1500
                            'id' => $input_id,
1501
                            'multiple' => 'multiple',
1502
                        ],
1503
                        false
1504
                    )
1505
                    .'</p>'
1506
                    .'<script>$(function () {
1507
                            $(\'#'.$input_id.'\').select2({
1508
                                selectOnClose: false,
1509
                                placeholder: {id: -2, text: "'.get_lang('None').'"}
1510
                            });
1511
                        });</script>'
1512
                    .'<style>
1513
                        .select2-container--default .select2-selection--multiple .select2-selection__rendered li {
1514
                            display:block;
1515
                            width: 100%;
1516
                            white-space: break-spaces;
1517
                        }</style>'
1518
                ;
1519
            }
1520
1521
            if ($show_comment) {
1522
                if (in_array(
1523
                    $answerType,
1524
                    [
1525
                        MULTIPLE_ANSWER,
1526
                        MULTIPLE_ANSWER_COMBINATION,
1527
                        UNIQUE_ANSWER,
1528
                        UNIQUE_ANSWER_IMAGE,
1529
                        UNIQUE_ANSWER_NO_OPTION,
1530
                        GLOBAL_MULTIPLE_ANSWER,
1531
                    ]
1532
                )) {
1533
                    $s .= '</table>';
1534
                }
1535
            } elseif (in_array(
1536
                $answerType,
1537
                [
1538
                    MATCHING,
1539
                    MATCHING_COMBINATION,
1540
                    MATCHING_DRAGGABLE,
1541
                    MATCHING_DRAGGABLE_COMBINATION,
1542
                    UNIQUE_ANSWER_NO_OPTION,
1543
                    MULTIPLE_ANSWER_TRUE_FALSE,
1544
                    MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE,
1545
                    MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY,
1546
                ]
1547
            )) {
1548
                $s .= '</table>';
1549
            }
1550
1551
            if ($answerType == DRAGGABLE) {
1552
                $isVertical = $objQuestionTmp->extra == 'v';
1553
                $s .= "
1554
                           </ul>
1555
                        </div><!-- .col-md-12 -->
1556
                    </div><!-- .row -->
1557
                ";
1558
                $counterAnswer = 1;
1559
                $s .= $isVertical ? '' : '<div class="row">';
1560
                for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
1561
                    $answerCorrect = $objAnswerTmp->isCorrect($answerId);
1562
                    $windowId = $questionId.'_'.$counterAnswer;
1563
                    if ($answerCorrect) {
1564
                        $s .= $isVertical ? '<div class="row">' : '';
1565
                        $s .= '
1566
                            <div class="'.($isVertical ? 'col-md-12' : 'col-xs-12 col-sm-4 col-md-3 col-lg-2').'">
1567
                                <div class="droppable-item">
1568
                                    <span class="number">'.$counterAnswer.'.</span>
1569
                                    <div id="drop_'.$windowId.'" class="droppable">
1570
                                    </div>
1571
                                 </div>
1572
                            </div>
1573
                        ';
1574
                        $s .= $isVertical ? '</div>' : '';
1575
                        $counterAnswer++;
1576
                    }
1577
                }
1578
1579
                $s .= $isVertical ? '' : '</div>'; // row
1580
//                $s .= '</div>';
1581
            }
1582
1583
            if (in_array($answerType, [MATCHING, MATCHING_COMBINATION, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION])) {
1584
                $s .= '</div>'; //drag_question
1585
            }
1586
1587
            $s .= '</div>'; //question_options row
1588
1589
            // destruction of the Answer object
1590
            unset($objAnswerTmp);
1591
            // destruction of the Question object
1592
            unset($objQuestionTmp);
1593
            if ('export' == $origin) {
1594
                return $s;
1595
            }
1596
            echo $s;
1597
        } elseif (in_array($answerType, [HOT_SPOT, HOT_SPOT_DELINEATION, HOT_SPOT_COMBINATION])) {
1598
            global $exe_id;
1599
            // Question is a HOT_SPOT
1600
            // Checking document/images visibility
1601
            if (api_is_platform_admin() || api_is_course_admin()) {
1602
                $doc_id = $objQuestionTmp->getPictureId();
1603
                if (is_numeric($doc_id)) {
1604
                    $images_folder_visibility = api_get_item_visibility(
1605
                        $course,
1606
                        'document',
1607
                        $doc_id,
1608
                        api_get_session_id()
1609
                    );
1610
                    if (!$images_folder_visibility) {
1611
                        // Show only to the course/platform admin if the image is set to visibility = false
1612
                        echo Display::return_message(
1613
                            get_lang('ChangeTheVisibilityOfTheCurrentImage'),
1614
                            'warning'
1615
                        );
1616
                    }
1617
                }
1618
            }
1619
            $questionDescription = $objQuestionTmp->selectDescription();
1620
1621
            // Get the answers, make a list
1622
            $objAnswerTmp = new Answer($questionId, $course_id);
1623
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
1624
1625
            // get answers of hotpost
1626
            $answers_hotspot = [];
1627
            for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
1628
                $answers = $objAnswerTmp->selectAnswerByAutoId(
1629
                    $objAnswerTmp->selectAutoId($answerId)
1630
                );
1631
                $answers_hotspot[$answers['iid']] = $objAnswerTmp->selectAnswer(
1632
                    $answerId
1633
                );
1634
            }
1635
1636
            $answerList = '';
1637
            $hotspotColor = 0;
1638
            if ($answerType != HOT_SPOT_DELINEATION) {
1639
                $answerList = '
1640
                    <div class="well well-sm">
1641
                        <h5 class="page-header">'.get_lang('HotspotZones').'</h5>
1642
                        <ol>
1643
                ';
1644
1645
                if (!empty($answers_hotspot)) {
1646
                    Session::write("hotspot_ordered$questionId", array_keys($answers_hotspot));
1647
                    foreach ($answers_hotspot as $value) {
1648
                        $answerList .= '<li>';
1649
                        if ($freeze) {
1650
                            $answerList .= '<span class="hotspot-color-'.$hotspotColor
1651
                                .' fa fa-square" aria-hidden="true"></span>'.PHP_EOL;
1652
                        }
1653
                        $answerList .= $value;
1654
                        $answerList .= '</li>';
1655
                        $hotspotColor++;
1656
                    }
1657
                }
1658
1659
                $answerList .= '
1660
                        </ol>
1661
                    </div>
1662
                ';
1663
            }
1664
1665
            if ($freeze) {
1666
                $relPath = api_get_path(WEB_CODE_PATH);
1667
                echo "
1668
                    <div class=\"row\">
1669
                        <div class=\"col-sm-9\">
1670
                            <div id=\"hotspot-preview-$questionId\"></div>
1671
                        </div>
1672
                        <div class=\"col-sm-3\">
1673
                            $answerList
1674
                        </div>
1675
                    </div>
1676
                    <script>
1677
                        new ".(in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION]) ? "HotspotQuestion" : "DelineationQuestion")."({
1678
                            questionId: $questionId,
1679
                            exerciseId: {$exercise->iid},
1680
                            exeId: 0,
1681
                            selector: '#hotspot-preview-$questionId',
1682
                            'for': 'preview',
1683
                            relPath: '$relPath'
1684
                        });
1685
                    </script>
1686
                ";
1687
1688
                return;
1689
            }
1690
1691
            if (!$only_questions) {
1692
                if ($show_title) {
1693
                    if ($exercise->display_category_name) {
1694
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->iid);
1695
                    }
1696
                    echo $objQuestionTmp->getTitleToDisplay($current_item, $exerciseId);
1697
                }
1698
1699
                if ($questionRequireAuth) {
1700
                    WhispeakAuthPlugin::quizQuestionAuthentify($questionId, $exercise);
1701
1702
                    return false;
1703
                }
1704
1705
                //@todo I need to the get the feedback type
1706
                echo <<<HOTSPOT
1707
                    <input type="hidden" name="hidden_hotspot_id" value="$questionId" />
1708
                    <div class="exercise_questions">
1709
                        $questionDescription
1710
                        <div class="row">
1711
HOTSPOT;
1712
            }
1713
1714
            $relPath = api_get_path(WEB_CODE_PATH);
1715
            $s .= "<div class=\"col-sm-8 col-md-9\">
1716
                   <div class=\"hotspot-image\"></div>
1717
                    <script>
1718
                        $(function() {
1719
                            new ".($answerType == HOT_SPOT_DELINEATION ? 'DelineationQuestion' : 'HotspotQuestion')."({
1720
                                questionId: $questionId,
1721
                                exerciseId: {$exercise->iid},
1722
                                exeId: 0,
1723
                                selector: '#question_div_' + $questionId + ' .hotspot-image',
1724
                                'for': 'user',
1725
                                relPath: '$relPath'
1726
                            });
1727
                        });
1728
                    </script>
1729
                </div>
1730
                <div class=\"col-sm-4 col-md-3\">
1731
                    $answerList
1732
                </div>
1733
            ";
1734
1735
            echo <<<HOTSPOT
1736
                            $s
1737
                        </div>
1738
                    </div>
1739
HOTSPOT;
1740
        } elseif ($answerType == ANNOTATION) {
1741
            global $exe_id;
1742
            $relPath = api_get_path(WEB_CODE_PATH);
1743
            if (api_is_platform_admin() || api_is_course_admin()) {
1744
                $docId = DocumentManager::get_document_id($course, '/images/'.$pictureName);
1745
                if ($docId) {
1746
                    $images_folder_visibility = api_get_item_visibility(
1747
                        $course,
1748
                        'document',
1749
                        $docId,
1750
                        api_get_session_id()
1751
                    );
1752
1753
                    if (!$images_folder_visibility) {
1754
                        echo Display::return_message(get_lang('ChangeTheVisibilityOfTheCurrentImage'), 'warning');
1755
                    }
1756
                }
1757
1758
                if ($freeze) {
1759
                    echo Display::img(
1760
                        api_get_path(WEB_COURSE_PATH).$course['path'].'/document/images/'.$pictureName,
1761
                        $objQuestionTmp->selectTitle(),
1762
                        ['width' => '600px']
1763
                    );
1764
1765
                    return 0;
1766
                }
1767
            }
1768
1769
            if (!$only_questions) {
1770
                if ($show_title) {
1771
                    if ($exercise->display_category_name) {
1772
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->iid);
1773
                    }
1774
                    echo $objQuestionTmp->getTitleToDisplay($current_item, $exerciseId);
1775
                }
1776
1777
                if ($questionRequireAuth) {
1778
                    WhispeakAuthPlugin::quizQuestionAuthentify($questionId, $exercise);
1779
1780
                    return false;
1781
                }
1782
1783
                echo '
1784
                    <input type="hidden" name="hidden_hotspot_id" value="'.$questionId.'" />
1785
                    <div class="exercise_questions">
1786
                        '.$objQuestionTmp->selectDescription().'
1787
                        <div class="row">
1788
                            <div class="col-sm-8 col-md-9">
1789
                                <div id="annotation-canvas-'.$questionId.'" class="annotation-canvas center-block">
1790
                                </div>
1791
                            </div>
1792
                            <div class="col-sm-4 col-md-3" id="annotation-toolbar-'.$questionId.'">
1793
                                <div class="btn-toolbar" style="margin-top: 0;">
1794
                                    <div class="btn-group" data-toggle="buttons">
1795
                                        <label class="btn btn-default active"
1796
                                            aria-label="'.get_lang('AddAnnotationPath').'">
1797
                                            <input
1798
                                                type="radio" value="0"
1799
                                                name="'.$questionId.'-options" autocomplete="off" checked>
1800
                                            <span class="fa fa-pencil" aria-hidden="true"></span>
1801
                                        </label>
1802
                                        <label class="btn btn-default"
1803
                                            aria-label="'.get_lang('AddAnnotationText').'">
1804
                                            <input
1805
                                                type="radio" value="1"
1806
                                                name="'.$questionId.'-options" autocomplete="off">
1807
                                            <span class="fa fa-font fa-fw" aria-hidden="true"></span>
1808
                                        </label>
1809
                                    </div>
1810
                                    <div class="btn-group">
1811
                                        <button type="button" class="btn btn-default btn-small"
1812
                                            title="'.get_lang('ClearAnswers').'"
1813
                                            id="btn-reset-'.$questionId.'">
1814
                                            <span class="fa fa-times-rectangle fa-fw" aria-hidden="true"></span>
1815
                                        </button>
1816
                                    </div>
1817
                                    <div class="btn-group">
1818
                                        <button type="button" class="btn btn-default"
1819
                                            title="'.get_lang('Undo').'"
1820
                                            id="btn-undo-'.$questionId.'">
1821
                                            <span class="fa fa-undo fa-fw" aria-hidden="true"></span>
1822
                                        </button>
1823
                                        <button type="button" class="btn btn-default"
1824
                                            title="'.get_lang('Redo').'"
1825
                                            id="btn-redo-'.$questionId.'">
1826
                                            <span class="fa fa-repeat fa-fw" aria-hidden="true"></span>
1827
                                        </button>
1828
                                    </div>
1829
                                </div>
1830
                            </div>
1831
                        </div>
1832
                        <script>
1833
                            AnnotationQuestion({
1834
                                questionId: '.$questionId.',
1835
                                exerciseId: '.$exe_id.',
1836
                                relPath: \''.$relPath.'\',
1837
                                courseId: '.$course_id.',
1838
                            });
1839
                        </script>
1840
                    </div>
1841
                ';
1842
            }
1843
            $objAnswerTmp = new Answer($questionId);
1844
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
1845
            unset($objAnswerTmp, $objQuestionTmp);
1846
        }
1847
1848
        return $nbrAnswers;
1849
    }
1850
1851
    /**
1852
     * Get an HTML string with the list of exercises where the given question
1853
     * is being used.
1854
     *
1855
     * @param int $questionId    The iid of the question being observed
1856
     * @param int $excludeTestId If defined, exclude this (current) test from the list of results
1857
     *
1858
     * @return string An HTML string containing a div and a table
1859
     */
1860
    public static function showTestsWhereQuestionIsUsed(int $questionId, int $excludeTestId = 0)
1861
    {
1862
        $questionId = (int) $questionId;
1863
        $sql = "SELECT qz.title quiz_title,
1864
                        c.title course_title,
1865
                        s.name session_name,
1866
                        qz.iid as quiz_id,
1867
                        qz.c_id,
1868
                        qz.session_id
1869
                FROM c_quiz qz,
1870
                    c_quiz_rel_question qq,
1871
                    course c,
1872
                    session s
1873
                WHERE qz.c_id = c.id AND
1874
                    (qz.session_id = s.id OR qz.session_id = 0) AND
1875
                    qq.exercice_id = qz.iid AND ";
1876
        if (!empty($excludeTestId)) {
1877
            $excludeTestId = (int) $excludeTestId;
1878
            $sql .= " qz.iid != $excludeTestId AND ";
1879
        }
1880
        $sql .= "     qq.question_id = $questionId
1881
                GROUP BY qq.iid";
1882
1883
        $result = [];
1884
        $html = "";
1885
1886
        $sqlResult = Database::query($sql);
1887
1888
        if (Database::num_rows($sqlResult) != 0) {
1889
            while ($row = Database::fetch_array($sqlResult, 'ASSOC')) {
1890
                $tmp = [];
1891
                $tmp[0] = $row['course_title'];
1892
                $tmp[1] = $row['session_name'];
1893
                $tmp[2] = $row['quiz_title'];
1894
                $courseDetails = api_get_course_info_by_id($row['c_id']);
1895
                $courseCode = $courseDetails['code'];
1896
                // Send do other test with r=1 to reset current test session variables
1897
                $urlToQuiz = api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_cidreq_params($courseCode, $row['session_id']).'&exerciseId='.$row['quiz_id'].'&r=1';
1898
                $tmp[3] = '<a href="'.$urlToQuiz.'">'.Display::return_icon('quiz.png', get_lang('Edit')).'</a>';
1899
                if ((int) $row['session_id'] == 0) {
1900
                    $tmp[1] = '-';
1901
                }
1902
1903
                $result[] = $tmp;
1904
            }
1905
1906
            $headers = [
1907
                get_lang('Course'),
1908
                get_lang('Session'),
1909
                get_lang('Quiz'),
1910
                get_lang('LinkToTestEdition'),
1911
            ];
1912
1913
            $title = Display::div(
1914
                get_lang('QuestionAlsoUsedInTheFollowingTests'),
1915
                [
1916
                    'class' => 'section-title',
1917
                    'style' => 'margin-top: 25px; border-bottom: none',
1918
                ]
1919
            );
1920
1921
            $html = $title.Display::table($headers, $result);
1922
        }
1923
1924
        echo $html;
1925
    }
1926
1927
    /**
1928
     * Get the table as array of results of exercises attempts with questions score.
1929
     *
1930
     * @return array
1931
     */
1932
    public static function getTrackExerciseAttemptsTable(Exercise $objExercise)
1933
    {
1934
        $tblQuiz = Database::get_course_table(TABLE_QUIZ_TEST);
1935
        $tblTrackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1936
1937
        $exerciseId = $objExercise->iid;
1938
        $courseId = $objExercise->course_id;
1939
        $sessionId = (int) $objExercise->sessionId;
1940
        $questionList = $objExercise->getQuestionForTeacher(0, $objExercise->getQuestionCount());
1941
1942
        $headers = [
1943
            get_lang('UserName'),
1944
            get_lang('Email'),
1945
        ];
1946
1947
        $extraField = new ExtraField('user');
1948
        $extraFieldValue = new ExtraFieldValue('user');
1949
        $extraFieldQuestion = new ExtraFieldValue('question');
1950
1951
        $extraFields = $extraField->get_all(['filter = ?' => 1]);
1952
        $userExtraFields = [];
1953
        if (!empty($extraFields)) {
1954
            foreach ($extraFields as $extra) {
1955
                $headers[] = $extra['display_text'];
1956
                $userExtraFields[] = $extra['variable'];
1957
            }
1958
        }
1959
1960
        $headersXls = $headers;
1961
        if (!empty($questionList)) {
1962
            foreach ($questionList as $questionId) {
1963
                $questionObj = Question::read($questionId);
1964
                $questionName = cut($questionObj->question, 200);
1965
                $headers[] = '<a href="#" title="'.$questionName.'">'.$questionId.'</a>';
1966
                $headersXls[] = $questionName;
1967
            }
1968
        }
1969
1970
        $sql = "SELECT
1971
                    te.exe_id,
1972
                    te.exe_user_id
1973
                FROM
1974
                    $tblTrackExercises te
1975
                INNER JOIN
1976
                    $tblQuiz q ON (q.iid = te.exe_exo_id AND q.c_id = te.c_id)
1977
                WHERE
1978
                    te.c_id = $courseId AND
1979
                    te.session_id = $sessionId AND
1980
                    te.status = '' AND
1981
                    te.exe_exo_id = $exerciseId
1982
                ";
1983
        $rs = Database::query($sql);
1984
        $data = [];
1985
        if (Database::num_rows($rs) > 0) {
1986
            $x = 0;
1987
            while ($row = Database::fetch_array($rs)) {
1988
                $userInfo = api_get_user_info($row['exe_user_id']);
1989
                $data[$x]['username'] = $userInfo['username'];
1990
                $data[$x]['email'] = $userInfo['email'];
1991
                if (!empty($userExtraFields)) {
1992
                    foreach ($userExtraFields as $variable) {
1993
                        $extra = $extraFieldValue->get_values_by_handler_and_field_variable(
1994
                            $row['exe_user_id'],
1995
                            $variable
1996
                        );
1997
                        $data[$x][$variable] = $extra['value'] ?? '';
1998
                    }
1999
                }
2000
2001
                // the questions
2002
                if (!empty($questionList)) {
2003
                    foreach ($questionList as $questionId) {
2004
                        $questionObj = Question::read($questionId);
2005
                        $questionName = cut($questionObj->question, 200);
2006
                        $questionResult = $objExercise->manage_answer(
2007
                            $row['exe_id'],
2008
                            $questionId,
2009
                            '',
2010
                            'exercise_show',
2011
                            [],
2012
                            false,
2013
                            true,
2014
                            false,
2015
                            $objExercise->selectPropagateNeg()
2016
                        );
2017
2018
                        $displayValue = $questionResult['score'];
2019
                        $differentiation = $extraFieldQuestion->get_values_by_handler_and_field_variable($questionId, 'differentiation');
2020
                        if (!empty($differentiation['value'])) {
2021
                            $answerType = $questionObj->selectType();
2022
                            $objAnswerTmp = new Answer($questionId, api_get_course_int_id());
2023
                            $userChoice = [];
2024
                            if (!empty($questionResult['correct_answer_id']) && HOT_SPOT_DELINEATION != $answerType) {
2025
                                foreach ($questionResult['correct_answer_id'] as $answerId) {
2026
                                    $answer = $objAnswerTmp->getAnswerByAutoId($answerId);
2027
                                    if (!empty($answer)) {
2028
                                        $userChoice[] = $answer['answer'];
2029
                                    } else {
2030
                                        $answer = $objAnswerTmp->selectAnswer($answerId);
2031
                                        $userChoice[] = $answer;
2032
                                    }
2033
                                }
2034
                            }
2035
                            if (!empty($userChoice)) {
2036
                                $displayValue = implode('|', $userChoice);
2037
                            }
2038
                        }
2039
                        $questionModalUrl = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=show_question_attempt&exercise='.$exerciseId.'&question='.$questionId.'&exe_id='.$row['exe_id'];
2040
                        $data[$x][$questionId] = '<a href="'.$questionModalUrl.'" class="ajax" data-title="'.$questionName.'" title="'.get_lang('ClicToSeeDetails').'">'.$displayValue.'</a>';
2041
                    }
2042
                }
2043
                $x++;
2044
            }
2045
        }
2046
2047
        $table['headers'] = $headers;
2048
        $table['headers_xls'] = $headersXls;
2049
        $table['rows'] = $data;
2050
2051
        return $table;
2052
    }
2053
2054
    /**
2055
     * @param int $exeId
2056
     *
2057
     * @return array
2058
     */
2059
    public static function get_exercise_track_exercise_info($exeId)
2060
    {
2061
        $quizTable = Database::get_course_table(TABLE_QUIZ_TEST);
2062
        $trackExerciseTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2063
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
2064
        $exeId = (int) $exeId;
2065
        $result = [];
2066
        if (!empty($exeId)) {
2067
            $sql = " SELECT q.*, tee.*
2068
                FROM $quizTable as q
2069
                INNER JOIN $trackExerciseTable as tee
2070
                ON q.iid = tee.exe_exo_id
2071
                INNER JOIN $courseTable c
2072
                ON c.id = tee.c_id
2073
                WHERE tee.exe_id = $exeId
2074
                AND q.c_id = c.id";
2075
2076
            $sqlResult = Database::query($sql);
2077
            if (Database::num_rows($sqlResult)) {
2078
                $result = Database::fetch_array($sqlResult, 'ASSOC');
2079
                $result['duration_formatted'] = '';
2080
                if (!empty($result['exe_duration'])) {
2081
                    $time = api_format_time($result['exe_duration'], 'js');
2082
                    $result['duration_formatted'] = $time;
2083
                }
2084
            }
2085
        }
2086
2087
        return $result;
2088
    }
2089
2090
    /**
2091
     * Validates the time control key.
2092
     *
2093
     * @param int $exercise_id
2094
     * @param int $lp_id
2095
     * @param int $lp_item_id
2096
     *
2097
     * @return bool
2098
     */
2099
    public static function exercise_time_control_is_valid(
2100
        $exercise_id,
2101
        $lp_id = 0,
2102
        $lp_item_id = 0
2103
    ) {
2104
        $course_id = api_get_course_int_id();
2105
        $exercise_id = (int) $exercise_id;
2106
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
2107
        $sql = "SELECT expired_time FROM $table
2108
                WHERE iid = $exercise_id";
2109
        $result = Database::query($sql);
2110
        $row = Database::fetch_array($result, 'ASSOC');
2111
        if (!empty($row['expired_time'])) {
2112
            $current_expired_time_key = self::get_time_control_key(
2113
                $exercise_id,
2114
                $lp_id,
2115
                $lp_item_id
2116
            );
2117
            if (isset($_SESSION['expired_time'][$current_expired_time_key])) {
2118
                $current_time = time();
2119
                $expired_time = api_strtotime(
2120
                    $_SESSION['expired_time'][$current_expired_time_key],
2121
                    'UTC'
2122
                );
2123
                $total_time_allowed = $expired_time + 30;
2124
                if ($total_time_allowed < $current_time) {
2125
                    return false;
2126
                }
2127
2128
                return true;
2129
            }
2130
2131
            return false;
2132
        }
2133
2134
        return true;
2135
    }
2136
2137
    /**
2138
     * Deletes the time control token.
2139
     *
2140
     * @param int $exercise_id
2141
     * @param int $lp_id
2142
     * @param int $lp_item_id
2143
     */
2144
    public static function exercise_time_control_delete(
2145
        $exercise_id,
2146
        $lp_id = 0,
2147
        $lp_item_id = 0
2148
    ) {
2149
        $current_expired_time_key = self::get_time_control_key(
2150
            $exercise_id,
2151
            $lp_id,
2152
            $lp_item_id
2153
        );
2154
        unset($_SESSION['expired_time'][$current_expired_time_key]);
2155
    }
2156
2157
    /**
2158
     * Generates the time control key.
2159
     *
2160
     * @param int $exercise_id
2161
     * @param int $lp_id
2162
     * @param int $lp_item_id
2163
     *
2164
     * @return string
2165
     */
2166
    public static function get_time_control_key(
2167
        $exercise_id,
2168
        $lp_id = 0,
2169
        $lp_item_id = 0
2170
    ) {
2171
        $exercise_id = (int) $exercise_id;
2172
        $lp_id = (int) $lp_id;
2173
        $lp_item_id = (int) $lp_item_id;
2174
2175
        return
2176
            api_get_course_int_id().'_'.
2177
            api_get_session_id().'_'.
2178
            $exercise_id.'_'.
2179
            api_get_user_id().'_'.
2180
            $lp_id.'_'.
2181
            $lp_item_id;
2182
    }
2183
2184
    /**
2185
     * Get session time control.
2186
     *
2187
     * @param int $exercise_id
2188
     * @param int $lp_id
2189
     * @param int $lp_item_id
2190
     *
2191
     * @return int
2192
     */
2193
    public static function get_session_time_control_key(
2194
        $exercise_id,
2195
        $lp_id = 0,
2196
        $lp_item_id = 0
2197
    ) {
2198
        $return_value = 0;
2199
        $time_control_key = self::get_time_control_key(
2200
            $exercise_id,
2201
            $lp_id,
2202
            $lp_item_id
2203
        );
2204
        if (isset($_SESSION['expired_time']) && isset($_SESSION['expired_time'][$time_control_key])) {
2205
            $return_value = $_SESSION['expired_time'][$time_control_key];
2206
        }
2207
2208
        return $return_value;
2209
    }
2210
2211
    /**
2212
     * Gets count of exam results.
2213
     *
2214
     * @param int    $exerciseId
2215
     * @param array  $conditions
2216
     * @param string $courseCode
2217
     * @param bool   $showSession
2218
     * @param bool   $searchAllTeacherCourses
2219
     * @param int    $status
2220
     *
2221
     * @return array
2222
     */
2223
    public static function get_count_exam_results(
2224
        $exerciseId,
2225
        $conditions,
2226
        $courseCode = '',
2227
        $showSession = false,
2228
        $searchAllTeacherCourses = false,
2229
        $status = 0
2230
    ) {
2231
        return self::get_exam_results_data(
2232
            null,
2233
            null,
2234
            null,
2235
            null,
2236
            $exerciseId,
2237
            $conditions,
2238
            true,
2239
            $courseCode,
2240
            $showSession,
2241
            false,
2242
            [],
2243
            false,
2244
            false,
2245
            false,
2246
            $searchAllTeacherCourses,
2247
            $status
2248
        );
2249
    }
2250
2251
    /**
2252
     * @param string $path
2253
     *
2254
     * @return int
2255
     */
2256
    public static function get_count_exam_hotpotatoes_results($path)
2257
    {
2258
        return self::get_exam_results_hotpotatoes_data(
2259
            0,
2260
            0,
2261
            '',
2262
            '',
2263
            $path,
2264
            true,
2265
            ''
2266
        );
2267
    }
2268
2269
    /**
2270
     * @param int    $in_from
2271
     * @param int    $in_number_of_items
2272
     * @param int    $in_column
2273
     * @param int    $in_direction
2274
     * @param string $in_hotpot_path
2275
     * @param bool   $in_get_count
2276
     * @param null   $where_condition
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $where_condition is correct as it would always require null to be passed?
Loading history...
2277
     *
2278
     * @return array|int
2279
     */
2280
    public static function get_exam_results_hotpotatoes_data(
2281
        $in_from,
2282
        $in_number_of_items,
2283
        $in_column,
2284
        $in_direction,
2285
        $in_hotpot_path,
2286
        $in_get_count = false,
2287
        $where_condition = null
2288
    ) {
2289
        $courseId = api_get_course_int_id();
2290
        // by default in_column = 1 If parameters given, it is the name of the column witch is the bdd field name
2291
        if ($in_column == 1) {
2292
            $in_column = 'firstname';
2293
        }
2294
        $in_hotpot_path = Database::escape_string($in_hotpot_path);
2295
        $in_direction = Database::escape_string($in_direction);
2296
        $in_direction = !in_array(strtolower(trim($in_direction)), ['asc', 'desc']) ? 'asc' : $in_direction;
2297
        $in_column = Database::escape_string($in_column);
2298
        $in_number_of_items = (int) $in_number_of_items;
2299
        $in_from = (int) $in_from;
2300
2301
        $TBL_TRACK_HOTPOTATOES = Database::get_main_table(
2302
            TABLE_STATISTIC_TRACK_E_HOTPOTATOES
2303
        );
2304
        $TBL_USER = Database::get_main_table(TABLE_MAIN_USER);
2305
2306
        $sql = "SELECT *, thp.id AS thp_id
2307
                FROM $TBL_TRACK_HOTPOTATOES thp
2308
                JOIN $TBL_USER u
2309
                ON thp.exe_user_id = u.user_id
2310
                WHERE
2311
                    thp.c_id = $courseId AND
2312
                    exe_name LIKE '$in_hotpot_path%'";
2313
2314
        // just count how many answers
2315
        if ($in_get_count) {
2316
            $res = Database::query($sql);
2317
2318
            return Database::num_rows($res);
2319
        }
2320
        // get a number of sorted results
2321
        $sql .= " $where_condition
2322
            ORDER BY `$in_column` $in_direction
2323
            LIMIT $in_from, $in_number_of_items";
2324
2325
        $res = Database::query($sql);
2326
        $result = [];
2327
        $apiIsAllowedToEdit = api_is_allowed_to_edit();
2328
        $urlBase = api_get_path(WEB_CODE_PATH).
2329
            'exercise/hotpotatoes_exercise_report.php?action=delete&'.
2330
            api_get_cidreq().'&id=';
2331
        while ($data = Database::fetch_array($res)) {
2332
            $actions = null;
2333
2334
            if ($apiIsAllowedToEdit) {
2335
                $url = $urlBase.$data['thp_id'].'&path='.$data['exe_name'];
2336
                $actions = Display::url(
2337
                    Display::return_icon('delete.png', get_lang('Delete')),
2338
                    $url
2339
                );
2340
            }
2341
2342
            $result[] = [
2343
                'firstname' => $data['firstname'],
2344
                'lastname' => $data['lastname'],
2345
                'username' => $data['username'],
2346
                'group_name' => implode(
2347
                    '<br/>',
2348
                    GroupManager::get_user_group_name($data['user_id'])
2349
                ),
2350
                'exe_date' => $data['exe_date'],
2351
                'score' => $data['exe_result'].' / '.$data['exe_weighting'],
2352
                'actions' => $actions,
2353
            ];
2354
        }
2355
2356
        return $result;
2357
    }
2358
2359
    /**
2360
     * @param string $exercisePath
2361
     * @param int    $userId
2362
     * @param int    $courseId
2363
     * @param int    $sessionId
2364
     *
2365
     * @return array
2366
     */
2367
    public static function getLatestHotPotatoResult(
2368
        $exercisePath,
2369
        $userId,
2370
        $courseId,
2371
        $sessionId
2372
    ) {
2373
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES);
2374
        $exercisePath = Database::escape_string($exercisePath);
2375
        $userId = (int) $userId;
2376
        $courseId = (int) $courseId;
2377
2378
        $sql = "SELECT * FROM $table
2379
                WHERE
2380
                    c_id = $courseId AND
2381
                    exe_name LIKE '$exercisePath%' AND
2382
                    exe_user_id = $userId
2383
                ORDER BY id
2384
                LIMIT 1";
2385
        $result = Database::query($sql);
2386
        $attempt = [];
2387
        if (Database::num_rows($result)) {
2388
            $attempt = Database::fetch_array($result, 'ASSOC');
2389
        }
2390
2391
        return $attempt;
2392
    }
2393
2394
    /**
2395
     * Export the pending attempts to excel.
2396
     *
2397
     * @params $values
2398
     */
2399
    public static function exportPendingAttemptsToExcel($values)
2400
    {
2401
        $headers = [
2402
            get_lang('Course'),
2403
            get_lang('Exercise'),
2404
            get_lang('FirstName'),
2405
            get_lang('LastName'),
2406
            get_lang('LoginName'),
2407
            get_lang('Duration').' ('.get_lang('MinMinute').')',
2408
            get_lang('StartDate'),
2409
            get_lang('EndDate'),
2410
            get_lang('Score'),
2411
            get_lang('IP'),
2412
            get_lang('Status'),
2413
            get_lang('Corrector'),
2414
            get_lang('CorrectionDate'),
2415
        ];
2416
        $tableXls[] = $headers;
2417
2418
        $courseId = $values['course_id'] ?? 0;
2419
        $exerciseId = $values['exercise_id'] ?? 0;
2420
        $status = $values['status'] ?? 0;
2421
        $whereCondition = '';
2422
        if (isset($_GET['filter_by_user']) && !empty($_GET['filter_by_user'])) {
2423
            $filter_user = (int) $_GET['filter_by_user'];
2424
            if (empty($whereCondition)) {
2425
                $whereCondition .= " te.exe_user_id  = '$filter_user'";
2426
            } else {
2427
                $whereCondition .= " AND te.exe_user_id  = '$filter_user'";
2428
            }
2429
        }
2430
2431
        if (isset($_GET['group_id_in_toolbar']) && !empty($_GET['group_id_in_toolbar'])) {
2432
            $groupIdFromToolbar = (int) $_GET['group_id_in_toolbar'];
2433
            if (!empty($groupIdFromToolbar)) {
2434
                if (empty($whereCondition)) {
2435
                    $whereCondition .= " te.group_id  = '$groupIdFromToolbar'";
2436
                } else {
2437
                    $whereCondition .= " AND group_id  = '$groupIdFromToolbar'";
2438
                }
2439
            }
2440
        }
2441
2442
        if (!empty($whereCondition)) {
2443
            $whereCondition = " AND $whereCondition";
2444
        }
2445
2446
        if (!empty($courseId)) {
2447
            $whereCondition .= " AND te.c_id = $courseId";
2448
        }
2449
2450
        $result = ExerciseLib::get_exam_results_data(
2451
            0,
2452
            10000000,
2453
            'c_id',
2454
            'asc',
2455
            $exerciseId,
2456
            $whereCondition,
2457
            false,
2458
            null,
2459
            false,
2460
            false,
2461
            [],
2462
            false,
2463
            false,
2464
            false,
2465
            true,
2466
            $status
2467
        );
2468
2469
        if (!empty($result)) {
2470
            foreach ($result as $attempt) {
2471
                $data = [
2472
                    $attempt['course'],
2473
                    $attempt['exercise'],
2474
                    $attempt['firstname'],
2475
                    $attempt['lastname'],
2476
                    $attempt['username'],
2477
                    $attempt['exe_duration'],
2478
                    $attempt['start_date'],
2479
                    $attempt['exe_date'],
2480
                    strip_tags($attempt['score']),
2481
                    $attempt['user_ip'],
2482
                    strip_tags($attempt['status']),
2483
                    $attempt['qualificator_fullname'],
2484
                    $attempt['date_of_qualification'],
2485
                ];
2486
                $tableXls[] = $data;
2487
            }
2488
        }
2489
2490
        $fileName = get_lang('PendingAttempts').'_'.api_get_local_time();
2491
        Export::arrayToXls($tableXls, $fileName);
2492
2493
        return true;
2494
    }
2495
2496
    /**
2497
     * Gets exercise results.
2498
     *
2499
     * @todo this function should be moved in a library  + no global calls
2500
     *
2501
     * @param int    $from
2502
     * @param int    $number_of_items
2503
     * @param int    $column
2504
     * @param string $direction
2505
     * @param int    $exercise_id
2506
     * @param null   $extra_where_conditions
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $extra_where_conditions is correct as it would always require null to be passed?
Loading history...
2507
     * @param bool   $get_count
2508
     * @param string $courseCode
2509
     * @param bool   $showSessionField
2510
     * @param bool   $showExerciseCategories
2511
     * @param array  $userExtraFieldsToAdd
2512
     * @param bool   $useCommaAsDecimalPoint
2513
     * @param bool   $roundValues
2514
     * @param bool   $getOnlyIds
2515
     *
2516
     * @return array
2517
     */
2518
    public static function get_exam_results_data(
2519
        $from,
2520
        $number_of_items,
2521
        $column,
2522
        $direction,
2523
        $exercise_id,
2524
        $extra_where_conditions = null,
2525
        $get_count = false,
2526
        $courseCode = null,
2527
        $showSessionField = false,
2528
        $showExerciseCategories = false,
2529
        $userExtraFieldsToAdd = [],
2530
        $useCommaAsDecimalPoint = false,
2531
        $roundValues = false,
2532
        $getOnlyIds = false,
2533
        $searchAllTeacherCourses = false,
2534
        $status = 0
2535
    ) {
2536
        //@todo replace all this globals
2537
        global $filter;
2538
        $courseCode = empty($courseCode) ? api_get_course_id() : $courseCode;
2539
        $courseInfo = api_get_course_info($courseCode);
2540
        $documentPath = '';
2541
        $sessionId = api_get_session_id();
2542
        $courseId = 0;
2543
        if (!empty($courseInfo)) {
2544
            $courseId = $courseInfo['real_id'];
2545
            $documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
2546
        }
2547
2548
        $is_allowedToEdit =
2549
            api_is_allowed_to_edit(null, true) ||
2550
            api_is_allowed_to_edit(true) ||
2551
            api_is_drh() ||
2552
            api_is_student_boss() ||
2553
            api_is_session_admin();
2554
2555
        $courseCondition = "c_id = $courseId";
2556
        $statusCondition = '';
2557
2558
        if (!empty($status)) {
2559
            switch ($status) {
2560
                case 2:
2561
                    // validated
2562
                    $statusCondition = ' AND revised = 1 ';
2563
                    break;
2564
                case 3:
2565
                    // not validated
2566
                    $statusCondition = ' AND revised = 0 ';
2567
                    break;
2568
            }
2569
        }
2570
2571
        if (false === $searchAllTeacherCourses) {
2572
            if (empty($courseInfo)) {
2573
                return [];
2574
            }
2575
        } else {
2576
            $courses = CourseManager::get_courses_list_by_user_id(api_get_user_id(), false, false, false);
2577
2578
            if (empty($courses)) {
2579
                return [];
2580
            }
2581
2582
            $courses = array_column($courses, 'real_id');
2583
            $is_allowedToEdit = true;
2584
            $courseCondition = "c_id IN ('".implode("', '", $courses)."') ";
2585
        }
2586
2587
        $exercise_id = (int) $exercise_id;
2588
2589
        $TBL_USER = Database::get_main_table(TABLE_MAIN_USER);
2590
        $TBL_EXERCICES = Database::get_course_table(TABLE_QUIZ_TEST);
2591
        $TBL_GROUP_REL_USER = Database::get_course_table(TABLE_GROUP_USER);
2592
        $TBL_GROUP = Database::get_course_table(TABLE_GROUP);
2593
        $TBL_TRACK_EXERCICES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2594
        $TBL_TRACK_HOTPOTATOES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES);
2595
        $TBL_TRACK_ATTEMPT_RECORDING = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING);
2596
2597
        $session_id_and = '';
2598
        $sessionCondition = '';
2599
        if (!$showSessionField) {
2600
            $session_id_and = " AND te.session_id = $sessionId ";
2601
            $sessionCondition = " AND ttte.session_id = $sessionId";
2602
        }
2603
2604
        if ($searchAllTeacherCourses) {
2605
            $session_id_and = " AND te.session_id = 0 ";
2606
            $sessionCondition = " AND ttte.session_id = 0";
2607
        }
2608
2609
        if (empty($sessionId) &&
2610
            api_get_configuration_value('show_exercise_session_attempts_in_base_course')
2611
        ) {
2612
            $session_id_and = '';
2613
            $sessionCondition = '';
2614
        }
2615
2616
        $showAttemptsInSessions = api_get_configuration_value('show_exercise_attempts_in_all_user_sessions');
2617
        if ($showAttemptsInSessions) {
2618
            $sessions = SessionManager::get_sessions_by_general_coach(api_get_user_id());
2619
            if (!empty($sessions)) {
2620
                $sessionIds = [];
2621
                foreach ($sessions as $session) {
2622
                    $sessionIds[] = $session['id'];
2623
                }
2624
                $session_id_and = " AND te.session_id IN(".implode(',', $sessionIds).")";
2625
                $sessionCondition = " AND ttte.session_id IN(".implode(',', $sessionIds).")";
2626
            } else {
2627
                return false;
2628
            }
2629
        }
2630
2631
        $exercise_where = '';
2632
        $exerciseFilter = '';
2633
        if (!empty($exercise_id)) {
2634
            $exercise_where .= ' AND te.exe_exo_id = '.$exercise_id.' ';
2635
            $exerciseFilter = " AND exe_exo_id = $exercise_id ";
2636
        }
2637
2638
        $hotpotatoe_where = '';
2639
        if (!empty($_GET['path'])) {
2640
            $hotpotatoe_path = Database::escape_string($_GET['path']);
2641
            $hotpotatoe_where .= ' AND exe_name = "'.$hotpotatoe_path.'"  ';
2642
        }
2643
2644
        // sql for chamilo-type tests for teacher / tutor view
2645
        $sql_inner_join_tbl_track_exercices = "
2646
        (
2647
            SELECT DISTINCT ttte.*, if(tr.exe_id,1, 0) as revised, tr.author as corrector, MAX(tr.insert_date) as correction_date
2648
            FROM $TBL_TRACK_EXERCICES ttte
2649
            LEFT JOIN $TBL_TRACK_ATTEMPT_RECORDING tr
2650
            ON (ttte.exe_id = tr.exe_id)
2651
            WHERE
2652
                $courseCondition
2653
                $exerciseFilter
2654
                $sessionCondition
2655
            GROUP BY ttte.exe_id
2656
        )";
2657
2658
        if ($is_allowedToEdit) {
2659
            //@todo fix to work with COURSE_RELATION_TYPE_RRHH in both queries
2660
            // Hack in order to filter groups
2661
            $sql_inner_join_tbl_user = '';
2662
            if (strpos($extra_where_conditions, 'group_id')) {
2663
                $sql_inner_join_tbl_user = "
2664
                (
2665
                    SELECT
2666
                        u.user_id,
2667
                        firstname,
2668
                        lastname,
2669
                        official_code,
2670
                        email,
2671
                        username,
2672
                        g.name as group_name,
2673
                        g.id as group_id
2674
                    FROM $TBL_USER u
2675
                    INNER JOIN $TBL_GROUP_REL_USER gru
2676
                    ON (gru.user_id = u.user_id AND gru.c_id= $courseId )
2677
                    INNER JOIN $TBL_GROUP g
2678
                    ON (gru.group_id = g.id AND g.c_id= $courseId )
2679
                )";
2680
            }
2681
2682
            if (strpos($extra_where_conditions, 'group_all')) {
2683
                $extra_where_conditions = str_replace(
2684
                    "AND (  group_id = 'group_all'  )",
2685
                    '',
2686
                    $extra_where_conditions
2687
                );
2688
                $extra_where_conditions = str_replace(
2689
                    "AND group_id = 'group_all'",
2690
                    '',
2691
                    $extra_where_conditions
2692
                );
2693
                $extra_where_conditions = str_replace(
2694
                    "group_id = 'group_all' AND",
2695
                    '',
2696
                    $extra_where_conditions
2697
                );
2698
2699
                $sql_inner_join_tbl_user = "
2700
                (
2701
                    SELECT
2702
                        u.user_id,
2703
                        firstname,
2704
                        lastname,
2705
                        official_code,
2706
                        email,
2707
                        username,
2708
                        '' as group_name,
2709
                        '' as group_id
2710
                    FROM $TBL_USER u
2711
                )";
2712
                $sql_inner_join_tbl_user = null;
2713
            }
2714
2715
            if (strpos($extra_where_conditions, 'group_none')) {
2716
                $extra_where_conditions = str_replace(
2717
                    "AND (  group_id = 'group_none'  )",
2718
                    "AND (  group_id is null  )",
2719
                    $extra_where_conditions
2720
                );
2721
                $extra_where_conditions = str_replace(
2722
                    "AND group_id = 'group_none'",
2723
                    "AND (  group_id is null  )",
2724
                    $extra_where_conditions
2725
                );
2726
                $sql_inner_join_tbl_user = "
2727
            (
2728
                SELECT
2729
                    u.user_id,
2730
                    firstname,
2731
                    lastname,
2732
                    official_code,
2733
                    email,
2734
                    username,
2735
                    g.name as group_name,
2736
                    g.id as group_id
2737
                FROM $TBL_USER u
2738
                LEFT OUTER JOIN $TBL_GROUP_REL_USER gru
2739
                ON ( gru.user_id = u.user_id AND gru.c_id = $courseId )
2740
                LEFT OUTER JOIN $TBL_GROUP g
2741
                ON (gru.group_id = g.id AND g.c_id = $courseId )
2742
            )";
2743
            }
2744
2745
            // All
2746
            $is_empty_sql_inner_join_tbl_user = false;
2747
            if (empty($sql_inner_join_tbl_user)) {
2748
                $is_empty_sql_inner_join_tbl_user = true;
2749
                $sql_inner_join_tbl_user = "
2750
            (
2751
                SELECT u.user_id, firstname, lastname, email, username, ' ' as group_name, '' as group_id, official_code
2752
                FROM $TBL_USER u
2753
                WHERE u.status NOT IN(".api_get_users_status_ignored_in_reports('string').")
2754
            )";
2755
            }
2756
2757
            $sqlFromOption = '';
2758
            $sqlWhereOption = '';
2759
            if (false === $searchAllTeacherCourses) {
2760
                $sqlFromOption = " , $TBL_GROUP_REL_USER AS gru ";
2761
                $sqlWhereOption = "  AND gru.c_id = $courseId AND gru.user_id = user.user_id ";
2762
            }
2763
2764
            $first_and_last_name = api_is_western_name_order() ? "firstname, lastname" : "lastname, firstname";
2765
2766
            if ($get_count) {
2767
                $sql_select = 'SELECT count(te.exe_id) ';
2768
            } else {
2769
                $sql_select = "SELECT DISTINCT
2770
                    user_id,
2771
                    $first_and_last_name,
2772
                    official_code,
2773
                    ce.title,
2774
                    username,
2775
                    te.exe_result,
2776
                    te.exe_weighting,
2777
                    te.exe_date,
2778
                    te.exe_id,
2779
                    te.c_id,
2780
                    te.session_id,
2781
                    email as exemail,
2782
                    te.start_date,
2783
                    ce.expired_time,
2784
                    steps_counter,
2785
                    exe_user_id,
2786
                    te.exe_duration,
2787
                    te.status as completion_status,
2788
                    propagate_neg,
2789
                    revised,
2790
                    group_name,
2791
                    group_id,
2792
                    orig_lp_id,
2793
                    te.user_ip,
2794
                    corrector,
2795
                    correction_date";
2796
            }
2797
2798
            $sql = " $sql_select
2799
                FROM $TBL_EXERCICES AS ce
2800
                INNER JOIN $sql_inner_join_tbl_track_exercices AS te
2801
                ON (te.exe_exo_id = ce.iid)
2802
                INNER JOIN $sql_inner_join_tbl_user AS user
2803
                ON (user.user_id = exe_user_id)
2804
                WHERE
2805
                    te.$courseCondition
2806
                    $session_id_and AND
2807
                    ce.active <> -1 AND
2808
                    ce.$courseCondition
2809
                    $exercise_where
2810
                    $extra_where_conditions
2811
                    $statusCondition
2812
                ";
2813
2814
            // sql for hotpotatoes tests for teacher / tutor view
2815
            if ($get_count) {
2816
                $hpsql_select = ' SELECT count(username) ';
2817
            } else {
2818
                $hpsql_select = " SELECT
2819
                    $first_and_last_name ,
2820
                    username,
2821
                    official_code,
2822
                    tth.exe_name,
2823
                    tth.exe_result ,
2824
                    tth.exe_weighting,
2825
                    tth.exe_date";
2826
            }
2827
2828
            $hpsql = " $hpsql_select
2829
                FROM
2830
                    $TBL_TRACK_HOTPOTATOES tth,
2831
                    $TBL_USER user
2832
                    $sqlFromOption
2833
                WHERE
2834
                    user.user_id=tth.exe_user_id AND
2835
                    tth.$courseCondition
2836
                    $hotpotatoe_where
2837
                    $sqlWhereOption AND
2838
                     user.status NOT IN (".api_get_users_status_ignored_in_reports('string').")
2839
                ORDER BY tth.c_id ASC, tth.exe_date DESC ";
2840
        }
2841
2842
        if (empty($sql)) {
2843
            return false;
2844
        }
2845
2846
        if ($get_count) {
2847
            $resx = Database::query($sql);
2848
            $rowx = Database::fetch_row($resx, 'ASSOC');
2849
2850
            return $rowx[0];
2851
        }
2852
2853
        $teacher_id_list = [];
2854
        if (!empty($courseCode)) {
2855
            $teacher_list = CourseManager::get_teacher_list_from_course_code($courseCode);
2856
            if (!empty($teacher_list)) {
2857
                foreach ($teacher_list as $teacher) {
2858
                    $teacher_id_list[] = $teacher['user_id'];
2859
                }
2860
            }
2861
        }
2862
2863
        $scoreDisplay = new ScoreDisplay();
2864
        $decimalSeparator = '.';
2865
        $thousandSeparator = ',';
2866
2867
        if ($useCommaAsDecimalPoint) {
2868
            $decimalSeparator = ',';
2869
            $thousandSeparator = '';
2870
        }
2871
2872
        $hideIp = api_get_configuration_value('exercise_hide_ip');
2873
        $listInfo = [];
2874
        // Simple exercises
2875
        if (empty($hotpotatoe_where)) {
2876
            $column = !empty($column) ? Database::escape_string($column) : null;
2877
            $from = (int) $from;
2878
            $number_of_items = (int) $number_of_items;
2879
            $direction = !in_array(strtolower(trim($direction)), ['asc', 'desc']) ? 'asc' : $direction;
2880
2881
            if (!empty($column)) {
2882
                $sql .= " ORDER BY `$column` $direction ";
2883
            }
2884
2885
            if (!$getOnlyIds) {
2886
                $sql .= " LIMIT $from, $number_of_items";
2887
            }
2888
2889
            $results = [];
2890
            $resx = Database::query($sql);
2891
            while ($rowx = Database::fetch_array($resx, 'ASSOC')) {
2892
                $results[] = $rowx;
2893
            }
2894
2895
            $clean_group_list = [];
2896
            $lp_list = [];
2897
2898
            if (!empty($courseInfo)) {
2899
                $group_list = GroupManager::get_group_list(null, $courseInfo);
2900
                if (!empty($group_list)) {
2901
                    foreach ($group_list as $group) {
2902
                        $clean_group_list[$group['id']] = $group['name'];
2903
                    }
2904
                }
2905
2906
                $lp_list_obj = new LearnpathList(api_get_user_id());
2907
                $lp_list = $lp_list_obj->get_flat_list();
2908
                $oldIds = array_column($lp_list, 'lp_old_id', 'iid');
2909
            }
2910
2911
            if (is_array($results)) {
2912
                $users_array_id = [];
2913
                $from_gradebook = false;
2914
                if (isset($_GET['gradebook']) && $_GET['gradebook'] === 'view') {
2915
                    $from_gradebook = true;
2916
                }
2917
                $sizeof = count($results);
2918
                $locked = api_resource_is_locked_by_gradebook($exercise_id, LINK_EXERCISE);
2919
                $timeNow = strtotime(api_get_utc_datetime());
2920
                $courseItemList = [];
2921
                // Looping results
2922
                for ($i = 0; $i < $sizeof; $i++) {
2923
                    $attempt = $results[$i];
2924
                    $revised = $attempt['revised'];
2925
                    $attemptSessionId = (int) $attempt['session_id'];
2926
                    if (false === $searchAllTeacherCourses) {
2927
                        $courseItemInfo = api_get_course_info();
2928
                        $cidReq = api_get_cidreq(false).'&id_session='.$attemptSessionId;
2929
                    } else {
2930
                        if (isset($courseItemList[$attempt['c_id']])) {
2931
                            $courseItemInfo = $courseItemList[$attempt['c_id']];
2932
                        } else {
2933
                            $courseItemInfo = api_get_course_info_by_id($attempt['c_id']);
2934
                            $courseItemList[$attempt['c_id']] = $courseItemInfo;
2935
                        }
2936
                        $cidReq = 'cidReq='.$courseItemInfo['code'].'&id_session='.$attemptSessionId;
2937
                    }
2938
2939
                    if ('incomplete' === $attempt['completion_status']) {
2940
                        // If the exercise was incomplete, we need to determine
2941
                        // if it is still into the time allowed, or if its
2942
                        // allowed time has expired and it can be closed
2943
                        // (it's "unclosed")
2944
                        $minutes = $attempt['expired_time'];
2945
                        if ($minutes == 0) {
2946
                            // There's no time limit, so obviously the attempt
2947
                            // can still be "ongoing", but the teacher should
2948
                            // be able to choose to close it, so mark it as
2949
                            // "unclosed" instead of "ongoing"
2950
                            $revised = 2;
2951
                        } else {
2952
                            $allowedSeconds = $minutes * 60;
2953
                            $timeAttemptStarted = strtotime($attempt['start_date']);
2954
                            $secondsSinceStart = $timeNow - $timeAttemptStarted;
2955
                            $revised = 3; // mark as "ongoing"
2956
                            if ($secondsSinceStart > $allowedSeconds) {
2957
                                $revised = 2; // mark as "unclosed"
2958
                            }
2959
                        }
2960
                    }
2961
2962
                    if (4 == $status && 2 != $revised) {
2963
                        // Filter by status "unclosed"
2964
                        continue;
2965
                    }
2966
2967
                    if (5 == $status && 3 != $revised) {
2968
                        // Filter by status "ongoing"
2969
                        continue;
2970
                    }
2971
2972
                    if (3 == $status && in_array($revised, [1, 2, 3])) {
2973
                        // Filter by status "not validated"
2974
                        continue;
2975
                    }
2976
2977
                    if ($from_gradebook && ($is_allowedToEdit)) {
2978
                        if (in_array(
2979
                            $attempt['username'].$attempt['firstname'].$attempt['lastname'],
2980
                            $users_array_id
2981
                        )) {
2982
                            continue;
2983
                        }
2984
                        $users_array_id[] = $attempt['username'].$attempt['firstname'].$attempt['lastname'];
2985
                    }
2986
2987
                    $lp_obj = isset($attempt['orig_lp_id']) &&
2988
                        isset($lp_list[$attempt['orig_lp_id']]) ? $lp_list[$attempt['orig_lp_id']] : null;
2989
                    if (empty($lp_obj)) {
2990
                        // Try to get the old id (id instead of iid)
2991
                        $lpNewId = isset($attempt['orig_lp_id']) &&
2992
                        isset($oldIds[$attempt['orig_lp_id']]) ? $oldIds[$attempt['orig_lp_id']] : null;
2993
                        if ($lpNewId) {
2994
                            $lp_obj = isset($lp_list[$lpNewId]) ? $lp_list[$lpNewId] : null;
2995
                        }
2996
                    }
2997
                    $lp_name = null;
2998
                    if ($lp_obj) {
2999
                        $url = api_get_path(WEB_CODE_PATH).
3000
                            'lp/lp_controller.php?'.$cidReq.'&action=view&lp_id='.$attempt['orig_lp_id'];
3001
                        $lp_name = Display::url(
3002
                            $lp_obj['lp_name'],
3003
                            $url,
3004
                            ['target' => '_blank']
3005
                        );
3006
                    }
3007
3008
                    // Add all groups by user
3009
                    $group_name_list = '';
3010
                    if ($is_empty_sql_inner_join_tbl_user) {
3011
                        $group_list = GroupManager::get_group_ids(
3012
                            api_get_course_int_id(),
3013
                            $attempt['user_id']
3014
                        );
3015
3016
                        foreach ($group_list as $id) {
3017
                            if (isset($clean_group_list[$id])) {
3018
                                $group_name_list .= $clean_group_list[$id].'<br/>';
3019
                            }
3020
                        }
3021
                        $attempt['group_name'] = $group_name_list;
3022
                    }
3023
3024
                    $attempt['exe_duration'] = !empty($attempt['exe_duration']) ? round($attempt['exe_duration'] / 60) : 0;
3025
                    $id = $attempt['exe_id'];
3026
                    $dt = api_convert_and_format_date($attempt['exe_weighting']);
3027
3028
                    // we filter the results if we have the permission to
3029
                    $result_disabled = 0;
3030
                    if (isset($attempt['results_disabled'])) {
3031
                        $result_disabled = (int) $attempt['results_disabled'];
3032
                    }
3033
                    if ($result_disabled == 0) {
3034
                        $my_res = $attempt['exe_result'];
3035
                        $my_total = $attempt['exe_weighting'];
3036
                        $attempt['start_date'] = api_get_local_time($attempt['start_date']);
3037
                        $attempt['exe_date'] = api_get_local_time($attempt['exe_date']);
3038
3039
                        if (!$attempt['propagate_neg'] && $my_res < 0) {
3040
                            $my_res = 0;
3041
                        }
3042
3043
                        $score = self::show_score(
3044
                            $my_res,
3045
                            $my_total,
3046
                            true,
3047
                            true,
3048
                            false,
3049
                            false,
3050
                            $decimalSeparator,
3051
                            $thousandSeparator,
3052
                            $roundValues
3053
                        );
3054
3055
                        $actions = '<div class="pull-right">';
3056
                        if ($is_allowedToEdit) {
3057
                            if (isset($teacher_id_list)) {
3058
                                if (in_array(
3059
                                    $attempt['exe_user_id'],
3060
                                    $teacher_id_list
3061
                                )) {
3062
                                    $actions .= Display::return_icon('teacher.png', get_lang('Teacher'));
3063
                                }
3064
                            }
3065
                            $revisedLabel = '';
3066
                            switch ($revised) {
3067
                                case 0:
3068
                                    $actions .= "<a href='exercise_show.php?".$cidReq."&action=qualify&id=$id'>".
3069
                                        Display::return_icon(
3070
                                            'quiz.png',
3071
                                            get_lang('Qualify')
3072
                                        );
3073
                                    $actions .= '</a>';
3074
                                    $revisedLabel = Display::label(
3075
                                        get_lang('NotValidated'),
3076
                                        'info'
3077
                                    );
3078
                                    break;
3079
                                case 1:
3080
                                    $actions .= "<a href='exercise_show.php?".$cidReq."&action=edit&id=$id'>".
3081
                                        Display::return_icon(
3082
                                            'edit.png',
3083
                                            get_lang('Edit'),
3084
                                            [],
3085
                                            ICON_SIZE_SMALL
3086
                                        );
3087
                                    $actions .= '</a>';
3088
                                    $revisedLabel = Display::label(
3089
                                        get_lang('Validated'),
3090
                                        'success'
3091
                                    );
3092
                                    break;
3093
                                case 2: //finished but not marked as such
3094
                                    $actions .= '<a href="exercise_report.php?'
3095
                                        .$cidReq
3096
                                        .'&exerciseId='.$exercise_id
3097
                                        .'&a=close&id='.$id
3098
                                        .'">'.
3099
                                        Display::return_icon(
3100
                                            'lock.png',
3101
                                            get_lang('MarkAttemptAsClosed'),
3102
                                            [],
3103
                                            ICON_SIZE_SMALL
3104
                                        );
3105
                                    $actions .= '</a>';
3106
                                    $revisedLabel = Display::label(
3107
                                        get_lang('Unclosed'),
3108
                                        'warning'
3109
                                    );
3110
                                    break;
3111
                                case 3: //still ongoing
3112
                                    $actions .= Display::return_icon(
3113
                                        'clock.png',
3114
                                        get_lang('AttemptStillOngoingPleaseWait'),
3115
                                        [],
3116
                                        ICON_SIZE_SMALL
3117
                                    );
3118
                                    $actions .= '';
3119
                                    $revisedLabel = Display::label(
3120
                                        get_lang('Ongoing'),
3121
                                        'danger'
3122
                                    );
3123
                                    break;
3124
                            }
3125
3126
                            if ($filter == 2) {
3127
                                $actions .= ' <a href="exercise_history.php?'.$cidReq.'&exe_id='.$id.'">'.
3128
                                    Display::return_icon(
3129
                                        'history.png',
3130
                                        get_lang('ViewHistoryChange')
3131
                                    ).'</a>';
3132
                            }
3133
3134
                            // Admin can always delete the attempt
3135
                            if (($locked == false || api_is_platform_admin()) && !api_is_student_boss()) {
3136
                                $ip = Tracking::get_ip_from_user_event(
3137
                                    $attempt['exe_user_id'],
3138
                                    api_get_utc_datetime(),
3139
                                    false
3140
                                );
3141
                                $actions .= '<a href="http://www.whatsmyip.org/ip-geo-location/?ip='.$ip.'" target="_blank">'
3142
                                    .Display::return_icon('info.png', $ip)
3143
                                    .'</a>';
3144
3145
                                $recalculateUrl = api_get_path(WEB_CODE_PATH).'exercise/recalculate.php?'.
3146
                                    $cidReq.'&'.
3147
                                    http_build_query([
3148
                                        'id' => $id,
3149
                                        'exercise' => $exercise_id,
3150
                                        'user' => $attempt['exe_user_id'],
3151
                                    ]);
3152
                                $actions .= Display::url(
3153
                                    Display::return_icon('reload.png', get_lang('RecalculateResults')),
3154
                                    $recalculateUrl,
3155
                                    [
3156
                                        'data-exercise' => $exercise_id,
3157
                                        'data-user' => $attempt['exe_user_id'],
3158
                                        'data-id' => $id,
3159
                                        'class' => 'exercise-recalculate',
3160
                                    ]
3161
                                );
3162
3163
                                $filterByUser = isset($_GET['filter_by_user']) ? (int) $_GET['filter_by_user'] : 0;
3164
                                $delete_link = '<a
3165
                                    href="exercise_report.php?'.$cidReq.'&filter_by_user='.$filterByUser.'&filter='.$filter.'&exerciseId='.$exercise_id.'&delete=delete&did='.$id.'"
3166
                                    onclick="javascript:if(!confirm(\''.sprintf(
3167
                                        addslashes(get_lang('DeleteAttempt')),
3168
                                        $attempt['username'],
3169
                                        $dt
3170
                                    ).'\')) return false;">';
3171
                                $delete_link .= Display::return_icon(
3172
                                        'delete.png',
3173
                                        addslashes(get_lang('Delete'))
3174
                                    ).'</a>';
3175
3176
                                if (api_is_drh() && !api_is_platform_admin()) {
3177
                                    $delete_link = null;
3178
                                }
3179
                                if (api_is_session_admin()) {
3180
                                    $delete_link = '';
3181
                                }
3182
                                if ($revised == 3) {
3183
                                    $delete_link = null;
3184
                                }
3185
                                $actions .= $delete_link;
3186
                            }
3187
                        } else {
3188
                            $attempt_url = api_get_path(WEB_CODE_PATH).'exercise/result.php?'.$cidReq.'&id='.$attempt['exe_id'];
3189
                            $attempt_link = Display::url(
3190
                                get_lang('Show'),
3191
                                $attempt_url,
3192
                                [
3193
                                    'class' => 'ajax btn btn-default',
3194
                                    'data-title' => get_lang('Show'),
3195
                                ]
3196
                            );
3197
                            $actions .= $attempt_link;
3198
                        }
3199
                        $actions .= '</div>';
3200
3201
                        if (!empty($userExtraFieldsToAdd)) {
3202
                            foreach ($userExtraFieldsToAdd as $variable) {
3203
                                $extraFieldValue = new ExtraFieldValue('user');
3204
                                $values = $extraFieldValue->get_values_by_handler_and_field_variable(
3205
                                    $attempt['user_id'],
3206
                                    $variable
3207
                                );
3208
                                if (isset($values['value'])) {
3209
                                    $attempt[$variable] = $values['value'];
3210
                                }
3211
                            }
3212
                        }
3213
3214
                        $exeId = $attempt['exe_id'];
3215
                        $attempt['id'] = $exeId;
3216
                        $category_list = [];
3217
                        if ($is_allowedToEdit) {
3218
                            $sessionName = '';
3219
                            $sessionStartAccessDate = '';
3220
                            if (!empty($attemptSessionId)) {
3221
                                $sessionInfo = api_get_session_info($attemptSessionId);
3222
                                if (!empty($sessionInfo)) {
3223
                                    $sessionName = $sessionInfo['name'];
3224
                                    $sessionStartAccessDate = api_get_local_time($sessionInfo['access_start_date']);
3225
                                }
3226
                            }
3227
3228
                            $courseId = $courseItemInfo['real_id'];
3229
3230
                            if ($searchAllTeacherCourses) {
3231
                                $attempt['course'] = $courseItemInfo['title'];
3232
                                $attempt['exercise'] = $attempt['title'];
3233
                            }
3234
3235
                            $objExercise = new Exercise($courseId);
3236
                            if ($showExerciseCategories) {
3237
                                // Getting attempt info
3238
                                $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
3239
                                if (!empty($exercise_stat_info['data_tracking'])) {
3240
                                    $question_list = explode(',', $exercise_stat_info['data_tracking']);
3241
                                    if (!empty($question_list)) {
3242
                                        foreach ($question_list as $questionId) {
3243
                                            $objQuestionTmp = Question::read($questionId, $objExercise->course);
3244
                                            // We're inside *one* question.
3245
                                            // Go through each possible answer for this question.
3246
                                            $result = $objExercise->manage_answer(
3247
                                                $exeId,
3248
                                                $questionId,
3249
                                                null,
3250
                                                'exercise_result',
3251
                                                false,
3252
                                                false,
3253
                                                true,
3254
                                                false,
3255
                                                $objExercise->selectPropagateNeg(),
3256
                                                null,
3257
                                                true
3258
                                            );
3259
3260
                                            $my_total_score = $result['score'];
3261
                                            $my_total_weight = $result['weight'];
3262
3263
                                            // Category report
3264
                                            $category_was_added_for_this_test = false;
3265
                                            if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
3266
                                                if (!isset($category_list[$objQuestionTmp->category]['score'])) {
3267
                                                    $category_list[$objQuestionTmp->category]['score'] = 0;
3268
                                                }
3269
                                                if (!isset($category_list[$objQuestionTmp->category]['total'])) {
3270
                                                    $category_list[$objQuestionTmp->category]['total'] = 0;
3271
                                                }
3272
                                                $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
3273
                                                $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
3274
                                                $category_was_added_for_this_test = true;
3275
                                            }
3276
3277
                                            if (isset($objQuestionTmp->category_list) &&
3278
                                                !empty($objQuestionTmp->category_list)
3279
                                            ) {
3280
                                                foreach ($objQuestionTmp->category_list as $category_id) {
3281
                                                    $category_list[$category_id]['score'] += $my_total_score;
3282
                                                    $category_list[$category_id]['total'] += $my_total_weight;
3283
                                                    $category_was_added_for_this_test = true;
3284
                                                }
3285
                                            }
3286
3287
                                            // No category for this question!
3288
                                            if ($category_was_added_for_this_test == false) {
3289
                                                if (!isset($category_list['none']['score'])) {
3290
                                                    $category_list['none']['score'] = 0;
3291
                                                }
3292
                                                if (!isset($category_list['none']['total'])) {
3293
                                                    $category_list['none']['total'] = 0;
3294
                                                }
3295
3296
                                                $category_list['none']['score'] += $my_total_score;
3297
                                                $category_list['none']['total'] += $my_total_weight;
3298
                                            }
3299
                                        }
3300
                                    }
3301
                                }
3302
                            }
3303
3304
                            foreach ($category_list as $categoryId => $result) {
3305
                                $scoreToDisplay = self::show_score(
3306
                                    $result['score'],
3307
                                    $result['total'],
3308
                                    true,
3309
                                    true,
3310
                                    false,
3311
                                    false,
3312
                                    $decimalSeparator,
3313
                                    $thousandSeparator,
3314
                                    $roundValues
3315
                                );
3316
                                $attempt['category_'.$categoryId] = $scoreToDisplay;
3317
                                $attempt['category_'.$categoryId.'_score_percentage'] = self::show_score(
3318
                                    $result['score'],
3319
                                    $result['total'],
3320
                                    true,
3321
                                    true,
3322
                                    true,
3323
                                    true,
3324
                                    $decimalSeparator,
3325
                                    $thousandSeparator,
3326
                                    $roundValues
3327
                                );
3328
                                $attempt['category_'.$categoryId.'_only_score'] = $result['score'];
3329
                                $attempt['category_'.$categoryId.'_total'] = $result['total'];
3330
                            }
3331
                            $attempt['session'] = $sessionName;
3332
                            $attempt['session_access_start_date'] = $sessionStartAccessDate;
3333
                            $attempt['status'] = $revisedLabel;
3334
                            $attempt['score'] = $score;
3335
                            $attempt['qualificator_fullname'] = '';
3336
                            $attempt['date_of_qualification'] = '';
3337
                            if (!empty($attempt['corrector'])) {
3338
                                $qualificatorAuthor = api_get_user_info($attempt['corrector']);
3339
                                $attempt['qualificator_fullname'] = api_get_person_name($qualificatorAuthor['firstname'], $qualificatorAuthor['lastname']);
3340
                            }
3341
                            if (!empty($attempt['correction_date'])) {
3342
                                $attempt['date_of_qualification'] = api_convert_and_format_date($attempt['correction_date'], DATE_TIME_FORMAT_SHORT);
3343
                            }
3344
                            $attempt['score_percentage'] = self::show_score(
3345
                                $my_res,
3346
                                $my_total,
3347
                                true,
3348
                                true,
3349
                                true,
3350
                                true,
3351
                                $decimalSeparator,
3352
                                $thousandSeparator,
3353
                                $roundValues
3354
                            );
3355
3356
                            if ($roundValues) {
3357
                                $whole = floor($my_res); // 1
3358
                                $fraction = $my_res - $whole; // .25
3359
                                if ($fraction >= 0.5) {
3360
                                    $onlyScore = ceil($my_res);
3361
                                } else {
3362
                                    $onlyScore = round($my_res);
3363
                                }
3364
                            } else {
3365
                                $onlyScore = $scoreDisplay->format_score(
3366
                                    $my_res,
3367
                                    false,
3368
                                    $decimalSeparator,
3369
                                    $thousandSeparator
3370
                                );
3371
                            }
3372
3373
                            $attempt['only_score'] = $onlyScore;
3374
3375
                            if ($roundValues) {
3376
                                $whole = floor($my_total); // 1
3377
                                $fraction = $my_total - $whole; // .25
3378
                                if ($fraction >= 0.5) {
3379
                                    $onlyTotal = ceil($my_total);
3380
                                } else {
3381
                                    $onlyTotal = round($my_total);
3382
                                }
3383
                            } else {
3384
                                $onlyTotal = $scoreDisplay->format_score(
3385
                                    $my_total,
3386
                                    false,
3387
                                    $decimalSeparator,
3388
                                    $thousandSeparator
3389
                                );
3390
                            }
3391
                            $attempt['total'] = $onlyTotal;
3392
                            $attempt['lp'] = $lp_name;
3393
                            $attempt['actions'] = $actions;
3394
                            if ($hideIp && isset($attempt['user_ip'])) {
3395
                                unset($attempt['user_ip']);
3396
                            }
3397
                            $listInfo[] = $attempt;
3398
                        } else {
3399
                            $attempt['status'] = $revisedLabel;
3400
                            $attempt['score'] = $score;
3401
                            $attempt['actions'] = $actions;
3402
                            if ($hideIp && isset($attempt['user_ip'])) {
3403
                                unset($attempt['user_ip']);
3404
                            }
3405
                            $listInfo[] = $attempt;
3406
                        }
3407
                    }
3408
                }
3409
            }
3410
        } else {
3411
            $hpresults = [];
3412
            $res = Database::query($hpsql);
3413
            if ($res !== false) {
3414
                $i = 0;
3415
                while ($resA = Database::fetch_array($res, 'NUM')) {
3416
                    for ($j = 0; $j < 6; $j++) {
3417
                        $hpresults[$i][$j] = $resA[$j];
3418
                    }
3419
                    $i++;
3420
                }
3421
            }
3422
3423
            // Print HotPotatoes test results.
3424
            if (is_array($hpresults)) {
3425
                for ($i = 0; $i < count($hpresults); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

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

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

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
3426
                    $hp_title = GetQuizName($hpresults[$i][3], $documentPath);
3427
                    if ($hp_title == '') {
3428
                        $hp_title = basename($hpresults[$i][3]);
3429
                    }
3430
3431
                    $hp_date = api_get_local_time(
3432
                        $hpresults[$i][6],
3433
                        null,
3434
                        date_default_timezone_get()
3435
                    );
3436
                    $hp_result = round(($hpresults[$i][4] / ($hpresults[$i][5] != 0 ? $hpresults[$i][5] : 1)) * 100, 2);
3437
                    $hp_result .= '% ('.$hpresults[$i][4].' / '.$hpresults[$i][5].')';
3438
3439
                    if ($is_allowedToEdit) {
3440
                        $listInfo[] = [
3441
                            $hpresults[$i][0],
3442
                            $hpresults[$i][1],
3443
                            $hpresults[$i][2],
3444
                            '',
3445
                            $hp_title,
3446
                            '-',
3447
                            $hp_date,
3448
                            $hp_result,
3449
                            '-',
3450
                        ];
3451
                    } else {
3452
                        $listInfo[] = [
3453
                            $hp_title,
3454
                            '-',
3455
                            $hp_date,
3456
                            $hp_result,
3457
                            '-',
3458
                        ];
3459
                    }
3460
                }
3461
            }
3462
        }
3463
3464
        return $listInfo;
3465
    }
3466
3467
    /**
3468
     * @param $score
3469
     * @param $weight
3470
     *
3471
     * @return array
3472
     */
3473
    public static function convertScoreToPlatformSetting($score, $weight)
3474
    {
3475
        $maxNote = api_get_setting('exercise_max_score');
3476
        $minNote = api_get_setting('exercise_min_score');
3477
3478
        if ($maxNote != '' && $minNote != '') {
3479
            if (!empty($weight) && (float) $weight !== (float) 0) {
3480
                $score = $minNote + ($maxNote - $minNote) * $score / $weight;
3481
            } else {
3482
                $score = $minNote;
3483
            }
3484
            $weight = $maxNote;
3485
        }
3486
3487
        return ['score' => $score, 'weight' => $weight];
3488
    }
3489
3490
    /**
3491
     * Converts the score with the exercise_max_note and exercise_min_score
3492
     * the platform settings + formats the results using the float_format function.
3493
     *
3494
     * @param float  $score
3495
     * @param float  $weight
3496
     * @param bool   $show_percentage       show percentage or not
3497
     * @param bool   $use_platform_settings use or not the platform settings
3498
     * @param bool   $show_only_percentage
3499
     * @param bool   $hidePercentageSign    hide "%" sign
3500
     * @param string $decimalSeparator
3501
     * @param string $thousandSeparator
3502
     * @param bool   $roundValues           This option rounds the float values into a int using ceil()
3503
     * @param bool   $removeEmptyDecimals
3504
     *
3505
     * @return string an html with the score modified
3506
     */
3507
    public static function show_score(
3508
        $score,
3509
        $weight,
3510
        $show_percentage = true,
3511
        $use_platform_settings = true,
3512
        $show_only_percentage = false,
3513
        $hidePercentageSign = false,
3514
        $decimalSeparator = '.',
3515
        $thousandSeparator = ',',
3516
        $roundValues = false,
3517
        $removeEmptyDecimals = false
3518
    ) {
3519
        if (is_null($score) && is_null($weight)) {
3520
            return '-';
3521
        }
3522
3523
        $decimalSeparator = empty($decimalSeparator) ? '.' : $decimalSeparator;
3524
        $thousandSeparator = empty($thousandSeparator) ? ',' : $thousandSeparator;
3525
3526
        if ($use_platform_settings) {
3527
            $result = self::convertScoreToPlatformSetting($score, $weight);
3528
            $score = $result['score'];
3529
            $weight = $result['weight'];
3530
        }
3531
3532
        $percentage = (100 * $score) / ($weight != 0 ? $weight : 1);
3533
3534
        // Formats values
3535
        $percentage = float_format($percentage, 1);
3536
        $score = float_format($score, 1);
3537
        $weight = float_format($weight, 1);
3538
3539
        if ($roundValues) {
3540
            $whole = floor($percentage); // 1
3541
            $fraction = $percentage - $whole; // .25
3542
3543
            // Formats values
3544
            if ($fraction >= 0.5) {
3545
                $percentage = ceil($percentage);
3546
            } else {
3547
                $percentage = round($percentage);
3548
            }
3549
3550
            $whole = floor($score); // 1
3551
            $fraction = $score - $whole; // .25
3552
            if ($fraction >= 0.5) {
3553
                $score = ceil($score);
3554
            } else {
3555
                $score = round($score);
3556
            }
3557
3558
            $whole = floor($weight); // 1
3559
            $fraction = $weight - $whole; // .25
3560
            if ($fraction >= 0.5) {
3561
                $weight = ceil($weight);
3562
            } else {
3563
                $weight = round($weight);
3564
            }
3565
        } else {
3566
            // Formats values
3567
            $percentage = float_format($percentage, 1, $decimalSeparator, $thousandSeparator);
3568
            $score = float_format($score, 1, $decimalSeparator, $thousandSeparator);
3569
            $weight = float_format($weight, 1, $decimalSeparator, $thousandSeparator);
3570
        }
3571
3572
        if ($show_percentage) {
3573
            $percentageSign = ' %';
3574
            if ($hidePercentageSign) {
3575
                $percentageSign = '';
3576
            }
3577
            $html = $percentage."$percentageSign ($score / $weight)";
3578
            if ($show_only_percentage) {
3579
                $html = $percentage.$percentageSign;
3580
            }
3581
        } else {
3582
            if ($removeEmptyDecimals) {
3583
                if (ScoreDisplay::hasEmptyDecimals($weight)) {
3584
                    $weight = round($weight);
3585
                }
3586
            }
3587
            $html = $score.' / '.$weight;
3588
        }
3589
3590
        // Over write score
3591
        $scoreBasedInModel = self::convertScoreToModel($percentage);
3592
        if (!empty($scoreBasedInModel)) {
3593
            $html = $scoreBasedInModel;
3594
        }
3595
3596
        // Ignore other formats and use the configuration['exercise_score_format'] value
3597
        // But also keep the round values settings.
3598
        $format = api_get_configuration_value('exercise_score_format');
3599
        if (!empty($format)) {
3600
            $html = ScoreDisplay::instance()->display_score([$score, $weight], $format);
3601
        }
3602
3603
        return Display::span($html, ['class' => 'score_exercise']);
3604
    }
3605
3606
    /**
3607
     * @param array $model
3608
     * @param float $percentage
3609
     *
3610
     * @return string
3611
     */
3612
    public static function getModelStyle($model, $percentage)
3613
    {
3614
        return '<span class="'.$model['css_class'].'">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>';
3615
    }
3616
3617
    /**
3618
     * @param float $percentage value between 0 and 100
3619
     *
3620
     * @return string
3621
     */
3622
    public static function convertScoreToModel($percentage)
3623
    {
3624
        $model = self::getCourseScoreModel();
3625
        if (!empty($model)) {
3626
            $scoreWithGrade = [];
3627
            foreach ($model['score_list'] as $item) {
3628
                if ($percentage >= $item['min'] && $percentage <= $item['max']) {
3629
                    $scoreWithGrade = $item;
3630
                    break;
3631
                }
3632
            }
3633
3634
            if (!empty($scoreWithGrade)) {
3635
                return self::getModelStyle($scoreWithGrade, $percentage);
3636
            }
3637
        }
3638
3639
        return '';
3640
    }
3641
3642
    /**
3643
     * @return array
3644
     */
3645
    public static function getCourseScoreModel()
3646
    {
3647
        $modelList = self::getScoreModels();
3648
        if (empty($modelList)) {
3649
            return [];
3650
        }
3651
3652
        $courseInfo = api_get_course_info();
3653
        if (!empty($courseInfo)) {
3654
            $scoreModelId = api_get_course_setting('score_model_id');
3655
            if (-1 != $scoreModelId) {
3656
                $modelIdList = array_column($modelList['models'], 'id');
3657
                if (in_array($scoreModelId, $modelIdList)) {
3658
                    foreach ($modelList['models'] as $item) {
3659
                        if ($item['id'] == $scoreModelId) {
3660
                            return $item;
3661
                        }
3662
                    }
3663
                }
3664
            }
3665
        }
3666
3667
        return [];
3668
    }
3669
3670
    /**
3671
     * @return array
3672
     */
3673
    public static function getScoreModels()
3674
    {
3675
        return api_get_configuration_value('score_grade_model');
3676
    }
3677
3678
    /**
3679
     * @param float  $score
3680
     * @param float  $weight
3681
     * @param string $passPercentage
3682
     *
3683
     * @return bool
3684
     */
3685
    public static function isSuccessExerciseResult($score, $weight, $passPercentage)
3686
    {
3687
        $percentage = float_format(
3688
            ($score / (0 != $weight ? $weight : 1)) * 100,
3689
            1
3690
        );
3691
        if (isset($passPercentage) && !empty($passPercentage)) {
3692
            if ($percentage >= $passPercentage) {
3693
                return true;
3694
            }
3695
        }
3696
3697
        return false;
3698
    }
3699
3700
    /**
3701
     * @param string $name
3702
     * @param $weight
3703
     * @param $selected
3704
     *
3705
     * @return bool
3706
     */
3707
    public static function addScoreModelInput(
3708
        FormValidator $form,
3709
        $name,
3710
        $weight,
3711
        $selected
3712
    ) {
3713
        $model = self::getCourseScoreModel();
3714
        if (empty($model)) {
3715
            return false;
3716
        }
3717
3718
        /** @var HTML_QuickForm_select $element */
3719
        $element = $form->createElement(
3720
            'select',
3721
            $name,
3722
            get_lang('Qualification'),
3723
            [],
3724
            ['class' => 'exercise_mark_select']
3725
        );
3726
3727
        foreach ($model['score_list'] as $item) {
3728
            $i = api_number_format($item['score_to_qualify'] / 100 * $weight, 2);
3729
            $label = self::getModelStyle($item, $i);
3730
            $attributes = [
3731
                'class' => $item['css_class'],
3732
            ];
3733
            if ($selected == $i) {
3734
                $attributes['selected'] = 'selected';
3735
            }
3736
            $element->addOption($label, $i, $attributes);
3737
        }
3738
        $form->addElement($element);
3739
    }
3740
3741
    /**
3742
     * @return string
3743
     */
3744
    public static function getJsCode()
3745
    {
3746
        // Filling the scores with the right colors.
3747
        $models = self::getCourseScoreModel();
3748
        $cssListToString = '';
3749
        if (!empty($models)) {
3750
            $cssList = array_column($models['score_list'], 'css_class');
3751
            $cssListToString = implode(' ', $cssList);
3752
        }
3753
3754
        if (empty($cssListToString)) {
3755
            return '';
3756
        }
3757
        $js = <<<EOT
3758
3759
        function updateSelect(element) {
3760
            var spanTag = element.parent().find('span.filter-option');
3761
            var value = element.val();
3762
            var selectId = element.attr('id');
3763
            var optionClass = $('#' + selectId + ' option[value="'+value+'"]').attr('class');
3764
            spanTag.removeClass('$cssListToString');
3765
            spanTag.addClass(optionClass);
3766
        }
3767
3768
        $(function() {
3769
            // Loading values
3770
            $('.exercise_mark_select').on('loaded.bs.select', function() {
3771
                updateSelect($(this));
3772
            });
3773
            // On change
3774
            $('.exercise_mark_select').on('changed.bs.select', function() {
3775
                updateSelect($(this));
3776
            });
3777
        });
3778
EOT;
3779
3780
        return $js;
3781
    }
3782
3783
    /**
3784
     * @param float  $score
3785
     * @param float  $weight
3786
     * @param string $pass_percentage
3787
     *
3788
     * @return string
3789
     */
3790
    public static function showSuccessMessage($score, $weight, $pass_percentage)
3791
    {
3792
        $res = '';
3793
        if (self::isPassPercentageEnabled($pass_percentage)) {
3794
            $isSuccess = self::isSuccessExerciseResult(
3795
                $score,
3796
                $weight,
3797
                $pass_percentage
3798
            );
3799
3800
            if ($isSuccess) {
3801
                $html = get_lang('CongratulationsYouPassedTheTest');
3802
                $icon = Display::return_icon(
3803
                    'completed.png',
3804
                    get_lang('Correct'),
3805
                    [],
3806
                    ICON_SIZE_MEDIUM
3807
                );
3808
            } else {
3809
                $html = get_lang('YouDidNotReachTheMinimumScore');
3810
                $icon = Display::return_icon(
3811
                    'warning.png',
3812
                    get_lang('Wrong'),
3813
                    [],
3814
                    ICON_SIZE_MEDIUM
3815
                );
3816
            }
3817
            $html = Display::tag('h4', $html);
3818
            $html .= Display::tag(
3819
                'h5',
3820
                $icon,
3821
                ['style' => 'width:40px; padding:2px 10px 0px 0px']
3822
            );
3823
            $res = $html;
3824
        }
3825
3826
        return $res;
3827
    }
3828
3829
    /**
3830
     * Return true if pass_pourcentage activated (we use the pass pourcentage feature
3831
     * return false if pass_percentage = 0 (we don't use the pass pourcentage feature.
3832
     *
3833
     * @param $value
3834
     *
3835
     * @return bool
3836
     *              In this version, pass_percentage and show_success_message are disabled if
3837
     *              pass_percentage is set to 0
3838
     */
3839
    public static function isPassPercentageEnabled($value)
3840
    {
3841
        return $value > 0;
3842
    }
3843
3844
    /**
3845
     * Converts a numeric value in a percentage example 0.66666 to 66.67 %.
3846
     *
3847
     * @param $value
3848
     *
3849
     * @return float Converted number
3850
     */
3851
    public static function convert_to_percentage($value)
3852
    {
3853
        $return = '-';
3854
        if ($value != '') {
3855
            $return = float_format($value * 100, 1).' %';
3856
        }
3857
3858
        return $return;
3859
    }
3860
3861
    /**
3862
     * Getting all active exercises from a course from a session
3863
     * (if a session_id is provided we will show all the exercises in the course +
3864
     * all exercises in the session).
3865
     *
3866
     * @param array  $course_info
3867
     * @param int    $session_id
3868
     * @param bool   $check_publication_dates
3869
     * @param string $search                  Search exercise name
3870
     * @param bool   $search_all_sessions     Search exercises in all sessions
3871
     * @param   int 0 = only inactive exercises
0 ignored issues
show
Documentation Bug introduced by
The doc comment 0 at position 0 could not be parsed: Unknown type name '0' at position 0 in 0.
Loading history...
3872
     *                  1 = only active exercises,
3873
     *                  2 = all exercises
3874
     *                  3 = active <> -1
3875
     *
3876
     * @return array array with exercise data
3877
     */
3878
    public static function get_all_exercises(
3879
        $course_info = null,
3880
        $session_id = 0,
3881
        $check_publication_dates = false,
3882
        $search = '',
3883
        $search_all_sessions = false,
3884
        $active = 2
3885
    ) {
3886
        $course_id = api_get_course_int_id();
3887
3888
        if (!empty($course_info) && !empty($course_info['real_id'])) {
3889
            $course_id = $course_info['real_id'];
3890
        }
3891
3892
        if ($session_id == -1) {
3893
            $session_id = 0;
3894
        }
3895
3896
        $now = api_get_utc_datetime();
3897
        $timeConditions = '';
3898
        if ($check_publication_dates) {
3899
            // Start and end are set
3900
            $timeConditions = " AND ((start_time <> '' AND start_time < '$now' AND end_time <> '' AND end_time > '$now' )  OR ";
3901
            // only start is set
3902
            $timeConditions .= " (start_time <> '' AND start_time < '$now' AND end_time is NULL) OR ";
3903
            // only end is set
3904
            $timeConditions .= " (start_time IS NULL AND end_time <> '' AND end_time > '$now') OR ";
3905
            // nothing is set
3906
            $timeConditions .= ' (start_time IS NULL AND end_time IS NULL)) ';
3907
        }
3908
3909
        $needle_where = !empty($search) ? " AND title LIKE '?' " : '';
3910
        $needle = !empty($search) ? "%".$search."%" : '';
3911
3912
        // Show courses by active status
3913
        $active_sql = '';
3914
        if ($active == 3) {
3915
            $active_sql = ' active <> -1 AND';
3916
        } else {
3917
            if ($active != 2) {
3918
                $active_sql = sprintf(' active = %d AND', $active);
3919
            }
3920
        }
3921
3922
        if ($search_all_sessions == true) {
3923
            $conditions = [
3924
                'where' => [
3925
                    $active_sql.' c_id = ? '.$needle_where.$timeConditions => [
3926
                        $course_id,
3927
                        $needle,
3928
                    ],
3929
                ],
3930
                'order' => 'title',
3931
            ];
3932
        } else {
3933
            if (empty($session_id)) {
3934
                $conditions = [
3935
                    'where' => [
3936
                        $active_sql.' (session_id = 0 OR session_id IS NULL) AND c_id = ? '.$needle_where.$timeConditions => [
3937
                            $course_id,
3938
                            $needle,
3939
                        ],
3940
                    ],
3941
                    'order' => 'title',
3942
                ];
3943
            } else {
3944
                $conditions = [
3945
                    'where' => [
3946
                        $active_sql.' (session_id = 0 OR session_id IS NULL OR session_id = ? ) AND c_id = ? '.$needle_where.$timeConditions => [
3947
                            $session_id,
3948
                            $course_id,
3949
                            $needle,
3950
                        ],
3951
                    ],
3952
                    'order' => 'title',
3953
                ];
3954
            }
3955
        }
3956
3957
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
3958
3959
        return Database::select('*', $table, $conditions);
3960
    }
3961
3962
    /**
3963
     * Getting all exercises (active only or all)
3964
     * from a course from a session
3965
     * (if a session_id is provided we will show all the exercises in the
3966
     * course + all exercises in the session).
3967
     *
3968
     * @param   array   course data
3969
     * @param   int     session id
3970
     * @param    int        course c_id
3971
     * @param bool $only_active_exercises
3972
     *
3973
     * @return array array with exercise data
3974
     *               modified by Hubert Borderiou
3975
     */
3976
    public static function get_all_exercises_for_course_id(
3977
        $course_info = null,
3978
        $session_id = 0,
3979
        $course_id = 0,
3980
        $only_active_exercises = true
3981
    ) {
3982
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
3983
3984
        if ($only_active_exercises) {
3985
            // Only active exercises.
3986
            $sql_active_exercises = "active = 1 AND ";
3987
        } else {
3988
            // Not only active means visible and invisible NOT deleted (-2)
3989
            $sql_active_exercises = "active IN (1, 0) AND ";
3990
        }
3991
3992
        if ($session_id == -1) {
3993
            $session_id = 0;
3994
        }
3995
3996
        $params = [
3997
            $session_id,
3998
            $course_id,
3999
        ];
4000
4001
        if (empty($session_id)) {
4002
            $conditions = [
4003
                'where' => ["$sql_active_exercises (session_id = 0 OR session_id IS NULL) AND c_id = ?" => [$course_id]],
4004
                'order' => 'title',
4005
            ];
4006
        } else {
4007
            // All exercises
4008
            $conditions = [
4009
                'where' => ["$sql_active_exercises (session_id = 0 OR session_id IS NULL OR session_id = ? ) AND c_id = ?" => $params],
4010
                'order' => 'title',
4011
            ];
4012
        }
4013
4014
        return Database::select('*', $table, $conditions);
4015
    }
4016
4017
    /**
4018
     * Gets the position of the score based in a given score (result/weight)
4019
     * and the exe_id based in the user list
4020
     * (NO Exercises in LPs ).
4021
     *
4022
     * @param float  $my_score      user score to be compared *attention*
4023
     *                              $my_score = score/weight and not just the score
4024
     * @param int    $my_exe_id     exe id of the exercise
4025
     *                              (this is necessary because if 2 students have the same score the one
4026
     *                              with the minor exe_id will have a best position, just to be fair and FIFO)
4027
     * @param int    $exercise_id
4028
     * @param string $course_code
4029
     * @param int    $session_id
4030
     * @param array  $user_list
4031
     * @param bool   $return_string
4032
     *
4033
     * @return int the position of the user between his friends in a course
4034
     *             (or course within a session)
4035
     */
4036
    public static function get_exercise_result_ranking(
4037
        $my_score,
4038
        $my_exe_id,
4039
        $exercise_id,
4040
        $course_code,
4041
        $session_id = 0,
4042
        $user_list = [],
4043
        $return_string = true,
4044
        $skipLpResults = true
4045
    ) {
4046
        //No score given we return
4047
        if (is_null($my_score)) {
4048
            return '-';
4049
        }
4050
        if (empty($user_list)) {
4051
            return '-';
4052
        }
4053
4054
        $best_attempts = [];
4055
        foreach ($user_list as $user_data) {
4056
            $user_id = $user_data['user_id'];
4057
            $best_attempts[$user_id] = self::get_best_attempt_by_user(
4058
                $user_id,
4059
                $exercise_id,
4060
                $course_code,
4061
                $session_id,
4062
                $skipLpResults
4063
            );
4064
        }
4065
4066
        if (empty($best_attempts)) {
4067
            return 1;
4068
        } else {
4069
            $position = 1;
4070
            $my_ranking = [];
4071
            foreach ($best_attempts as $user_id => $result) {
4072
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
4073
                    $my_ranking[$user_id] = $result['exe_result'] / $result['exe_weighting'];
4074
                } else {
4075
                    $my_ranking[$user_id] = 0;
4076
                }
4077
            }
4078
            //if (!empty($my_ranking)) {
4079
            asort($my_ranking);
4080
            $position = count($my_ranking);
4081
            if (!empty($my_ranking)) {
4082
                foreach ($my_ranking as $user_id => $ranking) {
4083
                    if ($my_score >= $ranking) {
4084
                        if ($my_score == $ranking && isset($best_attempts[$user_id]['exe_id'])) {
4085
                            $exe_id = $best_attempts[$user_id]['exe_id'];
4086
                            if ($my_exe_id < $exe_id) {
4087
                                $position--;
4088
                            }
4089
                        } else {
4090
                            $position--;
4091
                        }
4092
                    }
4093
                }
4094
            }
4095
            //}
4096
            $return_value = [
4097
                'position' => $position,
4098
                'count' => count($my_ranking),
4099
            ];
4100
4101
            if ($return_string) {
4102
                if (!empty($position) && !empty($my_ranking)) {
4103
                    $return_value = $position.'/'.count($my_ranking);
4104
                } else {
4105
                    $return_value = '-';
4106
                }
4107
            }
4108
4109
            return $return_value;
4110
        }
4111
    }
4112
4113
    /**
4114
     * Gets the position of the score based in a given score (result/weight) and the exe_id based in all attempts
4115
     * (NO Exercises in LPs ) old functionality by attempt.
4116
     *
4117
     * @param   float   user score to be compared attention => score/weight
4118
     * @param   int     exe id of the exercise
4119
     * (this is necessary because if 2 students have the same score the one
4120
     * with the minor exe_id will have a best position, just to be fair and FIFO)
4121
     * @param   int     exercise id
4122
     * @param   string  course code
4123
     * @param   int     session id
4124
     * @param bool $return_string
4125
     *
4126
     * @return int the position of the user between his friends in a course (or course within a session)
4127
     */
4128
    public static function get_exercise_result_ranking_by_attempt(
4129
        $my_score,
4130
        $my_exe_id,
4131
        $exercise_id,
4132
        $courseId,
4133
        $session_id = 0,
4134
        $return_string = true
4135
    ) {
4136
        if (empty($session_id)) {
4137
            $session_id = 0;
4138
        }
4139
        if (is_null($my_score)) {
4140
            return '-';
4141
        }
4142
        $user_results = Event::get_all_exercise_results(
4143
            $exercise_id,
4144
            $courseId,
4145
            $session_id,
4146
            false
4147
        );
4148
        $position_data = [];
4149
        if (empty($user_results)) {
4150
            return 1;
4151
        } else {
4152
            $position = 1;
4153
            $my_ranking = [];
4154
            foreach ($user_results as $result) {
4155
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
4156
                    $my_ranking[$result['exe_id']] = $result['exe_result'] / $result['exe_weighting'];
4157
                } else {
4158
                    $my_ranking[$result['exe_id']] = 0;
4159
                }
4160
            }
4161
            asort($my_ranking);
4162
            $position = count($my_ranking);
4163
            if (!empty($my_ranking)) {
4164
                foreach ($my_ranking as $exe_id => $ranking) {
4165
                    if ($my_score >= $ranking) {
4166
                        if ($my_score == $ranking) {
4167
                            if ($my_exe_id < $exe_id) {
4168
                                $position--;
4169
                            }
4170
                        } else {
4171
                            $position--;
4172
                        }
4173
                    }
4174
                }
4175
            }
4176
            $return_value = [
4177
                'position' => $position,
4178
                'count' => count($my_ranking),
4179
            ];
4180
4181
            if ($return_string) {
4182
                if (!empty($position) && !empty($my_ranking)) {
4183
                    return $position.'/'.count($my_ranking);
4184
                }
4185
            }
4186
4187
            return $return_value;
4188
        }
4189
    }
4190
4191
    /**
4192
     * Get the best attempt in a exercise (NO Exercises in LPs ).
4193
     *
4194
     * @param int $exercise_id
4195
     * @param int $courseId
4196
     * @param int $session_id
4197
     *
4198
     * @return array
4199
     */
4200
    public static function get_best_attempt_in_course($exercise_id, $courseId, $session_id, $skipLpResults = true)
4201
    {
4202
        $user_results = Event::get_all_exercise_results(
4203
            $exercise_id,
4204
            $courseId,
4205
            $session_id,
4206
            false,
4207
            null,
4208
            0,
4209
            $skipLpResults
4210
        );
4211
4212
        $best_score_data = [];
4213
        $best_score = 0;
4214
        if (!empty($user_results)) {
4215
            foreach ($user_results as $result) {
4216
                if (!empty($result['exe_weighting']) &&
4217
                    intval($result['exe_weighting']) != 0
4218
                ) {
4219
                    $score = $result['exe_result'] / $result['exe_weighting'];
4220
                    if ($score >= $best_score) {
4221
                        $best_score = $score;
4222
                        $best_score_data = $result;
4223
                    }
4224
                }
4225
            }
4226
        }
4227
4228
        return $best_score_data;
4229
    }
4230
4231
    /**
4232
     * Get the best score in a exercise (NO Exercises in LPs ).
4233
     *
4234
     * @param int $user_id
4235
     * @param int $exercise_id
4236
     * @param int $courseId
4237
     * @param int $session_id
4238
     *
4239
     * @return array
4240
     */
4241
    public static function get_best_attempt_by_user(
4242
        $user_id,
4243
        $exercise_id,
4244
        $courseId,
4245
        $session_id,
4246
        $skipLpResults = true
4247
    ) {
4248
        $user_results = Event::get_all_exercise_results(
4249
            $exercise_id,
4250
            $courseId,
4251
            $session_id,
4252
            false,
4253
            $user_id,
4254
            0,
4255
            $skipLpResults
4256
        );
4257
        $best_score_data = [];
4258
        $best_score = 0;
4259
        if (!empty($user_results)) {
4260
            foreach ($user_results as $result) {
4261
                if (!empty($result['exe_weighting']) && (float) $result['exe_weighting'] != 0) {
4262
                    $score = $result['exe_result'] / $result['exe_weighting'];
4263
                    if ($score >= $best_score) {
4264
                        $best_score = $score;
4265
                        $best_score_data = $result;
4266
                    }
4267
                }
4268
            }
4269
        }
4270
4271
        return $best_score_data;
4272
    }
4273
4274
    /**
4275
     * Get average score (NO Exercises in LPs ).
4276
     *
4277
     * @param int $exerciseId
4278
     * @param int $courseId
4279
     * @param int $sessionId
4280
     *
4281
     * @return float Average score
4282
     */
4283
    public static function get_average_score($exerciseId, $courseId, $sessionId, $groupId = 0)
4284
    {
4285
        $user_results = Event::get_all_exercise_results(
4286
            $exerciseId,
4287
            $courseId,
4288
            $sessionId,
4289
            true,
4290
            null,
4291
            $groupId
4292
        );
4293
        $avg_score = 0;
4294
        if (!empty($user_results)) {
4295
            foreach ($user_results as $result) {
4296
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
4297
                    $score = $result['exe_result'] / $result['exe_weighting'];
4298
                    $avg_score += $score;
4299
                }
4300
            }
4301
            $avg_score = float_format($avg_score / count($user_results), 1);
4302
        }
4303
4304
        return $avg_score;
4305
    }
4306
4307
    /**
4308
     * Get average quiz score by course (Only exercises not added in a LP).
4309
     *
4310
     * @param int $courseId
4311
     * @param int $sessionId
4312
     *
4313
     * @return float Average score
4314
     */
4315
    public static function get_average_score_by_course($courseId, $sessionId)
4316
    {
4317
        $user_results = Event::get_all_exercise_results_by_course(
4318
            $courseId,
4319
            $sessionId,
4320
            false
4321
        );
4322
        $avg_score = 0;
4323
        if (!empty($user_results)) {
4324
            foreach ($user_results as $result) {
4325
                if (!empty($result['exe_weighting']) && intval(
4326
                        $result['exe_weighting']
4327
                    ) != 0
4328
                ) {
4329
                    $score = $result['exe_result'] / $result['exe_weighting'];
4330
                    $avg_score += $score;
4331
                }
4332
            }
4333
            // We assume that all exe_weighting
4334
            $avg_score = $avg_score / count($user_results);
4335
        }
4336
4337
        return $avg_score;
4338
    }
4339
4340
    /**
4341
     * @param int $user_id
4342
     * @param int $courseId
4343
     * @param int $session_id
4344
     *
4345
     * @return float|int
4346
     */
4347
    public static function get_average_score_by_course_by_user(
4348
        $user_id,
4349
        $courseId,
4350
        $session_id
4351
    ) {
4352
        $user_results = Event::get_all_exercise_results_by_user(
4353
            $user_id,
4354
            $courseId,
4355
            $session_id
4356
        );
4357
        $avg_score = 0;
4358
        if (!empty($user_results)) {
4359
            foreach ($user_results as $result) {
4360
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
4361
                    $score = $result['exe_result'] / $result['exe_weighting'];
4362
                    $avg_score += $score;
4363
                }
4364
            }
4365
            // We assume that all exe_weighting
4366
            $avg_score = ($avg_score / count($user_results));
4367
        }
4368
4369
        return $avg_score;
4370
    }
4371
4372
    /**
4373
     * Get average score by score (NO Exercises in LPs ).
4374
     *
4375
     * @param int $exercise_id
4376
     * @param int $courseId
4377
     * @param int $session_id
4378
     * @param int $user_count
4379
     *
4380
     * @return float Best average score
4381
     */
4382
    public static function get_best_average_score_by_exercise(
4383
        $exercise_id,
4384
        $courseId,
4385
        $session_id,
4386
        $user_count
4387
    ) {
4388
        $user_results = Event::get_best_exercise_results_by_user(
4389
            $exercise_id,
4390
            $courseId,
4391
            $session_id
4392
        );
4393
        $avg_score = 0;
4394
        if (!empty($user_results)) {
4395
            foreach ($user_results as $result) {
4396
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
4397
                    $score = $result['exe_result'] / $result['exe_weighting'];
4398
                    $avg_score += $score;
4399
                }
4400
            }
4401
            // We asumme that all exe_weighting
4402
            if (!empty($user_count)) {
4403
                $avg_score = float_format($avg_score / $user_count, 1) * 100;
4404
            } else {
4405
                $avg_score = 0;
4406
            }
4407
        }
4408
4409
        return $avg_score;
4410
    }
4411
4412
    /**
4413
     * Get average score by score (NO Exercises in LPs ).
4414
     *
4415
     * @param int $exercise_id
4416
     * @param int $courseId
4417
     * @param int $session_id
4418
     *
4419
     * @return float Best average score
4420
     */
4421
    public static function getBestScoreByExercise(
4422
        $exercise_id,
4423
        $courseId,
4424
        $session_id
4425
    ) {
4426
        $user_results = Event::get_best_exercise_results_by_user(
4427
            $exercise_id,
4428
            $courseId,
4429
            $session_id
4430
        );
4431
        $avg_score = 0;
4432
        if (!empty($user_results)) {
4433
            foreach ($user_results as $result) {
4434
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
4435
                    $score = $result['exe_result'] / $result['exe_weighting'];
4436
                    $avg_score += $score;
4437
                }
4438
            }
4439
        }
4440
4441
        return $avg_score;
4442
    }
4443
4444
    /**
4445
     * @param string $course_code
4446
     * @param int    $session_id
4447
     *
4448
     * @return array
4449
     */
4450
    public static function get_exercises_to_be_taken($course_code, $session_id)
4451
    {
4452
        $course_info = api_get_course_info($course_code);
4453
        $exercises = self::get_all_exercises($course_info, $session_id);
4454
        $result = [];
4455
        $now = time() + 15 * 24 * 60 * 60;
4456
        foreach ($exercises as $exercise_item) {
4457
            if (isset($exercise_item['end_time']) &&
4458
                !empty($exercise_item['end_time']) &&
4459
                api_strtotime($exercise_item['end_time'], 'UTC') < $now
4460
            ) {
4461
                $result[] = $exercise_item;
4462
            }
4463
        }
4464
4465
        return $result;
4466
    }
4467
4468
    /**
4469
     * Get student results (only in completed exercises) stats by question.
4470
     *
4471
     * @param int  $question_id
4472
     * @param int  $exercise_id
4473
     * @param int  $courseId
4474
     * @param int  $session_id
4475
     * @param bool $onlyStudent Filter only enrolled students
4476
     *
4477
     * @return array
4478
     */
4479
    public static function get_student_stats_by_question(
4480
        $question_id,
4481
        $exercise_id,
4482
        $courseId,
4483
        $session_id,
4484
        $onlyStudent = false
4485
    ) {
4486
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4487
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4488
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4489
4490
        $question_id = (int) $question_id;
4491
        $exercise_id = (int) $exercise_id;
4492
        $session_id = (int) $session_id;
4493
        $courseId = (int) $courseId;
4494
4495
        $sql = "SELECT MAX(marks) as max, MIN(marks) as min, AVG(marks) as average
4496
                FROM $track_exercises e ";
4497
        if ($onlyStudent) {
4498
            if (empty($session_id)) {
4499
                $courseCondition = "
4500
                    INNER JOIN $courseUser c
4501
                    ON (
4502
                        e.exe_user_id = c.user_id AND
4503
                        e.c_id = c.c_id AND
4504
                        c.status = ".STUDENT."
4505
                        AND relation_type <> 2
4506
                    )";
4507
            } else {
4508
                $sessionRelCourse = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4509
                $courseCondition = "
4510
                    INNER JOIN $sessionRelCourse sc
4511
                    ON (
4512
                        e.exe_user_id = sc.user_id AND
4513
                        e.c_id = sc.c_id AND
4514
                        e.session_id = sc.session_id AND
4515
                        sc.status = 0
4516
                    ) ";
4517
            }
4518
            $sql .= $courseCondition;
4519
        }
4520
4521
        $sql .= "
4522
            INNER JOIN $track_attempt a
4523
    		ON (
4524
    		    a.exe_id = e.exe_id AND
4525
    		    e.c_id = a.c_id AND
4526
    		    e.session_id  = a.session_id
4527
            )
4528
    		WHERE
4529
    		    exe_exo_id 	= $exercise_id AND
4530
                a.c_id = $courseId AND
4531
                e.session_id = $session_id AND
4532
                question_id = $question_id AND
4533
                e.status = ''
4534
            LIMIT 1";
4535
        $result = Database::query($sql);
4536
        $return = [];
4537
        if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
4538
            $return = Database::fetch_array($result, 'ASSOC');
4539
        }
4540
4541
        return $return;
4542
    }
4543
4544
    /**
4545
     * Get the correct answer count for a fill blanks question.
4546
     *
4547
     * @param int $question_id
4548
     * @param int $exercise_id
4549
     *
4550
     * @return array
4551
     */
4552
    public static function getNumberStudentsFillBlanksAnswerCount(
4553
        $question_id,
4554
        $exercise_id
4555
    ) {
4556
        $listStudentsId = [];
4557
        $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
4558
            api_get_course_id(),
4559
            true
4560
        );
4561
        foreach ($listAllStudentInfo as $i => $listStudentInfo) {
4562
            $listStudentsId[] = $listStudentInfo['user_id'];
4563
        }
4564
4565
        $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
4566
            $exercise_id,
4567
            $question_id,
4568
            $listStudentsId,
4569
            '1970-01-01',
4570
            '3000-01-01'
4571
        );
4572
4573
        $arrayCount = [];
4574
4575
        foreach ($listFillTheBlankResult as $resultCount) {
4576
            foreach ($resultCount as $index => $count) {
4577
                //this is only for declare the array index per answer
4578
                $arrayCount[$index] = 0;
4579
            }
4580
        }
4581
4582
        foreach ($listFillTheBlankResult as $resultCount) {
4583
            foreach ($resultCount as $index => $count) {
4584
                $count = ($count === 0) ? 1 : 0;
4585
                $arrayCount[$index] += $count;
4586
            }
4587
        }
4588
4589
        return $arrayCount;
4590
    }
4591
4592
    /**
4593
     * Get the number of questions with answers.
4594
     *
4595
     * @param int    $question_id
4596
     * @param int    $exercise_id
4597
     * @param string $course_code
4598
     * @param int    $session_id
4599
     * @param string $questionType
4600
     *
4601
     * @return int
4602
     */
4603
    public static function get_number_students_question_with_answer_count(
4604
        $question_id,
4605
        $exercise_id,
4606
        $course_code,
4607
        $session_id,
4608
        $questionType = ''
4609
    ) {
4610
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4611
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4612
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4613
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4614
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4615
4616
        $question_id = intval($question_id);
4617
        $exercise_id = intval($exercise_id);
4618
        $courseId = api_get_course_int_id($course_code);
4619
        $session_id = intval($session_id);
4620
4621
        if (in_array($questionType, [FILL_IN_BLANKS, FILL_IN_BLANKS_COMBINATION])) {
4622
            $listStudentsId = [];
4623
            $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
4624
                api_get_course_id(),
4625
                true
4626
            );
4627
            foreach ($listAllStudentInfo as $i => $listStudentInfo) {
4628
                $listStudentsId[] = $listStudentInfo['user_id'];
4629
            }
4630
4631
            $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
4632
                $exercise_id,
4633
                $question_id,
4634
                $listStudentsId,
4635
                '1970-01-01',
4636
                '3000-01-01'
4637
            );
4638
4639
            return FillBlanks::getNbResultFillBlankAll($listFillTheBlankResult);
4640
        }
4641
4642
        if (empty($session_id)) {
4643
            $courseCondition = "
4644
            INNER JOIN $courseUser cu
4645
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
4646
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4647
        } else {
4648
            $courseCondition = "
4649
            INNER JOIN $courseUserSession cu
4650
            ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4651
            $courseConditionWhere = " AND cu.status = 0 ";
4652
        }
4653
4654
        $sql = "SELECT DISTINCT exe_user_id
4655
    		FROM $track_exercises e
4656
    		INNER JOIN $track_attempt a
4657
    		ON (
4658
    		    a.exe_id = e.exe_id AND
4659
    		    e.c_id = a.c_id AND
4660
    		    e.session_id  = a.session_id
4661
            )
4662
            INNER JOIN $courseTable c
4663
            ON (c.id = a.c_id)
4664
    		$courseCondition
4665
    		WHERE
4666
    		    exe_exo_id = $exercise_id AND
4667
                a.c_id = $courseId AND
4668
                e.session_id = $session_id AND
4669
                question_id = $question_id AND
4670
                answer <> '0' AND
4671
                e.status = ''
4672
                $courseConditionWhere
4673
            ";
4674
        $result = Database::query($sql);
4675
        $return = 0;
4676
        if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
4677
            $return = Database::num_rows($result);
4678
        }
4679
4680
        return $return;
4681
    }
4682
4683
    /**
4684
     * Get number of answers to hotspot questions.
4685
     *
4686
     * @param int    $answer_id
4687
     * @param int    $question_id
4688
     * @param int    $exercise_id
4689
     * @param string $course_code
4690
     * @param int    $session_id
4691
     *
4692
     * @return int
4693
     */
4694
    public static function get_number_students_answer_hotspot_count(
4695
        $answer_id,
4696
        $question_id,
4697
        $exercise_id,
4698
        $course_code,
4699
        $session_id
4700
    ) {
4701
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4702
        $track_hotspot = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4703
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4704
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4705
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4706
4707
        $question_id = (int) $question_id;
4708
        $answer_id = (int) $answer_id;
4709
        $exercise_id = (int) $exercise_id;
4710
        $course_code = Database::escape_string($course_code);
4711
        $session_id = (int) $session_id;
4712
4713
        if (empty($session_id)) {
4714
            $courseCondition = "
4715
            INNER JOIN $courseUser cu
4716
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
4717
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4718
        } else {
4719
            $courseCondition = "
4720
            INNER JOIN $courseUserSession cu
4721
            ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4722
            $courseConditionWhere = ' AND cu.status = 0 ';
4723
        }
4724
4725
        $sql = "SELECT DISTINCT exe_user_id
4726
    		FROM $track_exercises e
4727
    		INNER JOIN $track_hotspot a
4728
    		ON (a.hotspot_exe_id = e.exe_id)
4729
    		INNER JOIN $courseTable c
4730
    		ON (hotspot_course_code = c.code)
4731
    		$courseCondition
4732
    		WHERE
4733
    		    exe_exo_id              = $exercise_id AND
4734
                a.hotspot_course_code 	= '$course_code' AND
4735
                e.session_id            = $session_id AND
4736
                hotspot_answer_id       = $answer_id AND
4737
                hotspot_question_id     = $question_id AND
4738
                hotspot_correct         =  1 AND
4739
                e.status                = ''
4740
                $courseConditionWhere
4741
            ";
4742
4743
        $result = Database::query($sql);
4744
        $return = 0;
4745
        if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
4746
            $return = Database::num_rows($result);
4747
        }
4748
4749
        return $return;
4750
    }
4751
4752
    /**
4753
     * @param int    $answer_id
4754
     * @param int    $question_id
4755
     * @param int    $exercise_id
4756
     * @param int    $courseId
4757
     * @param int    $session_id
4758
     * @param string $question_type
4759
     * @param string $correct_answer
4760
     * @param string $current_answer
4761
     *
4762
     * @return int
4763
     */
4764
    public static function get_number_students_answer_count(
4765
        $answer_id,
4766
        $question_id,
4767
        $exercise_id,
4768
        $courseId,
4769
        $session_id,
4770
        $question_type = null,
4771
        $correct_answer = null,
4772
        $current_answer = null
4773
    ) {
4774
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4775
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4776
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4777
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4778
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4779
4780
        $question_id = (int) $question_id;
4781
        $answer_id = (int) $answer_id;
4782
        $exercise_id = (int) $exercise_id;
4783
        $courseId = (int) $courseId;
4784
        $session_id = (int) $session_id;
4785
4786
        switch ($question_type) {
4787
            case FILL_IN_BLANKS:
4788
            case FILL_IN_BLANKS_COMBINATION:
4789
                $answer_condition = '';
4790
                $select_condition = ' e.exe_id, answer ';
4791
                break;
4792
            case MATCHING:
4793
            case MATCHING_COMBINATION:
4794
            case MATCHING_DRAGGABLE:
4795
            case MATCHING_DRAGGABLE_COMBINATION:
4796
            default:
4797
                $answer_condition = " answer = $answer_id AND ";
4798
                $select_condition = ' DISTINCT exe_user_id ';
4799
        }
4800
4801
        if (empty($session_id)) {
4802
            $courseCondition = "
4803
            INNER JOIN $courseUser cu
4804
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
4805
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4806
        } else {
4807
            $courseCondition = "
4808
            INNER JOIN $courseUserSession cu
4809
            ON (cu.c_id = a.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4810
            $courseConditionWhere = ' AND cu.status = 0 ';
4811
        }
4812
4813
        $sql = "SELECT $select_condition
4814
    		FROM $track_exercises e
4815
    		INNER JOIN $track_attempt a
4816
    		ON (
4817
    		    a.exe_id = e.exe_id AND
4818
    		    e.c_id = a.c_id AND
4819
    		    e.session_id  = a.session_id
4820
            )
4821
            INNER JOIN $courseTable c
4822
            ON c.id = a.c_id
4823
    		$courseCondition
4824
    		WHERE
4825
    		    exe_exo_id = $exercise_id AND
4826
                a.c_id = $courseId AND
4827
                e.session_id = $session_id AND
4828
                $answer_condition
4829
                question_id = $question_id AND
4830
                e.status = ''
4831
                $courseConditionWhere
4832
            ";
4833
        $result = Database::query($sql);
4834
        $return = 0;
4835
        if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
4836
            $good_answers = 0;
4837
            switch ($question_type) {
4838
                case FILL_IN_BLANKS:
4839
                case FILL_IN_BLANKS_COMBINATION:
4840
                    while ($row = Database::fetch_array($result, 'ASSOC')) {
4841
                        $fill_blank = self::check_fill_in_blanks(
4842
                            $correct_answer,
4843
                            $row['answer'],
4844
                            $current_answer
4845
                        );
4846
                        if (isset($fill_blank[$current_answer]) && $fill_blank[$current_answer] == 1) {
4847
                            $good_answers++;
4848
                        }
4849
                    }
4850
4851
                    return $good_answers;
4852
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

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

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

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

Loading history...
4853
                case MATCHING:
4854
                case MATCHING_COMBINATION:
4855
                case MATCHING_DRAGGABLE:
4856
                case MATCHING_DRAGGABLE_COMBINATION:
4857
                default:
4858
                    $return = Database::num_rows($result);
4859
            }
4860
        }
4861
4862
        return $return;
4863
    }
4864
4865
    /**
4866
     * @param array  $answer
4867
     * @param string $user_answer
4868
     *
4869
     * @return array
4870
     */
4871
    public static function check_fill_in_blanks($answer, $user_answer, $current_answer)
4872
    {
4873
        // the question is encoded like this
4874
        // [A] B [C] D [E] F::10,10,10@1
4875
        // number 1 before the "@" means that is a switchable fill in blank question
4876
        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4877
        // means that is a normal fill blank question
4878
        // first we explode the "::"
4879
        $pre_array = explode('::', $answer);
4880
        // is switchable fill blank or not
4881
        $last = count($pre_array) - 1;
4882
        $is_set_switchable = explode('@', $pre_array[$last]);
4883
        $switchable_answer_set = false;
4884
        if (isset($is_set_switchable[1]) && $is_set_switchable[1] == 1) {
4885
            $switchable_answer_set = true;
4886
        }
4887
        $answer = '';
4888
        for ($k = 0; $k < $last; $k++) {
4889
            $answer .= $pre_array[$k];
4890
        }
4891
        // splits weightings that are joined with a comma
4892
        $answerWeighting = explode(',', $is_set_switchable[0]);
4893
4894
        // we save the answer because it will be modified
4895
        //$temp = $answer;
4896
        $temp = $answer;
4897
4898
        $answer = '';
4899
        $j = 0;
4900
        //initialise answer tags
4901
        $user_tags = $correct_tags = $real_text = [];
4902
        // the loop will stop at the end of the text
4903
        while (1) {
4904
            // quits the loop if there are no more blanks (detect '[')
4905
            if (($pos = api_strpos($temp, '[')) === false) {
4906
                // adds the end of the text
4907
                $answer = $temp;
4908
                $real_text[] = $answer;
4909
                break; //no more "blanks", quit the loop
4910
            }
4911
            // adds the piece of text that is before the blank
4912
            //and ends with '[' into a general storage array
4913
            $real_text[] = api_substr($temp, 0, $pos + 1);
4914
            $answer .= api_substr($temp, 0, $pos + 1);
4915
            //take the string remaining (after the last "[" we found)
4916
            $temp = api_substr($temp, $pos + 1);
4917
            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4918
            if (($pos = api_strpos($temp, ']')) === false) {
4919
                // adds the end of the text
4920
                $answer .= $temp;
4921
                break;
4922
            }
4923
4924
            $str = $user_answer;
4925
4926
            preg_match_all('#\[([^[]*)\]#', $str, $arr);
4927
            $str = str_replace('\r\n', '', $str);
4928
            $choices = $arr[1];
4929
            $choice = [];
4930
            $check = false;
4931
            $i = 0;
4932
            foreach ($choices as $item) {
4933
                if ($current_answer === $item) {
4934
                    $check = true;
4935
                }
4936
                if ($check) {
4937
                    $choice[] = $item;
4938
                    $i++;
4939
                }
4940
                if ($i == 3) {
4941
                    break;
4942
                }
4943
            }
4944
            $tmp = api_strrpos($choice[$j], ' / ');
4945
4946
            if ($tmp !== false) {
4947
                $choice[$j] = api_substr($choice[$j], 0, $tmp);
4948
            }
4949
4950
            $choice[$j] = trim($choice[$j]);
4951
4952
            //Needed to let characters ' and " to work as part of an answer
4953
            $choice[$j] = stripslashes($choice[$j]);
4954
4955
            $user_tags[] = api_strtolower($choice[$j]);
4956
            //put the contents of the [] answer tag into correct_tags[]
4957
            $correct_tags[] = api_strtolower(api_substr($temp, 0, $pos));
4958
            $j++;
4959
            $temp = api_substr($temp, $pos + 1);
4960
        }
4961
4962
        $answer = '';
4963
        $real_correct_tags = $correct_tags;
4964
        $chosen_list = [];
4965
        $good_answer = [];
4966
4967
        for ($i = 0; $i < count($real_correct_tags); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

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

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

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4968
            if (!$switchable_answer_set) {
4969
                //needed to parse ' and " characters
4970
                $user_tags[$i] = stripslashes($user_tags[$i]);
4971
                if ($correct_tags[$i] == $user_tags[$i]) {
4972
                    $good_answer[$correct_tags[$i]] = 1;
4973
                } elseif (!empty($user_tags[$i])) {
4974
                    $good_answer[$correct_tags[$i]] = 0;
4975
                } else {
4976
                    $good_answer[$correct_tags[$i]] = 0;
4977
                }
4978
            } else {
4979
                // switchable fill in the blanks
4980
                if (in_array($user_tags[$i], $correct_tags)) {
4981
                    $correct_tags = array_diff($correct_tags, $chosen_list);
4982
                    $good_answer[$correct_tags[$i]] = 1;
4983
                } elseif (!empty($user_tags[$i])) {
4984
                    $good_answer[$correct_tags[$i]] = 0;
4985
                } else {
4986
                    $good_answer[$correct_tags[$i]] = 0;
4987
                }
4988
            }
4989
            // adds the correct word, followed by ] to close the blank
4990
            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4991
            if (isset($real_text[$i + 1])) {
4992
                $answer .= $real_text[$i + 1];
4993
            }
4994
        }
4995
4996
        return $good_answer;
4997
    }
4998
4999
    /**
5000
     * It gets the number of users who finishing the exercise.
5001
     *
5002
     * @param int $exerciseId
5003
     * @param int $courseId
5004
     * @param int $sessionId
5005
     *
5006
     * @return int
5007
     */
5008
    public static function getNumberStudentsFinishExercise(
5009
        $exerciseId,
5010
        $courseId,
5011
        $sessionId
5012
    ) {
5013
        $tblTrackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5014
        $tblTrackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5015
5016
        $exerciseId = (int) $exerciseId;
5017
        $courseId = (int) $courseId;
5018
        $sessionId = (int) $sessionId;
5019
5020
        $sql = "SELECT DISTINCT exe_user_id
5021
                FROM $tblTrackExercises e
5022
                INNER JOIN $tblTrackAttempt a
5023
                ON (a.exe_id = e.exe_id)
5024
                WHERE
5025
                    exe_exo_id 	 = $exerciseId AND
5026
                    e.c_id  = $courseId AND
5027
                    e.session_id = $sessionId AND
5028
                    status = ''";
5029
        $result = Database::query($sql);
5030
        $return = 0;
5031
        if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
5032
            $return = Database::num_rows($result);
5033
        }
5034
5035
        return $return;
5036
    }
5037
5038
    /**
5039
     * Return an HTML select menu with the student groups.
5040
     *
5041
     * @param string $name     is the name and the id of the <select>
5042
     * @param string $default  default value for option
5043
     * @param string $onchange
5044
     *
5045
     * @return string the html code of the <select>
5046
     */
5047
    public static function displayGroupMenu($name, $default, $onchange = "")
5048
    {
5049
        // check the default value of option
5050
        $tabSelected = [$default => " selected='selected' "];
5051
        $res = "";
5052
        $res .= "<select name='$name' id='$name' onchange='".$onchange."' >";
5053
        $res .= "<option value='-1'".$tabSelected["-1"].">-- ".get_lang(
5054
                'AllGroups'
5055
            )." --</option>";
5056
        $res .= "<option value='0'".$tabSelected["0"].">- ".get_lang(
5057
                'NotInAGroup'
5058
            )." -</option>";
5059
        $tabGroups = GroupManager::get_group_list();
5060
        $currentCatId = 0;
5061
        $countGroups = count($tabGroups);
5062
        for ($i = 0; $i < $countGroups; $i++) {
5063
            $tabCategory = GroupManager::get_category_from_group(
5064
                $tabGroups[$i]['iid']
5065
            );
5066
            if ($tabCategory['iid'] != $currentCatId) {
5067
                $res .= "<option value='-1' disabled='disabled'>".$tabCategory['title']."</option>";
5068
                $currentCatId = $tabCategory['iid'];
5069
            }
5070
            $res .= "<option ".$tabSelected[$tabGroups[$i]['iid']]."style='margin-left:40px' value='".
5071
                $tabGroups[$i]['iid']."'>".
5072
                $tabGroups[$i]['name'].
5073
                "</option>";
5074
        }
5075
        $res .= "</select>";
5076
5077
        return $res;
5078
    }
5079
5080
    /**
5081
     * @param int $exe_id
5082
     */
5083
    public static function create_chat_exercise_session($exe_id)
5084
    {
5085
        if (!isset($_SESSION['current_exercises'])) {
5086
            $_SESSION['current_exercises'] = [];
5087
        }
5088
        $_SESSION['current_exercises'][$exe_id] = true;
5089
    }
5090
5091
    /**
5092
     * @param int $exe_id
5093
     */
5094
    public static function delete_chat_exercise_session($exe_id)
5095
    {
5096
        if (isset($_SESSION['current_exercises'])) {
5097
            $_SESSION['current_exercises'][$exe_id] = false;
5098
        }
5099
    }
5100
5101
    /**
5102
     * Display the exercise results.
5103
     *
5104
     * @param Exercise $objExercise
5105
     * @param int      $exeId
5106
     * @param bool     $save_user_result save users results (true) or just show the results (false)
5107
     * @param string   $remainingMessage
5108
     * @param bool     $allowSignature
5109
     * @param bool     $allowExportPdf
5110
     * @param bool     $isExport
5111
     */
5112
    public static function displayQuestionListByAttempt(
5113
        $objExercise,
5114
        $exeId,
5115
        $save_user_result = false,
5116
        $remainingMessage = '',
5117
        $allowSignature = false,
5118
        $allowExportPdf = false,
5119
        $isExport = false
5120
    ) {
5121
        $origin = api_get_origin();
5122
        $courseId = api_get_course_int_id();
5123
        $courseCode = api_get_course_id();
5124
        $sessionId = api_get_session_id();
5125
5126
        // Getting attempt info
5127
        $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
5128
5129
        // Getting question list
5130
        $question_list = [];
5131
        $studentInfo = [];
5132
        if (!empty($exercise_stat_info['data_tracking'])) {
5133
            $studentInfo = api_get_user_info($exercise_stat_info['exe_user_id']);
5134
            $question_list = explode(',', $exercise_stat_info['data_tracking']);
5135
        } else {
5136
            // Try getting the question list only if save result is off
5137
            if ($save_user_result == false) {
5138
                $question_list = $objExercise->get_validated_question_list();
5139
            }
5140
            if (in_array(
5141
                $objExercise->getFeedbackType(),
5142
                [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
5143
            )) {
5144
                $question_list = $objExercise->get_validated_question_list();
5145
            }
5146
        }
5147
5148
        if ($objExercise->getResultAccess()) {
5149
            if ($objExercise->hasResultsAccess($exercise_stat_info) === false) {
5150
                echo Display::return_message(
5151
                    sprintf(get_lang('YouPassedTheLimitOfXMinutesToSeeTheResults'), $objExercise->getResultsAccess())
5152
                );
5153
5154
                return false;
5155
            }
5156
5157
            if (!empty($objExercise->getResultAccess())) {
5158
                $url = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$objExercise->iid;
5159
                echo $objExercise->returnTimeLeftDiv();
5160
                echo $objExercise->showSimpleTimeControl(
5161
                    $objExercise->getResultAccessTimeDiff($exercise_stat_info),
5162
                    $url
5163
                );
5164
            }
5165
        }
5166
5167
        $counter = 1;
5168
        $total_score = $total_weight = 0;
5169
        $exercise_content = null;
5170
        // Hide results
5171
        $show_results = false;
5172
        $show_only_score = false;
5173
        if (in_array($objExercise->results_disabled,
5174
            [
5175
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
5176
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
5177
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
5178
            ]
5179
        )) {
5180
            $show_results = true;
5181
        }
5182
5183
        if (in_array(
5184
            $objExercise->results_disabled,
5185
            [
5186
                RESULT_DISABLE_SHOW_SCORE_ONLY,
5187
                RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES,
5188
                RESULT_DISABLE_RANKING,
5189
            ]
5190
        )
5191
        ) {
5192
            $show_only_score = true;
5193
        }
5194
5195
        // Not display expected answer, but score, and feedback
5196
        $show_all_but_expected_answer = false;
5197
        if ($objExercise->results_disabled == RESULT_DISABLE_SHOW_SCORE_ONLY &&
5198
            $objExercise->getFeedbackType() == EXERCISE_FEEDBACK_TYPE_END
5199
        ) {
5200
            $show_all_but_expected_answer = true;
5201
            $show_results = true;
5202
            $show_only_score = false;
5203
        }
5204
5205
        $showTotalScoreAndUserChoicesInLastAttempt = true;
5206
        $showTotalScore = true;
5207
        $showQuestionScore = true;
5208
        $attemptResult = [];
5209
5210
        if (in_array(
5211
            $objExercise->results_disabled,
5212
            [
5213
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
5214
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
5215
                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
5216
            ])
5217
        ) {
5218
            $show_only_score = true;
5219
            $show_results = true;
5220
            $numberAttempts = 0;
5221
            if ($objExercise->attempts > 0) {
5222
                $attempts = Event::getExerciseResultsByUser(
5223
                    api_get_user_id(),
5224
                    $objExercise->iid,
5225
                    $courseId,
5226
                    $sessionId,
5227
                    $exercise_stat_info['orig_lp_id'],
5228
                    $exercise_stat_info['orig_lp_item_id'],
5229
                    'desc'
5230
                );
5231
                if ($attempts) {
5232
                    $numberAttempts = count($attempts);
5233
                }
5234
5235
                if ($save_user_result) {
5236
                    $numberAttempts++;
5237
                }
5238
5239
                $showTotalScore = false;
5240
                if ($objExercise->results_disabled == RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT) {
5241
                    $showTotalScore = true;
5242
                }
5243
                $showTotalScoreAndUserChoicesInLastAttempt = false;
5244
                if ($numberAttempts >= $objExercise->attempts) {
5245
                    $showTotalScore = true;
5246
                    $show_results = true;
5247
                    $show_only_score = false;
5248
                    $showTotalScoreAndUserChoicesInLastAttempt = true;
5249
                }
5250
5251
                if ($objExercise->results_disabled == RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK) {
5252
                    $showTotalScore = true;
5253
                    $show_results = true;
5254
                    $show_only_score = false;
5255
                    $showTotalScoreAndUserChoicesInLastAttempt = false;
5256
                    if ($numberAttempts >= $objExercise->attempts) {
5257
                        $showTotalScoreAndUserChoicesInLastAttempt = true;
5258
                    }
5259
5260
                    // Check if the current attempt is the last.
5261
                    /*if (false === $save_user_result && !empty($attempts)) {
5262
                        $showTotalScoreAndUserChoicesInLastAttempt = false;
5263
                        $position = 1;
5264
                        foreach ($attempts as $attempt) {
5265
                            if ($exeId == $attempt['exe_id']) {
5266
                                break;
5267
                            }
5268
                            $position++;
5269
                        }
5270
5271
                        if ($position == $objExercise->attempts) {
5272
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
5273
                        }
5274
                    }*/
5275
                }
5276
            }
5277
5278
            if ($objExercise->results_disabled ==
5279
                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK
5280
            ) {
5281
                $show_only_score = false;
5282
                $show_results = true;
5283
                $show_all_but_expected_answer = false;
5284
                $showTotalScore = false;
5285
                $showQuestionScore = false;
5286
                if ($numberAttempts >= $objExercise->attempts) {
5287
                    $showTotalScore = true;
5288
                    $showQuestionScore = true;
5289
                }
5290
            }
5291
        }
5292
5293
        // When exporting to PDF hide feedback/comment/score show warning in hotspot.
5294
        if ($allowExportPdf && $isExport) {
5295
            $showTotalScore = false;
5296
            $showQuestionScore = false;
5297
            $objExercise->feedback_type = 2;
5298
            $objExercise->hideComment = true;
5299
            $objExercise->hideNoAnswer = true;
5300
            $objExercise->results_disabled = 0;
5301
            $objExercise->hideExpectedAnswer = true;
5302
            $show_results = true;
5303
        }
5304
5305
        if ('embeddable' !== $origin &&
5306
            !empty($exercise_stat_info['exe_user_id']) &&
5307
            !empty($studentInfo)
5308
        ) {
5309
            // Shows exercise header.
5310
            echo $objExercise->showExerciseResultHeader(
5311
                $studentInfo,
5312
                $exercise_stat_info,
5313
                $save_user_result,
5314
                $allowSignature,
5315
                $allowExportPdf
5316
            );
5317
        }
5318
5319
        // Display text when test is finished #4074 and for LP #4227
5320
        // Allows to do a remove_XSS for end text result of exercise with
5321
        // user status COURSEMANAGERLOWSECURITY BT#20194
5322
        if (true === api_get_configuration_value('exercise_result_end_text_html_strict_filtering')) {
5323
            $endOfMessage = Security::remove_XSS($objExercise->getTextWhenFinished(), COURSEMANAGERLOWSECURITY);
5324
        } else {
5325
            $endOfMessage = Security::remove_XSS($objExercise->getTextWhenFinished());
5326
        }
5327
        if (!empty($endOfMessage)) {
5328
            echo Display::div(
5329
                $endOfMessage,
5330
                ['id' => 'quiz_end_message']
5331
            );
5332
        }
5333
5334
        $question_list_answers = [];
5335
        $category_list = [];
5336
        $loadChoiceFromSession = false;
5337
        $fromDatabase = true;
5338
        $exerciseResult = null;
5339
        $exerciseResultCoordinates = null;
5340
        $delineationResults = null;
5341
        if (true === $save_user_result && in_array(
5342
            $objExercise->getFeedbackType(),
5343
            [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
5344
        )) {
5345
            $loadChoiceFromSession = true;
5346
            $fromDatabase = false;
5347
            $exerciseResult = Session::read('exerciseResult');
5348
            $exerciseResultCoordinates = Session::read('exerciseResultCoordinates');
5349
            $delineationResults = Session::read('hotspot_delineation_result');
5350
            $delineationResults = isset($delineationResults[$objExercise->iid]) ? $delineationResults[$objExercise->iid] : null;
5351
        }
5352
5353
        $countPendingQuestions = 0;
5354
        $result = [];
5355
        // Loop over all question to show results for each of them, one by one
5356
        if (!empty($question_list)) {
5357
            foreach ($question_list as $questionId) {
5358
                // Creates a temporary Question object
5359
                $objQuestionTmp = Question::read($questionId, $objExercise->course);
5360
                // This variable came from exercise_submit_modal.php
5361
                ob_start();
5362
                $choice = null;
5363
                $delineationChoice = null;
5364
                if ($loadChoiceFromSession) {
5365
                    $choice = isset($exerciseResult[$questionId]) ? $exerciseResult[$questionId] : null;
5366
                    $delineationChoice = isset($delineationResults[$questionId]) ? $delineationResults[$questionId] : null;
5367
                }
5368
5369
                // We're inside *one* question. Go through each possible answer for this question
5370
                $result = $objExercise->manage_answer(
5371
                    $exeId,
5372
                    $questionId,
5373
                    $choice,
5374
                    'exercise_result',
5375
                    $exerciseResultCoordinates,
5376
                    $save_user_result,
5377
                    $fromDatabase,
5378
                    $show_results,
5379
                    $objExercise->selectPropagateNeg(),
5380
                    $delineationChoice,
5381
                    $showTotalScoreAndUserChoicesInLastAttempt
5382
                );
5383
5384
                if (empty($result)) {
5385
                    continue;
5386
                }
5387
5388
                $total_score += (float) $result['score'];
5389
                $total_weight += (float) $result['weight'];
5390
5391
                $question_list_answers[] = [
5392
                    'question' => $result['open_question'],
5393
                    'answer' => $result['open_answer'],
5394
                    'answer_type' => $result['answer_type'],
5395
                    'generated_oral_file' => $result['generated_oral_file'],
5396
                ];
5397
5398
                $my_total_score = $result['score'];
5399
                $my_total_weight = $result['weight'];
5400
                $scorePassed = self::scorePassed($my_total_score, $my_total_weight);
5401
5402
                // Category report
5403
                $category_was_added_for_this_test = false;
5404
                if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
5405
                    if (!isset($category_list[$objQuestionTmp->category]['score'])) {
5406
                        $category_list[$objQuestionTmp->category]['score'] = 0;
5407
                    }
5408
                    if (!isset($category_list[$objQuestionTmp->category]['total'])) {
5409
                        $category_list[$objQuestionTmp->category]['total'] = 0;
5410
                    }
5411
                    if (!isset($category_list[$objQuestionTmp->category]['total_questions'])) {
5412
                        $category_list[$objQuestionTmp->category]['total_questions'] = 0;
5413
                    }
5414
                    if (!isset($category_list[$objQuestionTmp->category]['passed'])) {
5415
                        $category_list[$objQuestionTmp->category]['passed'] = 0;
5416
                    }
5417
                    if (!isset($category_list[$objQuestionTmp->category]['wrong'])) {
5418
                        $category_list[$objQuestionTmp->category]['wrong'] = 0;
5419
                    }
5420
                    if (!isset($category_list[$objQuestionTmp->category]['no_answer'])) {
5421
                        $category_list[$objQuestionTmp->category]['no_answer'] = 0;
5422
                    }
5423
5424
                    $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
5425
                    $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
5426
                    if ($scorePassed) {
5427
                        // Only count passed if score is not empty
5428
                        if (!empty($my_total_score)) {
5429
                            $category_list[$objQuestionTmp->category]['passed']++;
5430
                        }
5431
                    } else {
5432
                        if ($result['user_answered']) {
5433
                            $category_list[$objQuestionTmp->category]['wrong']++;
5434
                        } else {
5435
                            $category_list[$objQuestionTmp->category]['no_answer']++;
5436
                        }
5437
                    }
5438
5439
                    $category_list[$objQuestionTmp->category]['total_questions']++;
5440
                    $category_was_added_for_this_test = true;
5441
                }
5442
                if (isset($objQuestionTmp->category_list) && !empty($objQuestionTmp->category_list)) {
5443
                    foreach ($objQuestionTmp->category_list as $category_id) {
5444
                        $category_list[$category_id]['score'] += $my_total_score;
5445
                        $category_list[$category_id]['total'] += $my_total_weight;
5446
                        $category_was_added_for_this_test = true;
5447
                    }
5448
                }
5449
5450
                // No category for this question!
5451
                if ($category_was_added_for_this_test == false) {
5452
                    if (!isset($category_list['none']['score'])) {
5453
                        $category_list['none']['score'] = 0;
5454
                    }
5455
                    if (!isset($category_list['none']['total'])) {
5456
                        $category_list['none']['total'] = 0;
5457
                    }
5458
5459
                    $category_list['none']['score'] += $my_total_score;
5460
                    $category_list['none']['total'] += $my_total_weight;
5461
                }
5462
5463
                if ($objExercise->selectPropagateNeg() == 0 && $my_total_score < 0) {
5464
                    $my_total_score = 0;
5465
                }
5466
5467
                $comnt = null;
5468
                if ($show_results) {
5469
                    $comnt = Event::get_comments($exeId, $questionId);
5470
                    $teacherAudio = self::getOralFeedbackAudio(
5471
                        $exeId,
5472
                        $questionId,
5473
                        api_get_user_id()
5474
                    );
5475
5476
                    if (!empty($comnt) || $teacherAudio) {
5477
                        echo '<b>'.get_lang('Feedback').'</b>';
5478
                    }
5479
5480
                    if (!empty($comnt)) {
5481
                        echo self::getFeedbackText($comnt);
5482
                    }
5483
5484
                    if ($teacherAudio) {
5485
                        echo $teacherAudio;
5486
                    }
5487
                }
5488
5489
                $calculatedScore = [
5490
                    'result' => self::show_score(
5491
                        $my_total_score,
5492
                        $my_total_weight,
5493
                        false
5494
                    ),
5495
                    'pass' => $scorePassed,
5496
                    'score' => $my_total_score,
5497
                    'weight' => $my_total_weight,
5498
                    'comments' => $comnt,
5499
                    'user_answered' => $result['user_answered'],
5500
                ];
5501
5502
                $score = [];
5503
                if ($show_results) {
5504
                    $score = $calculatedScore;
5505
                }
5506
                if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
5507
                    $reviewScore = [
5508
                        'score' => $my_total_score,
5509
                        'comments' => Event::get_comments($exeId, $questionId),
5510
                    ];
5511
                    $check = $objQuestionTmp->isQuestionWaitingReview($reviewScore);
5512
                    if (false === $check) {
5513
                        $countPendingQuestions++;
5514
                    }
5515
                }
5516
5517
                $contents = ob_get_clean();
5518
5519
                // Hide correct answers.
5520
                if ($scorePassed && false === $objExercise->disableHideCorrectAnsweredQuestions) {
5521
                    // Skip correct answers.
5522
                    $hide = (int) $objExercise->getPageConfigurationAttribute('hide_correct_answered_questions');
5523
                    if (1 === $hide) {
5524
                        continue;
5525
                    }
5526
                }
5527
5528
                $question_content = '';
5529
                if ($show_results) {
5530
                    $question_content = '<div class="question_row_answer">';
5531
                    if (false === $showQuestionScore) {
5532
                        $score = [];
5533
                    }
5534
5535
                    // Shows question title an description
5536
                    $question_content .= $objQuestionTmp->return_header(
5537
                        $objExercise,
5538
                        $counter,
5539
                        $score
5540
                    );
5541
                }
5542
                $counter++;
5543
                $question_content .= $contents;
5544
                if ($show_results) {
5545
                    $question_content .= '</div>';
5546
                }
5547
5548
                $calculatedScore['question_content'] = $question_content;
5549
                $attemptResult[] = $calculatedScore;
5550
5551
                if ($objExercise->showExpectedChoice()) {
5552
                    $exercise_content .= Display::div(
5553
                        Display::panel($question_content),
5554
                        ['class' => 'question-panel']
5555
                    );
5556
                } else {
5557
                    // $show_all_but_expected_answer should not happen at
5558
                    // the same time as $show_results
5559
                    if ($show_results && !$show_only_score) {
5560
                        $exercise_content .= Display::div(
5561
                            Display::panel($question_content),
5562
                            ['class' => 'question-panel']
5563
                        );
5564
                    }
5565
                }
5566
            }
5567
        }
5568
5569
        $totalScoreText = null;
5570
        $certificateBlock = '';
5571
        if (($show_results || $show_only_score) && $showTotalScore) {
5572
            if ($result['answer_type'] == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
5573
                echo '<h1 style="text-align : center; margin : 20px 0;">'.get_lang('YourResults').'</h1><br />';
5574
            }
5575
            $totalScoreText .= '<div class="question_row_score">';
5576
            if ($result['answer_type'] == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
5577
                $totalScoreText .= self::getQuestionDiagnosisRibbon(
5578
                    $objExercise,
5579
                    $total_score,
5580
                    $total_weight,
5581
                    true
5582
                );
5583
            } else {
5584
                $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
5585
                if ('true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)) {
5586
                    $formula = $pluginEvaluation->getFormulaForExercise($objExercise->selectId());
5587
5588
                    if (!empty($formula)) {
5589
                        $total_score = $pluginEvaluation->getResultWithFormula($exeId, $formula);
5590
                        $total_weight = $pluginEvaluation->getMaxScore();
5591
                    }
5592
                }
5593
5594
                $totalScoreText .= self::getTotalScoreRibbon(
5595
                    $objExercise,
5596
                    $total_score,
5597
                    $total_weight,
5598
                    true,
5599
                    $countPendingQuestions
5600
                );
5601
            }
5602
            $totalScoreText .= '</div>';
5603
5604
            if (!empty($studentInfo)) {
5605
                $certificateBlock = self::generateAndShowCertificateBlock(
5606
                    $total_score,
5607
                    $total_weight,
5608
                    $objExercise,
5609
                    $studentInfo['id'],
5610
                    $courseCode,
5611
                    $sessionId
5612
                );
5613
            }
5614
        }
5615
5616
        if ($result['answer_type'] == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
5617
            $chartMultiAnswer = MultipleAnswerTrueFalseDegreeCertainty::displayStudentsChartResults(
5618
                $exeId,
5619
                $objExercise
5620
            );
5621
            echo $chartMultiAnswer;
5622
        }
5623
5624
        if (!empty($category_list) &&
5625
            ($show_results || $show_only_score || RESULT_DISABLE_RADAR == $objExercise->results_disabled)
5626
        ) {
5627
            // Adding total
5628
            $category_list['total'] = [
5629
                'score' => $total_score,
5630
                'total' => $total_weight,
5631
            ];
5632
            echo TestCategory::get_stats_table_by_attempt($objExercise, $category_list);
5633
        }
5634
5635
        if ($show_all_but_expected_answer) {
5636
            $exercise_content .= Display::return_message(get_lang('ExerciseWithFeedbackWithoutCorrectionComment'));
5637
        }
5638
5639
        // Remove audio auto play from questions on results page - refs BT#7939
5640
        $exercise_content = preg_replace(
5641
            ['/autoplay[\=\".+\"]+/', '/autostart[\=\".+\"]+/'],
5642
            '',
5643
            $exercise_content
5644
        );
5645
5646
        echo $totalScoreText;
5647
        echo $certificateBlock;
5648
5649
        // Ofaj change BT#11784
5650
        if (api_get_configuration_value('quiz_show_description_on_results_page') &&
5651
            !empty($objExercise->description)
5652
        ) {
5653
            echo Display::div(Security::remove_XSS($objExercise->description), ['class' => 'exercise_description']);
5654
        }
5655
5656
        echo $exercise_content;
5657
        if (!$show_only_score) {
5658
            echo $totalScoreText;
5659
        }
5660
5661
        if ($save_user_result) {
5662
            // Tracking of results
5663
            if ($exercise_stat_info) {
5664
                $learnpath_id = $exercise_stat_info['orig_lp_id'];
5665
                $learnpath_item_id = $exercise_stat_info['orig_lp_item_id'];
5666
                $learnpath_item_view_id = $exercise_stat_info['orig_lp_item_view_id'];
5667
5668
                if (api_is_allowed_to_session_edit()) {
5669
                    Event::updateEventExercise(
5670
                        $exercise_stat_info['exe_id'],
5671
                        $objExercise->selectId(),
5672
                        $total_score,
5673
                        $total_weight,
5674
                        $sessionId,
5675
                        $learnpath_id,
5676
                        $learnpath_item_id,
5677
                        $learnpath_item_view_id,
5678
                        $exercise_stat_info['exe_duration'],
5679
                        $question_list
5680
                    );
5681
5682
                    $allowStats = api_get_configuration_value('allow_gradebook_stats');
5683
                    if ($allowStats) {
5684
                        $objExercise->generateStats(
5685
                            $objExercise->selectId(),
5686
                            api_get_course_info(),
5687
                            $sessionId
5688
                        );
5689
                    }
5690
                }
5691
            }
5692
5693
            // Send notification at the end
5694
            if (!api_is_allowed_to_edit(null, true) &&
5695
                !api_is_excluded_user_type()
5696
            ) {
5697
                $objExercise->send_mail_notification_for_exam(
5698
                    'end',
5699
                    $question_list_answers,
5700
                    $origin,
5701
                    $exeId,
5702
                    $total_score,
5703
                    $total_weight
5704
                );
5705
            }
5706
        }
5707
5708
        if (in_array(
5709
            $objExercise->selectResultsDisabled(),
5710
            [RESULT_DISABLE_RANKING, RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING]
5711
        )) {
5712
            echo Display::page_header(get_lang('Ranking'), null, 'h4');
5713
            echo self::displayResultsInRanking(
5714
                $objExercise,
5715
                api_get_user_id(),
5716
                $courseId,
5717
                $sessionId
5718
            );
5719
        }
5720
5721
        if (!empty($remainingMessage)) {
5722
            echo Display::return_message($remainingMessage, 'normal', false);
5723
        }
5724
5725
        $failedAnswersCount = 0;
5726
        $wrongQuestionHtml = '';
5727
        $all = '';
5728
        foreach ($attemptResult as $item) {
5729
            if (false === $item['pass']) {
5730
                $failedAnswersCount++;
5731
                $wrongQuestionHtml .= $item['question_content'].'<br />';
5732
            }
5733
            $all .= $item['question_content'].'<br />';
5734
        }
5735
5736
        $passed = self::isPassPercentageAttemptPassed(
5737
            $objExercise,
5738
            $total_score,
5739
            $total_weight
5740
        );
5741
5742
        $percentage = 0;
5743
        if (!empty($total_weight)) {
5744
            $percentage = ($total_score / $total_weight) * 100;
5745
        }
5746
5747
        return [
5748
            'category_list' => $category_list,
5749
            'attempts_result_list' => $attemptResult, // array of results
5750
            'exercise_passed' => $passed, // boolean
5751
            'total_answers_count' => count($attemptResult), // int
5752
            'failed_answers_count' => $failedAnswersCount, // int
5753
            'failed_answers_html' => $wrongQuestionHtml,
5754
            'all_answers_html' => $all,
5755
            'total_score' => $total_score,
5756
            'total_weight' => $total_weight,
5757
            'total_percentage' => $percentage,
5758
            'count_pending_questions' => $countPendingQuestions,
5759
        ];
5760
    }
5761
5762
    /**
5763
     * It validates unique score when all user answers are correct by question.
5764
     * It is used for global questions.
5765
     *
5766
     * @param       $answerType
5767
     * @param       $listCorrectAnswers
5768
     * @param       $exeId
5769
     * @param       $questionId
5770
     * @param       $questionWeighting
5771
     * @param array $choice
5772
     * @param int   $nbrAnswers
5773
     *
5774
     * @return int|mixed
5775
     */
5776
    public static function getUserQuestionScoreGlobal(
5777
        $answerType,
5778
        $listCorrectAnswers,
5779
        $exeId,
5780
        $questionId,
5781
        $questionWeighting,
5782
        $choice = [],
5783
        $nbrAnswers = 0
5784
    ) {
5785
        $nbrCorrect = 0;
5786
        $nbrOptions = 0;
5787
        switch ($answerType) {
5788
            case FILL_IN_BLANKS_COMBINATION:
5789
                if (!empty($listCorrectAnswers)) {
5790
                    foreach ($listCorrectAnswers['student_score'] as $idx => $val) {
5791
                        if (1 === (int) $val) {
5792
                            $nbrCorrect++;
5793
                        }
5794
                    }
5795
                    $nbrOptions = (int) $listCorrectAnswers['words_count'];
5796
                }
5797
                break;
5798
            case HOT_SPOT_COMBINATION:
5799
                if (!empty($listCorrectAnswers)) {
5800
                    foreach ($listCorrectAnswers as $idx => $val) {
5801
                        if (1 === (int) $choice[$idx]) {
5802
                            $nbrCorrect++;
5803
                        }
5804
                    }
5805
                } else {
5806
                    // We get the user answers from database
5807
                    $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
5808
                    $sql = "SELECT count(hotspot_id) as ct
5809
                                FROM $TBL_TRACK_HOTSPOT
5810
                                WHERE
5811
                                    hotspot_exe_id = '".Database::escape_string($exeId)."' AND
5812
                                    hotspot_question_id = '".Database::escape_string($questionId)."' AND
5813
                                    hotspot_correct = 1";
5814
                    $result = Database::query($sql);
5815
                    $nbrCorrect = (int) Database::result($result, 0, 0);
5816
                }
5817
                $nbrOptions = $nbrAnswers;
5818
                break;
5819
            case MATCHING_COMBINATION:
5820
            case MATCHING_DRAGGABLE_COMBINATION:
5821
                if (isset($listCorrectAnswers['form_values'])) {
5822
                    if (isset($listCorrectAnswers['form_values']['correct'])) {
5823
                        $nbrCorrect = count($listCorrectAnswers['form_values']['correct']);
5824
                        $nbrOptions = (int) $listCorrectAnswers['form_values']['count_options'];
5825
                    }
5826
                } else {
5827
                    if (isset($listCorrectAnswers['from_database'])) {
5828
                        if (isset($listCorrectAnswers['from_database']['correct'])) {
5829
                            $nbrCorrect = count($listCorrectAnswers['from_database']['correct']);
5830
                            $nbrOptions = (int) $listCorrectAnswers['from_database']['count_options'];
5831
                        }
5832
                    }
5833
                }
5834
                break;
5835
        }
5836
5837
        $questionScore = 0;
5838
        if ($nbrCorrect > 0 && $nbrCorrect == $nbrOptions) {
5839
            $questionScore = $questionWeighting;
5840
        }
5841
5842
        return $questionScore;
5843
    }
5844
5845
    /**
5846
     * Display the ranking of results in a exercise.
5847
     *
5848
     * @param Exercise $exercise
5849
     * @param int      $currentUserId
5850
     * @param int      $courseId
5851
     * @param int      $sessionId
5852
     *
5853
     * @return string
5854
     */
5855
    public static function displayResultsInRanking($exercise, $currentUserId, $courseId, $sessionId = 0)
5856
    {
5857
        $exerciseId = $exercise->iid;
5858
        $data = self::exerciseResultsInRanking($exerciseId, $courseId, $sessionId);
5859
5860
        $table = new HTML_Table(['class' => 'table table-hover table-striped table-bordered']);
5861
        $table->setHeaderContents(0, 0, get_lang('Position'), ['class' => 'text-right']);
5862
        $table->setHeaderContents(0, 1, get_lang('Username'));
5863
        $table->setHeaderContents(0, 2, get_lang('Score'), ['class' => 'text-right']);
5864
        $table->setHeaderContents(0, 3, get_lang('Date'), ['class' => 'text-center']);
5865
5866
        foreach ($data as $r => $item) {
5867
            if (!isset($item[1])) {
5868
                continue;
5869
            }
5870
            $selected = $item[1]->getId() == $currentUserId;
5871
5872
            foreach ($item as $c => $value) {
5873
                $table->setCellContents($r + 1, $c, $value);
5874
5875
                $attrClass = '';
5876
5877
                if (in_array($c, [0, 2])) {
5878
                    $attrClass = 'text-right';
5879
                } elseif (3 == $c) {
5880
                    $attrClass = 'text-center';
5881
                }
5882
5883
                if ($selected) {
5884
                    $attrClass .= ' warning';
5885
                }
5886
5887
                $table->setCellAttributes($r + 1, $c, ['class' => $attrClass]);
5888
            }
5889
        }
5890
5891
        return $table->toHtml();
5892
    }
5893
5894
    /**
5895
     * Get the ranking for results in a exercise.
5896
     * Function used internally by ExerciseLib::displayResultsInRanking.
5897
     *
5898
     * @param int $exerciseId
5899
     * @param int $courseId
5900
     * @param int $sessionId
5901
     *
5902
     * @return array
5903
     */
5904
    public static function exerciseResultsInRanking($exerciseId, $courseId, $sessionId = 0)
5905
    {
5906
        $em = Database::getManager();
5907
5908
        $dql = 'SELECT DISTINCT te.exeUserId FROM ChamiloCoreBundle:TrackEExercises te WHERE te.exeExoId = :id AND te.cId = :cId';
5909
        $dql .= api_get_session_condition($sessionId, true, false, 'te.sessionId');
5910
5911
        $result = $em
5912
            ->createQuery($dql)
5913
            ->setParameters(['id' => $exerciseId, 'cId' => $courseId])
5914
            ->getScalarResult();
5915
5916
        $data = [];
5917
        /** @var TrackEExercises $item */
5918
        foreach ($result as $item) {
5919
            $data[] = self::get_best_attempt_by_user($item['exeUserId'], $exerciseId, $courseId, $sessionId);
5920
        }
5921
5922
        usort(
5923
            $data,
5924
            function ($a, $b) {
5925
                if ($a['exe_result'] != $b['exe_result']) {
5926
                    return $a['exe_result'] > $b['exe_result'] ? -1 : 1;
5927
                }
5928
5929
                if ($a['exe_date'] != $b['exe_date']) {
5930
                    return $a['exe_date'] < $b['exe_date'] ? -1 : 1;
5931
                }
5932
5933
                return 0;
5934
            }
5935
        );
5936
5937
        // flags to display the same position in case of tie
5938
        $lastScore = $data[0]['exe_result'];
5939
        $position = 1;
5940
        $data = array_map(
5941
            function ($item) use (&$lastScore, &$position) {
5942
                if ($item['exe_result'] < $lastScore) {
5943
                    $position++;
5944
                }
5945
5946
                $lastScore = $item['exe_result'];
5947
5948
                return [
5949
                    $position,
5950
                    api_get_user_entity($item['exe_user_id']),
5951
                    self::show_score($item['exe_result'], $item['exe_weighting'], true, true, true),
5952
                    api_convert_and_format_date($item['exe_date'], DATE_TIME_FORMAT_SHORT),
5953
                ];
5954
            },
5955
            $data
5956
        );
5957
5958
        return $data;
5959
    }
5960
5961
    /**
5962
     * Get a special ribbon on top of "degree of certainty" questions (
5963
     * variation from getTotalScoreRibbon() for other question types).
5964
     *
5965
     * @param Exercise $objExercise
5966
     * @param float    $score
5967
     * @param float    $weight
5968
     * @param bool     $checkPassPercentage
5969
     *
5970
     * @return string
5971
     */
5972
    public static function getQuestionDiagnosisRibbon($objExercise, $score, $weight, $checkPassPercentage = false)
5973
    {
5974
        $displayChartDegree = true;
5975
        $ribbon = $displayChartDegree ? '<div class="ribbon">' : '';
5976
5977
        if ($checkPassPercentage) {
5978
            $passPercentage = $objExercise->selectPassPercentage();
5979
            $isSuccess = self::isSuccessExerciseResult($score, $weight, $passPercentage);
5980
            // Color the final test score if pass_percentage activated
5981
            $ribbonTotalSuccessOrError = '';
5982
            if (self::isPassPercentageEnabled($passPercentage)) {
5983
                if ($isSuccess) {
5984
                    $ribbonTotalSuccessOrError = ' ribbon-total-success';
5985
                } else {
5986
                    $ribbonTotalSuccessOrError = ' ribbon-total-error';
5987
                }
5988
            }
5989
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total '.$ribbonTotalSuccessOrError.'">' : '';
5990
        } else {
5991
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total">' : '';
5992
        }
5993
5994
        if ($displayChartDegree) {
5995
            $ribbon .= '<h3>'.get_lang('YourTotalScore').':&nbsp;';
5996
            $ribbon .= self::show_score($score, $weight, false, true);
5997
            $ribbon .= '</h3>';
5998
            $ribbon .= '</div>';
5999
        }
6000
6001
        if ($checkPassPercentage) {
6002
            $ribbon .= self::showSuccessMessage(
6003
                $score,
6004
                $weight,
6005
                $objExercise->selectPassPercentage()
6006
            );
6007
        }
6008
6009
        $ribbon .= $displayChartDegree ? '</div>' : '';
6010
6011
        return $ribbon;
6012
    }
6013
6014
    public static function isPassPercentageAttemptPassed($objExercise, $score, $weight)
6015
    {
6016
        $passPercentage = $objExercise->selectPassPercentage();
6017
6018
        return self::isSuccessExerciseResult($score, $weight, $passPercentage);
6019
    }
6020
6021
    /**
6022
     * @param float $score
6023
     * @param float $weight
6024
     * @param bool  $checkPassPercentage
6025
     * @param int   $countPendingQuestions
6026
     *
6027
     * @return string
6028
     */
6029
    public static function getTotalScoreRibbon(
6030
        Exercise $objExercise,
6031
        $score,
6032
        $weight,
6033
        $checkPassPercentage = false,
6034
        $countPendingQuestions = 0
6035
    ) {
6036
        $hide = (int) $objExercise->getPageConfigurationAttribute('hide_total_score');
6037
        if (1 === $hide) {
6038
            return '';
6039
        }
6040
6041
        $passPercentage = $objExercise->selectPassPercentage();
6042
        $ribbon = '<div class="title-score">';
6043
        if ($checkPassPercentage) {
6044
            $isSuccess = self::isSuccessExerciseResult(
6045
                $score,
6046
                $weight,
6047
                $passPercentage
6048
            );
6049
            // Color the final test score if pass_percentage activated
6050
            $class = '';
6051
            if (self::isPassPercentageEnabled($passPercentage)) {
6052
                if ($isSuccess) {
6053
                    $class = ' ribbon-total-success';
6054
                } else {
6055
                    $class = ' ribbon-total-error';
6056
                }
6057
            }
6058
            $ribbon .= '<div class="total '.$class.'">';
6059
        } else {
6060
            $ribbon .= '<div class="total">';
6061
        }
6062
        $ribbon .= '<h3>'.get_lang('YourTotalScore').':&nbsp;';
6063
        $ribbon .= self::show_score($score, $weight, false, true);
6064
        $ribbon .= '</h3>';
6065
        $ribbon .= '</div>';
6066
        if ($checkPassPercentage) {
6067
            $ribbon .= self::showSuccessMessage(
6068
                $score,
6069
                $weight,
6070
                $passPercentage
6071
            );
6072
        }
6073
        $ribbon .= '</div>';
6074
6075
        if (!empty($countPendingQuestions)) {
6076
            $ribbon .= '<br />';
6077
            $ribbon .= Display::return_message(
6078
                sprintf(
6079
                    get_lang('TempScoreXQuestionsNotCorrectedYet'),
6080
                    $countPendingQuestions
6081
                ),
6082
                'warning'
6083
            );
6084
        }
6085
6086
        return $ribbon;
6087
    }
6088
6089
    /**
6090
     * @param int $countLetter
6091
     *
6092
     * @return mixed
6093
     */
6094
    public static function detectInputAppropriateClass($countLetter)
6095
    {
6096
        $limits = [
6097
            0 => 'input-mini',
6098
            10 => 'input-mini',
6099
            15 => 'input-medium',
6100
            20 => 'input-xlarge',
6101
            40 => 'input-xlarge',
6102
            60 => 'input-xxlarge',
6103
            100 => 'input-xxlarge',
6104
            200 => 'input-xxlarge',
6105
        ];
6106
6107
        foreach ($limits as $size => $item) {
6108
            if ($countLetter <= $size) {
6109
                return $item;
6110
            }
6111
        }
6112
6113
        return $limits[0];
6114
    }
6115
6116
    /**
6117
     * @param int    $senderId
6118
     * @param array  $course_info
6119
     * @param string $test
6120
     * @param string $url
6121
     *
6122
     * @return string
6123
     */
6124
    public static function getEmailNotification($senderId, $course_info, $test, $url)
6125
    {
6126
        $teacher_info = api_get_user_info($senderId);
6127
        $from_name = api_get_person_name(
6128
            $teacher_info['firstname'],
6129
            $teacher_info['lastname'],
6130
            null,
6131
            PERSON_NAME_EMAIL_ADDRESS
6132
        );
6133
6134
        $view = new Template('', false, false, false, false, false, false);
6135
        $view->assign('course_title', Security::remove_XSS($course_info['name']));
6136
        $view->assign('test_title', Security::remove_XSS($test));
6137
        $view->assign('url', $url);
6138
        $view->assign('teacher_name', $from_name);
6139
        $template = $view->get_template('mail/exercise_result_alert_body.tpl');
6140
6141
        return $view->fetch($template);
6142
    }
6143
6144
    /**
6145
     * @return string
6146
     */
6147
    public static function getNotCorrectedYetText()
6148
    {
6149
        return Display::return_message(get_lang('notCorrectedYet'), 'warning');
6150
    }
6151
6152
    /**
6153
     * @param string $message
6154
     *
6155
     * @return string
6156
     */
6157
    public static function getFeedbackText($message)
6158
    {
6159
        return Display::return_message($message, 'warning', false);
6160
    }
6161
6162
    /**
6163
     * Get the recorder audio component for save a teacher audio feedback.
6164
     *
6165
     * @param Template $template
6166
     * @param int      $attemptId
6167
     * @param int      $questionId
6168
     * @param int      $userId
6169
     *
6170
     * @return string
6171
     */
6172
    public static function getOralFeedbackForm($template, $attemptId, $questionId, $userId)
6173
    {
6174
        $template->assign('user_id', $userId);
6175
        $template->assign('question_id', $questionId);
6176
        $template->assign('directory', "/../exercises/teacher_audio/$attemptId/");
6177
        $template->assign('file_name', "{$questionId}_{$userId}");
6178
6179
        return $template->fetch($template->get_template('exercise/oral_expression.tpl'));
6180
    }
6181
6182
    /**
6183
     * Get the audio componen for a teacher audio feedback.
6184
     *
6185
     * @param int $attemptId
6186
     * @param int $questionId
6187
     * @param int $userId
6188
     *
6189
     * @return string
6190
     */
6191
    public static function getOralFeedbackAudio($attemptId, $questionId, $userId)
6192
    {
6193
        $courseInfo = api_get_course_info();
6194
        $sessionId = api_get_session_id();
6195
        $groupId = api_get_group_id();
6196
        $sysCourseDir = api_get_path(SYS_COURSE_PATH).$courseInfo['path'];
6197
        $webCourseDir = api_get_path(WEB_COURSE_PATH).$courseInfo['path'];
6198
        $fileName = "{$questionId}_{$userId}".DocumentManager::getDocumentSuffix($courseInfo, $sessionId, $groupId);
6199
        $filePath = null;
6200
6201
        $relFilePath = "/exercises/teacher_audio/$attemptId/$fileName";
6202
6203
        if (file_exists($sysCourseDir.$relFilePath.'.ogg')) {
6204
            $filePath = $webCourseDir.$relFilePath.'.ogg';
6205
        } elseif (file_exists($sysCourseDir.$relFilePath.'.wav.wav')) {
6206
            $filePath = $webCourseDir.$relFilePath.'.wav.wav';
6207
        } elseif (file_exists($sysCourseDir.$relFilePath.'.wav')) {
6208
            $filePath = $webCourseDir.$relFilePath.'.wav';
6209
        }
6210
6211
        if (!$filePath) {
6212
            return '';
6213
        }
6214
6215
        return Display::tag(
6216
            'audio',
6217
            null,
6218
            ['src' => $filePath]
6219
        );
6220
    }
6221
6222
    /**
6223
     * @return array
6224
     */
6225
    public static function getNotificationSettings()
6226
    {
6227
        $emailAlerts = [
6228
            2 => get_lang('SendEmailToTeacherWhenStudentStartQuiz'),
6229
            1 => get_lang('SendEmailToTeacherWhenStudentEndQuiz'), // default
6230
            3 => get_lang('SendEmailToTeacherWhenStudentEndQuizOnlyIfOpenQuestion'),
6231
            4 => get_lang('SendEmailToTeacherWhenStudentEndQuizOnlyIfOralQuestion'),
6232
        ];
6233
6234
        return $emailAlerts;
6235
    }
6236
6237
    /**
6238
     * Get the additional actions added in exercise_additional_teacher_modify_actions configuration.
6239
     *
6240
     * @param int $exerciseId
6241
     * @param int $iconSize
6242
     *
6243
     * @return string
6244
     */
6245
    public static function getAdditionalTeacherActions($exerciseId, $iconSize = ICON_SIZE_SMALL)
6246
    {
6247
        $additionalActions = api_get_configuration_value('exercise_additional_teacher_modify_actions') ?: [];
6248
        $actions = [];
6249
6250
        foreach ($additionalActions as $additionalAction) {
6251
            $actions[] = call_user_func(
6252
                $additionalAction,
6253
                $exerciseId,
6254
                $iconSize
6255
            );
6256
        }
6257
6258
        return implode(PHP_EOL, $actions);
6259
    }
6260
6261
    /**
6262
     * @param int $userId
6263
     * @param int $courseId
6264
     * @param int $sessionId
6265
     *
6266
     * @throws \Doctrine\ORM\Query\QueryException
6267
     *
6268
     * @return int
6269
     */
6270
    public static function countAnsweredQuestionsByUserAfterTime(DateTime $time, $userId, $courseId, $sessionId)
6271
    {
6272
        $em = Database::getManager();
6273
6274
        $time = api_get_utc_datetime($time->format('Y-m-d H:i:s'), false, true);
6275
6276
        $result = $em
6277
            ->createQuery('
6278
                SELECT COUNT(ea) FROM ChamiloCoreBundle:TrackEAttempt ea
6279
                WHERE ea.userId = :user AND ea.cId = :course AND ea.sessionId = :session
6280
                    AND ea.tms > :time
6281
            ')
6282
            ->setParameters(['user' => $userId, 'course' => $courseId, 'session' => $sessionId, 'time' => $time])
6283
            ->getSingleScalarResult();
6284
6285
        return $result;
6286
    }
6287
6288
    /**
6289
     * @param int $userId
6290
     * @param int $numberOfQuestions
6291
     * @param int $courseId
6292
     * @param int $sessionId
6293
     *
6294
     * @throws \Doctrine\ORM\Query\QueryException
6295
     *
6296
     * @return bool
6297
     */
6298
    public static function isQuestionsLimitPerDayReached($userId, $numberOfQuestions, $courseId, $sessionId)
6299
    {
6300
        $questionsLimitPerDay = (int) api_get_course_setting('quiz_question_limit_per_day');
6301
6302
        if ($questionsLimitPerDay <= 0) {
6303
            return false;
6304
        }
6305
6306
        $midnightTime = ChamiloApi::getServerMidnightTime();
6307
6308
        $answeredQuestionsCount = self::countAnsweredQuestionsByUserAfterTime(
6309
            $midnightTime,
6310
            $userId,
6311
            $courseId,
6312
            $sessionId
6313
        );
6314
6315
        return ($answeredQuestionsCount + $numberOfQuestions) > $questionsLimitPerDay;
6316
    }
6317
6318
    /**
6319
     * By default, allowed types are unique-answer (and image) or multiple-answer questions.
6320
     * Types can be extended by the configuration setting "exercise_embeddable_extra_types".
6321
     */
6322
    public static function getEmbeddableTypes(): array
6323
    {
6324
        $allowedTypes = [
6325
            UNIQUE_ANSWER,
6326
            MULTIPLE_ANSWER,
6327
            FILL_IN_BLANKS,
6328
            MATCHING,
6329
            FREE_ANSWER,
6330
            MULTIPLE_ANSWER_COMBINATION,
6331
            UNIQUE_ANSWER_NO_OPTION,
6332
            MULTIPLE_ANSWER_TRUE_FALSE,
6333
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE,
6334
            ORAL_EXPRESSION,
6335
            GLOBAL_MULTIPLE_ANSWER,
6336
            CALCULATED_ANSWER,
6337
            UNIQUE_ANSWER_IMAGE,
6338
            READING_COMPREHENSION,
6339
            MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY,
6340
            UPLOAD_ANSWER,
6341
            MATCHING_COMBINATION,
6342
            FILL_IN_BLANKS_COMBINATION,
6343
            MULTIPLE_ANSWER_DROPDOWN,
6344
            MULTIPLE_ANSWER_DROPDOWN_COMBINATION,
6345
        ];
6346
        $defaultTypes = [UNIQUE_ANSWER, MULTIPLE_ANSWER, UNIQUE_ANSWER_IMAGE];
6347
        $types = $defaultTypes;
6348
6349
        $extraTypes = api_get_configuration_value('exercise_embeddable_extra_types');
6350
6351
        if (false !== $extraTypes && !empty($extraTypes['types'])) {
6352
            $types = array_merge($defaultTypes, $extraTypes['types']);
6353
        }
6354
6355
        return array_filter(
6356
            array_unique($types),
6357
            function ($type) use ($allowedTypes) {
6358
                return in_array($type, $allowedTypes);
6359
            }
6360
        );
6361
    }
6362
6363
    /**
6364
     * Check if an exercise complies with the requirements to be embedded in the mobile app or a video.
6365
     * By making sure it is set on one question per page, and that the exam does not have immediate feedback,
6366
     * and it only contains allowed types.
6367
     *
6368
     * @see Exercise::getEmbeddableTypes()
6369
     */
6370
    public static function isQuizEmbeddable(array $exercise): bool
6371
    {
6372
        $exercise['iid'] = isset($exercise['iid']) ? (int) $exercise['iid'] : 0;
6373
6374
        if (ONE_PER_PAGE != $exercise['type'] ||
6375
            in_array($exercise['feedback_type'], [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])
6376
        ) {
6377
            return false;
6378
        }
6379
6380
        $questionRepository = Database::getManager()->getRepository(CQuizQuestion::class);
6381
6382
        $countAll = $questionRepository->countQuestionsInExercise($exercise['iid']);
6383
        $countAllowed = $questionRepository->countEmbeddableQuestionsInExercise($exercise['iid']);
6384
6385
        return $countAll === $countAllowed;
6386
    }
6387
6388
    /**
6389
     * Generate a certificate linked to current quiz and.
6390
     * Return the HTML block with links to download and view the certificate.
6391
     *
6392
     * @param float  $totalScore
6393
     * @param float  $totalWeight
6394
     * @param int    $studentId
6395
     * @param string $courseCode
6396
     * @param int    $sessionId
6397
     *
6398
     * @return string
6399
     */
6400
    public static function generateAndShowCertificateBlock(
6401
        $totalScore,
6402
        $totalWeight,
6403
        Exercise $objExercise,
6404
        $studentId,
6405
        $courseCode,
6406
        $sessionId = 0
6407
    ) {
6408
        if (!api_get_configuration_value('quiz_generate_certificate_ending') ||
6409
            !self::isSuccessExerciseResult($totalScore, $totalWeight, $objExercise->selectPassPercentage())
6410
        ) {
6411
            return '';
6412
        }
6413
6414
        /** @var Category $category */
6415
        $category = Category::load(null, null, $courseCode, null, null, $sessionId, 'ORDER By id');
6416
6417
        if (empty($category)) {
6418
            return '';
6419
        }
6420
6421
        /** @var Category $category */
6422
        $category = $category[0];
6423
        $categoryId = $category->get_id();
6424
        $link = LinkFactory::load(
6425
            null,
6426
            null,
6427
            $objExercise->selectId(),
6428
            null,
6429
            $courseCode,
6430
            $categoryId
6431
        );
6432
6433
        if (empty($link)) {
6434
            return '';
6435
        }
6436
6437
        $resourceDeletedMessage = $category->show_message_resource_delete($courseCode);
6438
6439
        if (false !== $resourceDeletedMessage || api_is_allowed_to_edit() || api_is_excluded_user_type()) {
6440
            return '';
6441
        }
6442
6443
        $certificate = Category::generateUserCertificate($categoryId, $studentId);
6444
6445
        if (!is_array($certificate)) {
6446
            return '';
6447
        }
6448
6449
        return Category::getDownloadCertificateBlock($certificate);
6450
    }
6451
6452
    /**
6453
     * @param int $exerciseId
6454
     */
6455
    public static function getExerciseTitleById($exerciseId)
6456
    {
6457
        $em = Database::getManager();
6458
6459
        return $em
6460
            ->createQuery('SELECT cq.title
6461
                FROM ChamiloCourseBundle:CQuiz cq
6462
                WHERE cq.iid = :iid'
6463
            )
6464
            ->setParameter('iid', $exerciseId)
6465
            ->getSingleScalarResult();
6466
    }
6467
6468
    /**
6469
     * @param int $exeId      ID from track_e_exercises
6470
     * @param int $userId     User ID
6471
     * @param int $exerciseId Exercise ID
6472
     * @param int $courseId   Optional. Coure ID.
6473
     *
6474
     * @return TrackEExercises|null
6475
     */
6476
    public static function recalculateResult($exeId, $userId, $exerciseId, $courseId = 0)
6477
    {
6478
        if (empty($userId) || empty($exerciseId)) {
6479
            return null;
6480
        }
6481
6482
        $em = Database::getManager();
6483
        /** @var TrackEExercises $trackedExercise */
6484
        $trackedExercise = $em->getRepository('ChamiloCoreBundle:TrackEExercises')->find($exeId);
6485
6486
        if (empty($trackedExercise)) {
6487
            return null;
6488
        }
6489
6490
        if ($trackedExercise->getExeUserId() != $userId ||
6491
            $trackedExercise->getExeExoId() != $exerciseId
6492
        ) {
6493
            return null;
6494
        }
6495
6496
        $questionList = $trackedExercise->getDataTracking();
6497
6498
        if (empty($questionList)) {
6499
            return null;
6500
        }
6501
6502
        $questionList = explode(',', $questionList);
6503
6504
        $exercise = new Exercise($courseId);
6505
        $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : [];
6506
6507
        if ($exercise->read($exerciseId) === false) {
6508
            return null;
6509
        }
6510
6511
        $totalScore = 0;
6512
        $totalWeight = 0;
6513
6514
        $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
6515
6516
        $formula = 'true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)
6517
            ? $pluginEvaluation->getFormulaForExercise($exerciseId)
6518
            : 0;
6519
6520
        if (empty($formula)) {
6521
            foreach ($questionList as $questionId) {
6522
                $question = Question::read($questionId, $courseInfo);
6523
6524
                if (false === $question) {
6525
                    continue;
6526
                }
6527
6528
                $totalWeight += $question->selectWeighting();
6529
6530
                // We're inside *one* question. Go through each possible answer for this question
6531
                $result = $exercise->manage_answer(
6532
                    $exeId,
6533
                    $questionId,
6534
                    [],
6535
                    'exercise_result',
6536
                    [],
6537
                    false,
6538
                    true,
6539
                    false,
6540
                    $exercise->selectPropagateNeg(),
6541
                    [],
6542
                    [],
6543
                    true
6544
                );
6545
6546
                //  Adding the new score.
6547
                $totalScore += $result['score'];
6548
            }
6549
        } else {
6550
            $totalScore = $pluginEvaluation->getResultWithFormula($exeId, $formula);
6551
            $totalWeight = $pluginEvaluation->getMaxScore();
6552
        }
6553
6554
        $trackedExercise
6555
            ->setExeResult($totalScore)
6556
            ->setExeWeighting($totalWeight);
6557
6558
        $em->persist($trackedExercise);
6559
        $em->flush();
6560
6561
        return $trackedExercise;
6562
    }
6563
6564
    public static function getTotalQuestionAnswered($courseId, $exerciseId, $questionId, $sessionId = 0, $groups = [], $users = [])
6565
    {
6566
        $courseId = (int) $courseId;
6567
        $exerciseId = (int) $exerciseId;
6568
        $questionId = (int) $questionId;
6569
        $sessionId = (int) $sessionId;
6570
6571
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
6572
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6573
6574
        $userCondition = '';
6575
        $allUsers = [];
6576
        if (!empty($groups)) {
6577
            foreach ($groups as $groupId) {
6578
                $groupUsers = GroupManager::get_users($groupId, null, null, null, false, $courseId);
6579
                if (!empty($groupUsers)) {
6580
                    $allUsers = array_merge($allUsers, $groupUsers);
6581
                }
6582
            }
6583
        }
6584
6585
        if (!empty($users)) {
6586
            $allUsers = array_merge($allUsers, $users);
6587
        }
6588
6589
        if (!empty($allUsers)) {
6590
            $allUsers = array_map('intval', $allUsers);
6591
            $usersToString = implode("', '", $allUsers);
6592
            $userCondition = " AND user_id IN ('$usersToString') ";
6593
        }
6594
6595
        $sessionCondition = '';
6596
        if (!empty($sessionId)) {
6597
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
6598
        }
6599
6600
        $sql = "SELECT count(te.exe_id) total
6601
                FROM $attemptTable t
6602
                INNER JOIN $trackTable te
6603
                ON (te.c_id = t.c_id AND t.exe_id = te.exe_id)
6604
                WHERE
6605
                    t.c_id = $courseId AND
6606
                    exe_exo_id = $exerciseId AND
6607
                    t.question_id = $questionId AND
6608
                    status != 'incomplete'
6609
                    $sessionCondition
6610
                    $userCondition
6611
        ";
6612
        $queryTotal = Database::query($sql);
6613
        $totalRow = Database::fetch_array($queryTotal, 'ASSOC');
6614
        $total = 0;
6615
        if ($totalRow) {
6616
            $total = (int) $totalRow['total'];
6617
        }
6618
6619
        return $total;
6620
    }
6621
6622
    public static function getWrongQuestionResults($courseId, $exerciseId, $sessionId = 0, $groups = [], $users = [])
6623
    {
6624
        $courseId = (int) $courseId;
6625
        $exerciseId = (int) $exerciseId;
6626
6627
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
6628
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
6629
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6630
6631
        $sessionCondition = '';
6632
        if (!empty($sessionId)) {
6633
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
6634
        }
6635
6636
        $userCondition = '';
6637
        $allUsers = [];
6638
        if (!empty($groups)) {
6639
            foreach ($groups as $groupId) {
6640
                $groupUsers = GroupManager::get_users($groupId, null, null, null, false, $courseId);
6641
                if (!empty($groupUsers)) {
6642
                    $allUsers = array_merge($allUsers, $groupUsers);
6643
                }
6644
            }
6645
        }
6646
6647
        if (!empty($users)) {
6648
            $allUsers = array_merge($allUsers, $users);
6649
        }
6650
6651
        if (!empty($allUsers)) {
6652
            $allUsers = array_map('intval', $allUsers);
6653
            $usersToString = implode("', '", $allUsers);
6654
            $userCondition .= " AND user_id IN ('$usersToString') ";
6655
        }
6656
6657
        $sql = "SELECT q.question, question_id, count(q.iid) count
6658
                FROM $attemptTable t
6659
                INNER JOIN $questionTable q
6660
                ON q.iid = t.question_id
6661
                INNER JOIN $trackTable te
6662
                ON t.exe_id = te.exe_id
6663
                WHERE
6664
                    t.c_id = $courseId AND
6665
                    t.marks != q.ponderation AND
6666
                    exe_exo_id = $exerciseId AND
6667
                    status != 'incomplete'
6668
                    $sessionCondition
6669
                    $userCondition
6670
                GROUP BY q.iid
6671
                ORDER BY count DESC
6672
        ";
6673
6674
        $result = Database::query($sql);
6675
6676
        return Database::store_result($result, 'ASSOC');
6677
    }
6678
6679
    public static function getExerciseResultsCount($type, $courseId, Exercise $exercise, $sessionId = 0)
6680
    {
6681
        $courseId = (int) $courseId;
6682
        $exerciseId = (int) $exercise->iid;
6683
6684
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6685
6686
        $sessionCondition = '';
6687
        if (!empty($sessionId)) {
6688
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
6689
        }
6690
6691
        $passPercentage = $exercise->selectPassPercentage();
6692
        $minPercentage = 100;
6693
        if (!empty($passPercentage)) {
6694
            $minPercentage = $passPercentage;
6695
        }
6696
6697
        $selectCount = 'count(DISTINCT te.exe_id)';
6698
        $scoreCondition = '';
6699
        switch ($type) {
6700
            case 'correct_student':
6701
                $selectCount = 'count(DISTINCT te.exe_user_id)';
6702
                $scoreCondition = " AND (exe_result/exe_weighting*100) >= $minPercentage ";
6703
                break;
6704
            case 'wrong_student':
6705
                $selectCount = 'count(DISTINCT te.exe_user_id)';
6706
                $scoreCondition = " AND (exe_result/exe_weighting*100) < $minPercentage ";
6707
                break;
6708
            case 'correct':
6709
                $scoreCondition = " AND (exe_result/exe_weighting*100) >= $minPercentage ";
6710
                break;
6711
            case 'wrong':
6712
                $scoreCondition = " AND (exe_result/exe_weighting*100) < $minPercentage ";
6713
                break;
6714
        }
6715
6716
        $sql = "SELECT $selectCount count
6717
                FROM $trackTable te
6718
                WHERE
6719
                    c_id = $courseId AND
6720
                    exe_exo_id = $exerciseId AND
6721
                    status != 'incomplete'
6722
                    $scoreCondition
6723
                    $sessionCondition
6724
        ";
6725
        $result = Database::query($sql);
6726
        $totalRow = Database::fetch_array($result, 'ASSOC');
6727
        $total = 0;
6728
        if ($totalRow) {
6729
            $total = (int) $totalRow['count'];
6730
        }
6731
6732
        return $total;
6733
    }
6734
6735
    public static function parseContent($content, $stats, Exercise $exercise, $trackInfo, $currentUserId = 0)
6736
    {
6737
        $wrongAnswersCount = $stats['failed_answers_count'];
6738
        $attemptDate = substr($trackInfo['exe_date'], 0, 10);
6739
        $exeId = $trackInfo['exe_id'];
6740
        $resultsStudentUrl = api_get_path(WEB_CODE_PATH).
6741
            'exercise/result.php?id='.$exeId.'&'.api_get_cidreq();
6742
        $resultsTeacherUrl = api_get_path(WEB_CODE_PATH).
6743
            'exercise/exercise_show.php?action=edit&id='.$exeId.'&'.api_get_cidreq(true, true, 'teacher');
6744
6745
        $content = str_replace(
6746
            [
6747
                '((exercise_error_count))',
6748
                '((all_answers_html))',
6749
                '((all_answers_teacher_html))',
6750
                '((exercise_title))',
6751
                '((exercise_attempt_date))',
6752
                '((link_to_test_result_page_student))',
6753
                '((link_to_test_result_page_teacher))',
6754
            ],
6755
            [
6756
                $wrongAnswersCount,
6757
                $stats['all_answers_html'],
6758
                $stats['all_answers_teacher_html'],
6759
                $exercise->get_formated_title(),
6760
                $attemptDate,
6761
                $resultsStudentUrl,
6762
                $resultsTeacherUrl,
6763
            ],
6764
            $content
6765
        );
6766
6767
        $currentUserId = empty($currentUserId) ? api_get_user_id() : (int) $currentUserId;
6768
6769
        $content = AnnouncementManager::parseContent(
6770
            $currentUserId,
6771
            $content,
6772
            api_get_course_id(),
6773
            api_get_session_id()
6774
        );
6775
6776
        return $content;
6777
    }
6778
6779
    public static function sendNotification(
6780
        $currentUserId,
6781
        $objExercise,
6782
        $exercise_stat_info,
6783
        $courseInfo,
6784
        $attemptCountToSend,
6785
        $stats,
6786
        $statsTeacher
6787
    ) {
6788
        $notifications = api_get_configuration_value('exercise_finished_notification_settings');
6789
        if (empty($notifications)) {
6790
            return false;
6791
        }
6792
6793
        $studentId = $exercise_stat_info['exe_user_id'];
6794
        $exerciseExtraFieldValue = new ExtraFieldValue('exercise');
6795
        $wrongAnswersCount = $stats['failed_answers_count'];
6796
        $exercisePassed = $stats['exercise_passed'];
6797
        $countPendingQuestions = $stats['count_pending_questions'];
6798
        $stats['all_answers_teacher_html'] = $statsTeacher['all_answers_html'];
6799
6800
        // If there are no pending questions (Open questions).
6801
        if (0 === $countPendingQuestions) {
6802
            /*$extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6803
                $objExercise->iid,
6804
                'signature_mandatory'
6805
            );
6806
6807
            if ($extraFieldData && isset($extraFieldData['value']) && 1 === (int) $extraFieldData['value']) {
6808
                if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($objExercise)) {
6809
                    $signature = ExerciseSignaturePlugin::getSignature($studentId, $exercise_stat_info);
6810
                    if (false !== $signature) {
6811
                        //return false;
6812
                    }
6813
                }
6814
            }*/
6815
6816
            // Notifications.
6817
            $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6818
                $objExercise->iid,
6819
                'notifications'
6820
            );
6821
            $exerciseNotification = '';
6822
            if ($extraFieldData && isset($extraFieldData['value'])) {
6823
                $exerciseNotification = $extraFieldData['value'];
6824
            }
6825
6826
            $subject = sprintf(get_lang('WrongAttemptXInCourseX'), $attemptCountToSend, $courseInfo['title']);
6827
            if ($exercisePassed) {
6828
                $subject = sprintf(get_lang('ExerciseValidationInCourseX'), $courseInfo['title']);
6829
            }
6830
6831
            if ($exercisePassed) {
6832
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6833
                    $objExercise->iid,
6834
                    'MailSuccess'
6835
                );
6836
            } else {
6837
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6838
                    $objExercise->iid,
6839
                    'MailAttempt'.$attemptCountToSend
6840
                );
6841
            }
6842
6843
            // Blocking exercise.
6844
            $blockPercentageExtra = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6845
                $objExercise->iid,
6846
                'blocking_percentage'
6847
            );
6848
            $blockPercentage = false;
6849
            if ($blockPercentageExtra && isset($blockPercentageExtra['value']) && $blockPercentageExtra['value']) {
6850
                $blockPercentage = $blockPercentageExtra['value'];
6851
            }
6852
            if ($blockPercentage) {
6853
                $passBlock = $stats['total_percentage'] > $blockPercentage;
6854
                if (false === $passBlock) {
6855
                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6856
                        $objExercise->iid,
6857
                        'MailIsBlockByPercentage'
6858
                    );
6859
                }
6860
            }
6861
6862
            $extraFieldValueUser = new ExtraFieldValue('user');
6863
6864
            if ($extraFieldData && isset($extraFieldData['value'])) {
6865
                $content = $extraFieldData['value'];
6866
                $content = self::parseContent($content, $stats, $objExercise, $exercise_stat_info, $studentId);
6867
                //if (false === $exercisePassed) {
6868
                if (0 !== $wrongAnswersCount) {
6869
                    $content .= $stats['failed_answers_html'];
6870
                }
6871
6872
                $sendMessage = true;
6873
                if (!empty($exerciseNotification)) {
6874
                    foreach ($notifications as $name => $notificationList) {
6875
                        if ($exerciseNotification !== $name) {
6876
                            continue;
6877
                        }
6878
                        foreach ($notificationList as $notificationName => $attemptData) {
6879
                            if ('student_check' === $notificationName) {
6880
                                $sendMsgIfInList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : '';
6881
                                if (!empty($sendMsgIfInList)) {
6882
                                    foreach ($sendMsgIfInList as $skipVariable => $skipValues) {
6883
                                        $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
6884
                                            $studentId,
6885
                                            $skipVariable
6886
                                        );
6887
6888
                                        if (empty($userExtraFieldValue)) {
6889
                                            $sendMessage = false;
6890
                                            break;
6891
                                        } else {
6892
                                            $sendMessage = false;
6893
                                            if (isset($userExtraFieldValue['value']) &&
6894
                                                in_array($userExtraFieldValue['value'], $skipValues)
6895
                                            ) {
6896
                                                $sendMessage = true;
6897
                                                break;
6898
                                            }
6899
                                        }
6900
                                    }
6901
                                }
6902
                                break;
6903
                            }
6904
                        }
6905
                    }
6906
                }
6907
6908
                // Send to student.
6909
                if ($sendMessage) {
6910
                    MessageManager::send_message($currentUserId, $subject, $content);
6911
                }
6912
            }
6913
6914
            if (!empty($exerciseNotification)) {
6915
                foreach ($notifications as $name => $notificationList) {
6916
                    if ($exerciseNotification !== $name) {
6917
                        continue;
6918
                    }
6919
                    foreach ($notificationList as $attemptData) {
6920
                        $skipNotification = false;
6921
                        $skipNotificationList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : [];
6922
                        if (!empty($skipNotificationList)) {
6923
                            foreach ($skipNotificationList as $skipVariable => $skipValues) {
6924
                                $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
6925
                                    $studentId,
6926
                                    $skipVariable
6927
                                );
6928
6929
                                if (empty($userExtraFieldValue)) {
6930
                                    $skipNotification = true;
6931
                                    break;
6932
                                } else {
6933
                                    if (isset($userExtraFieldValue['value'])) {
6934
                                        if (!in_array($userExtraFieldValue['value'], $skipValues)) {
6935
                                            $skipNotification = true;
6936
                                            break;
6937
                                        }
6938
                                    } else {
6939
                                        $skipNotification = true;
6940
                                        break;
6941
                                    }
6942
                                }
6943
                            }
6944
                        }
6945
6946
                        if ($skipNotification) {
6947
                            continue;
6948
                        }
6949
6950
                        $email = isset($attemptData['email']) ? $attemptData['email'] : '';
6951
                        $emailList = explode(',', $email);
6952
                        if (empty($emailList)) {
6953
                            continue;
6954
                        }
6955
                        $attempts = isset($attemptData['attempts']) ? $attemptData['attempts'] : [];
6956
                        foreach ($attempts as $attempt) {
6957
                            $sendMessage = false;
6958
                            if (isset($attempt['attempt']) && $attemptCountToSend !== (int) $attempt['attempt']) {
6959
                                continue;
6960
                            }
6961
6962
                            if (!isset($attempt['status'])) {
6963
                                continue;
6964
                            }
6965
6966
                            if ($blockPercentage && isset($attempt['is_block_by_percentage'])) {
6967
                                if ($attempt['is_block_by_percentage']) {
6968
                                    if ($passBlock) {
6969
                                        continue;
6970
                                    }
6971
                                } else {
6972
                                    if (false === $passBlock) {
6973
                                        continue;
6974
                                    }
6975
                                }
6976
                            }
6977
6978
                            switch ($attempt['status']) {
6979
                                case 'passed':
6980
                                    if ($exercisePassed) {
6981
                                        $sendMessage = true;
6982
                                    }
6983
                                    break;
6984
                                case 'failed':
6985
                                    if (false === $exercisePassed) {
6986
                                        $sendMessage = true;
6987
                                    }
6988
                                    break;
6989
                                case 'all':
6990
                                    $sendMessage = true;
6991
                                    break;
6992
                            }
6993
6994
                            if ($sendMessage) {
6995
                                $attachments = [];
6996
                                if (isset($attempt['add_pdf']) && $attempt['add_pdf']) {
6997
                                    // Get pdf content
6998
                                    $pdfExtraData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6999
                                        $objExercise->iid,
7000
                                        $attempt['add_pdf']
7001
                                    );
7002
7003
                                    if ($pdfExtraData && isset($pdfExtraData['value'])) {
7004
                                        $pdfContent = self::parseContent(
7005
                                            $pdfExtraData['value'],
7006
                                            $stats,
7007
                                            $objExercise,
7008
                                            $exercise_stat_info,
7009
                                            $studentId
7010
                                        );
7011
7012
                                        @$pdf = new PDF();
7013
                                        $filename = get_lang('Exercise');
7014
                                        $cssFile = api_get_path(SYS_CSS_PATH).'themes/chamilo/default.css';
7015
                                        $pdfPath = @$pdf->content_to_pdf(
7016
                                            "<html><body>$pdfContent</body></html>",
7017
                                            file_get_contents($cssFile),
7018
                                            $filename,
7019
                                            api_get_course_id(),
7020
                                            'F',
7021
                                            false,
7022
                                            null,
7023
                                            false,
7024
                                            true
7025
                                        );
7026
                                        $attachments[] = ['filename' => $filename, 'path' => $pdfPath];
7027
                                    }
7028
                                }
7029
7030
                                $content = isset($attempt['content_default']) ? $attempt['content_default'] : '';
7031
                                if (isset($attempt['content'])) {
7032
                                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
7033
                                        $objExercise->iid,
7034
                                        $attempt['content']
7035
                                    );
7036
                                    if ($extraFieldData && isset($extraFieldData['value']) && !empty($extraFieldData['value'])) {
7037
                                        $content = $extraFieldData['value'];
7038
                                    }
7039
                                }
7040
7041
                                if (!empty($content)) {
7042
                                    $content = self::parseContent(
7043
                                        $content,
7044
                                        $stats,
7045
                                        $objExercise,
7046
                                        $exercise_stat_info,
7047
                                        $studentId
7048
                                    );
7049
                                    foreach ($emailList as $email) {
7050
                                        if (empty($email)) {
7051
                                            continue;
7052
                                        }
7053
                                        api_mail_html(
7054
                                            null,
7055
                                            $email,
7056
                                            $subject,
7057
                                            $content,
7058
                                            null,
7059
                                            null,
7060
                                            [],
7061
                                            $attachments
7062
                                        );
7063
                                    }
7064
                                }
7065
7066
                                if (isset($attempt['post_actions'])) {
7067
                                    foreach ($attempt['post_actions'] as $action => $params) {
7068
                                        switch ($action) {
7069
                                            case 'subscribe_student_to_courses':
7070
                                                foreach ($params as $code) {
7071
                                                    CourseManager::subscribeUser($currentUserId, $code);
7072
                                                    break;
7073
                                                }
7074
                                                break;
7075
                                        }
7076
                                    }
7077
                                }
7078
                            }
7079
                        }
7080
                    }
7081
                }
7082
            }
7083
        }
7084
    }
7085
7086
    /**
7087
     * Delete an exercise attempt.
7088
     *
7089
     * Log the exe_id deleted with the exe_user_id related.
7090
     *
7091
     * @param int $exeId
7092
     */
7093
    public static function deleteExerciseAttempt($exeId)
7094
    {
7095
        $exeId = (int) $exeId;
7096
7097
        $trackExerciseInfo = self::get_exercise_track_exercise_info($exeId);
7098
7099
        if (empty($trackExerciseInfo)) {
7100
            return;
7101
        }
7102
7103
        $tblTrackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7104
        $tblTrackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
7105
7106
        Database::query("DELETE FROM $tblTrackExercises WHERE exe_id = $exeId");
7107
        Database::query("DELETE FROM $tblTrackAttempt WHERE exe_id = $exeId");
7108
7109
        Event::addEvent(
7110
            LOG_EXERCISE_ATTEMPT_DELETE,
7111
            LOG_EXERCISE_ATTEMPT,
7112
            $exeId,
7113
            api_get_utc_datetime()
7114
        );
7115
        Event::addEvent(
7116
            LOG_EXERCISE_ATTEMPT_DELETE,
7117
            LOG_EXERCISE_AND_USER_ID,
7118
            $exeId.'-'.$trackExerciseInfo['exe_user_id'],
7119
            api_get_utc_datetime()
7120
        );
7121
    }
7122
7123
    public static function scorePassed($score, $total)
7124
    {
7125
        $compareResult = bccomp($score, $total, 3);
7126
        $scorePassed = 1 === $compareResult || 0 === $compareResult;
7127
        if (false === $scorePassed) {
7128
            $epsilon = 0.00001;
7129
            if (abs($score - $total) < $epsilon) {
7130
                $scorePassed = true;
7131
            }
7132
        }
7133
7134
        return $scorePassed;
7135
    }
7136
7137
    public static function logPingForCheckingConnection()
7138
    {
7139
        $action = $_REQUEST['a'] ?? '';
7140
7141
        if ('ping' !== $action) {
7142
            return;
7143
        }
7144
7145
        if (!empty(api_get_user_id())) {
7146
            return;
7147
        }
7148
7149
        $exeId = $_REQUEST['exe_id'] ?? 0;
7150
7151
        error_log("Exercise ping received: exe_id = $exeId. _user not found in session.");
7152
    }
7153
7154
    public static function saveFileExerciseResultPdf(
7155
        int $exeId,
7156
        int $courseId,
7157
        int $sessionId
7158
    ) {
7159
        $cinfo = api_get_course_info_by_id($courseId);
7160
        $courseCode = $cinfo['code'];
7161
        $cidReq = 'cidReq='.$courseCode.'&id_session='.$sessionId.'&gidReq=0&gradebook=0';
7162
7163
        $url = api_get_path(WEB_PATH).'main/exercise/exercise_show.php?'.$cidReq.'&id='.$exeId.'&action=export&export_type=all_results';
7164
        $ch = curl_init();
7165
        curl_setopt($ch, CURLOPT_URL, $url);
7166
        curl_setopt($ch, CURLOPT_COOKIE, session_id());
7167
        curl_setopt($ch, CURLOPT_AUTOREFERER, true);
7168
        curl_setopt($ch, CURLOPT_COOKIESESSION, true);
7169
        curl_setopt($ch, CURLOPT_FAILONERROR, false);
7170
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
7171
        curl_setopt($ch, CURLOPT_HEADER, true);
7172
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
7173
        $result = curl_exec($ch);
7174
7175
        curl_close($ch);
7176
    }
7177
7178
    public static function exportExerciseAllResultsZip(
7179
        int $sessionId,
7180
        int $courseId,
7181
        int $exerciseId
7182
    ) {
7183
        $objExerciseTmp = new Exercise($courseId);
7184
        $exeResults = $objExerciseTmp->getExerciseAndResult(
7185
            $courseId,
7186
            $sessionId,
7187
            $exerciseId,
7188
            ''
7189
        );
7190
7191
        if (!empty($exeResults)) {
7192
            $exportName = 'S'.$sessionId.'-C'.$courseId.'-T'.$exerciseId;
7193
            $baseDir = api_get_path(SYS_ARCHIVE_PATH);
7194
            $folderName = 'pdfexport-'.$exportName;
7195
            $exportFolderPath = $baseDir.$folderName;
7196
7197
            // 1. Cleans the export folder if it exists.
7198
            if (is_dir($exportFolderPath)) {
7199
                rmdirr($exportFolderPath);
7200
            }
7201
7202
            // 2. Create the pdfs inside a new export folder path.
7203
            if (!empty($exeResults)) {
7204
                foreach ($exeResults as $exeResult) {
7205
                    $exeId = (int) $exeResult['exe_id'];
7206
                    ExerciseLib::saveFileExerciseResultPdf($exeId, $courseId, $sessionId);
7207
                }
7208
            }
7209
7210
            // 3. If export folder is not empty will be zipped.
7211
            $isFolderPathEmpty = (file_exists($exportFolderPath) && 2 == count(scandir($exportFolderPath)));
7212
            if (!$isFolderPathEmpty) {
7213
                $exportFilePath = $baseDir.$exportName.'.zip';
7214
                $zip = new \PclZip($exportFilePath);
7215
                $zip->create($exportFolderPath, PCLZIP_OPT_REMOVE_PATH, $exportFolderPath);
7216
                rmdirr($exportFolderPath);
7217
7218
                DocumentManager::file_send_for_download($exportFilePath, true, $exportName.'.zip');
7219
                exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
7220
            }
7221
        }
7222
7223
        return false;
7224
    }
7225
}
7226