ExerciseLib::getOralFeedbackForm()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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