ExerciseLib::exercise_time_control_is_valid()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 36
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 23
nc 4
nop 3
dl 0
loc 36
rs 9.552
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
6
use Chamilo\CoreBundle\Entity\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 (api_is_session_admin()) {
3254
                                    $delete_link = '';
3255
                                }
3256
                                if ($revised == 3) {
3257
                                    $delete_link = null;
3258
                                }
3259
                                $actions .= $delete_link;
3260
                            }
3261
                        } else {
3262
                            $attempt_url = api_get_path(WEB_CODE_PATH).'exercise/result.php?'.$cidReq.'&id='.$attempt['exe_id'];
3263
                            $attempt_link = Display::url(
3264
                                get_lang('Show'),
3265
                                $attempt_url,
3266
                                [
3267
                                    'class' => 'ajax btn btn-default',
3268
                                    'data-title' => get_lang('Show'),
3269
                                ]
3270
                            );
3271
                            $actions .= $attempt_link;
3272
                        }
3273
                        $actions .= '</div>';
3274
3275
                        if (!empty($userExtraFieldsToAdd)) {
3276
                            foreach ($userExtraFieldsToAdd as $variable) {
3277
                                $extraFieldValue = new ExtraFieldValue('user');
3278
                                $values = $extraFieldValue->get_values_by_handler_and_field_variable(
3279
                                    $attempt['user_id'],
3280
                                    $variable
3281
                                );
3282
                                if (isset($values['value'])) {
3283
                                    $attempt[$variable] = $values['value'];
3284
                                }
3285
                            }
3286
                        }
3287
3288
                        $exeId = $attempt['exe_id'];
3289
                        $attempt['id'] = $exeId;
3290
                        $category_list = [];
3291
                        if ($is_allowedToEdit) {
3292
                            $sessionName = '';
3293
                            $sessionStartAccessDate = '';
3294
                            if (!empty($attemptSessionId)) {
3295
                                $sessionInfo = api_get_session_info($attemptSessionId);
3296
                                if (!empty($sessionInfo)) {
3297
                                    $sessionName = $sessionInfo['name'];
3298
                                    $sessionStartAccessDate = api_get_local_time($sessionInfo['access_start_date']);
3299
                                }
3300
                            }
3301
3302
                            $courseId = $courseItemInfo['real_id'];
3303
3304
                            if ($searchAllTeacherCourses) {
3305
                                $attempt['course'] = $courseItemInfo['title'];
3306
                                $attempt['exercise'] = $attempt['title'];
3307
                            }
3308
3309
                            $objExercise = new Exercise($courseId);
3310
                            if ($showExerciseCategories) {
3311
                                // Getting attempt info
3312
                                $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
3313
                                if (!empty($exercise_stat_info['data_tracking'])) {
3314
                                    $question_list = explode(',', $exercise_stat_info['data_tracking']);
3315
                                    if (!empty($question_list)) {
3316
                                        foreach ($question_list as $questionId) {
3317
                                            $objQuestionTmp = Question::read($questionId, $objExercise->course);
3318
                                            // We're inside *one* question.
3319
                                            // Go through each possible answer for this question.
3320
                                            $result = $objExercise->manage_answer(
3321
                                                $exeId,
3322
                                                $questionId,
3323
                                                null,
3324
                                                'exercise_result',
3325
                                                false,
3326
                                                false,
3327
                                                true,
3328
                                                false,
3329
                                                $objExercise->selectPropagateNeg(),
3330
                                                null,
3331
                                                true
3332
                                            );
3333
3334
                                            $my_total_score = $result['score'];
3335
                                            $my_total_weight = $result['weight'];
3336
3337
                                            // Category report
3338
                                            $category_was_added_for_this_test = false;
3339
                                            if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
3340
                                                if (!isset($category_list[$objQuestionTmp->category]['score'])) {
3341
                                                    $category_list[$objQuestionTmp->category]['score'] = 0;
3342
                                                }
3343
                                                if (!isset($category_list[$objQuestionTmp->category]['total'])) {
3344
                                                    $category_list[$objQuestionTmp->category]['total'] = 0;
3345
                                                }
3346
                                                $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
3347
                                                $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
3348
                                                $category_was_added_for_this_test = true;
3349
                                            }
3350
3351
                                            if (isset($objQuestionTmp->category_list) &&
3352
                                                !empty($objQuestionTmp->category_list)
3353
                                            ) {
3354
                                                foreach ($objQuestionTmp->category_list as $category_id) {
3355
                                                    $category_list[$category_id]['score'] += $my_total_score;
3356
                                                    $category_list[$category_id]['total'] += $my_total_weight;
3357
                                                    $category_was_added_for_this_test = true;
3358
                                                }
3359
                                            }
3360
3361
                                            // No category for this question!
3362
                                            if ($category_was_added_for_this_test == false) {
3363
                                                if (!isset($category_list['none']['score'])) {
3364
                                                    $category_list['none']['score'] = 0;
3365
                                                }
3366
                                                if (!isset($category_list['none']['total'])) {
3367
                                                    $category_list['none']['total'] = 0;
3368
                                                }
3369
3370
                                                $category_list['none']['score'] += $my_total_score;
3371
                                                $category_list['none']['total'] += $my_total_weight;
3372
                                            }
3373
                                        }
3374
                                    }
3375
                                }
3376
                            }
3377
3378
                            foreach ($category_list as $categoryId => $result) {
3379
                                $scoreToDisplay = self::show_score(
3380
                                    $result['score'],
3381
                                    $result['total'],
3382
                                    true,
3383
                                    true,
3384
                                    false,
3385
                                    false,
3386
                                    $decimalSeparator,
3387
                                    $thousandSeparator,
3388
                                    $roundValues
3389
                                );
3390
                                $attempt['category_'.$categoryId] = $scoreToDisplay;
3391
                                $attempt['category_'.$categoryId.'_score_percentage'] = self::show_score(
3392
                                    $result['score'],
3393
                                    $result['total'],
3394
                                    true,
3395
                                    true,
3396
                                    true,
3397
                                    true,
3398
                                    $decimalSeparator,
3399
                                    $thousandSeparator,
3400
                                    $roundValues
3401
                                );
3402
                                $attempt['category_'.$categoryId.'_only_score'] = $result['score'];
3403
                                $attempt['category_'.$categoryId.'_total'] = $result['total'];
3404
                            }
3405
                            $attempt['session'] = $sessionName;
3406
                            $attempt['session_access_start_date'] = $sessionStartAccessDate;
3407
                            $attempt['status'] = $revisedLabel;
3408
                            $attempt['score'] = $score;
3409
                            $attempt['qualificator_fullname'] = '';
3410
                            $attempt['date_of_qualification'] = '';
3411
                            if (!empty($attempt['corrector'])) {
3412
                                $qualificatorAuthor = api_get_user_info($attempt['corrector']);
3413
                                $attempt['qualificator_fullname'] = api_get_person_name($qualificatorAuthor['firstname'], $qualificatorAuthor['lastname']);
3414
                            }
3415
                            if (!empty($attempt['correction_date'])) {
3416
                                $attempt['date_of_qualification'] = api_convert_and_format_date($attempt['correction_date'], DATE_TIME_FORMAT_SHORT);
3417
                            }
3418
                            $attempt['score_percentage'] = self::show_score(
3419
                                $my_res,
3420
                                $my_total,
3421
                                true,
3422
                                true,
3423
                                true,
3424
                                true,
3425
                                $decimalSeparator,
3426
                                $thousandSeparator,
3427
                                $roundValues
3428
                            );
3429
3430
                            if ($roundValues) {
3431
                                $whole = floor($my_res); // 1
3432
                                $fraction = $my_res - $whole; // .25
3433
                                if ($fraction >= 0.5) {
3434
                                    $onlyScore = ceil($my_res);
3435
                                } else {
3436
                                    $onlyScore = round($my_res);
3437
                                }
3438
                            } else {
3439
                                $onlyScore = $scoreDisplay->format_score(
3440
                                    $my_res,
3441
                                    false,
3442
                                    $decimalSeparator,
3443
                                    $thousandSeparator
3444
                                );
3445
                            }
3446
3447
                            $attempt['only_score'] = $onlyScore;
3448
3449
                            if ($roundValues) {
3450
                                $whole = floor($my_total); // 1
3451
                                $fraction = $my_total - $whole; // .25
3452
                                if ($fraction >= 0.5) {
3453
                                    $onlyTotal = ceil($my_total);
3454
                                } else {
3455
                                    $onlyTotal = round($my_total);
3456
                                }
3457
                            } else {
3458
                                $onlyTotal = $scoreDisplay->format_score(
3459
                                    $my_total,
3460
                                    false,
3461
                                    $decimalSeparator,
3462
                                    $thousandSeparator
3463
                                );
3464
                            }
3465
                            $attempt['total'] = $onlyTotal;
3466
                            $attempt['lp'] = $lp_name;
3467
                            $attempt['actions'] = $actions;
3468
                            if ($hideIp && isset($attempt['user_ip'])) {
3469
                                unset($attempt['user_ip']);
3470
                            }
3471
                            $listInfo[] = $attempt;
3472
                        } else {
3473
                            $attempt['status'] = $revisedLabel;
3474
                            $attempt['score'] = $score;
3475
                            $attempt['actions'] = $actions;
3476
                            if ($hideIp && isset($attempt['user_ip'])) {
3477
                                unset($attempt['user_ip']);
3478
                            }
3479
                            $listInfo[] = $attempt;
3480
                        }
3481
                    }
3482
                }
3483
            }
3484
        } else {
3485
            $hpresults = [];
3486
            $res = Database::query($hpsql);
3487
            if ($res !== false) {
3488
                $i = 0;
3489
                while ($resA = Database::fetch_array($res, 'NUM')) {
3490
                    for ($j = 0; $j < 6; $j++) {
3491
                        $hpresults[$i][$j] = $resA[$j];
3492
                    }
3493
                    $i++;
3494
                }
3495
            }
3496
3497
            // Print HotPotatoes test results.
3498
            if (is_array($hpresults)) {
3499
                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...
3500
                    $hp_title = GetQuizName($hpresults[$i][3], $documentPath);
3501
                    if ($hp_title == '') {
3502
                        $hp_title = basename($hpresults[$i][3]);
3503
                    }
3504
3505
                    $hp_date = api_get_local_time(
3506
                        $hpresults[$i][6],
3507
                        null,
3508
                        date_default_timezone_get()
3509
                    );
3510
                    $hp_result = round(($hpresults[$i][4] / ($hpresults[$i][5] != 0 ? $hpresults[$i][5] : 1)) * 100, 2);
3511
                    $hp_result .= '% ('.$hpresults[$i][4].' / '.$hpresults[$i][5].')';
3512
3513
                    if ($is_allowedToEdit) {
3514
                        $listInfo[] = [
3515
                            $hpresults[$i][0],
3516
                            $hpresults[$i][1],
3517
                            $hpresults[$i][2],
3518
                            '',
3519
                            $hp_title,
3520
                            '-',
3521
                            $hp_date,
3522
                            $hp_result,
3523
                            '-',
3524
                        ];
3525
                    } else {
3526
                        $listInfo[] = [
3527
                            $hp_title,
3528
                            '-',
3529
                            $hp_date,
3530
                            $hp_result,
3531
                            '-',
3532
                        ];
3533
                    }
3534
                }
3535
            }
3536
        }
3537
3538
        return $listInfo;
3539
    }
3540
3541
    /**
3542
     * @param $score
3543
     * @param $weight
3544
     *
3545
     * @return array
3546
     */
3547
    public static function convertScoreToPlatformSetting($score, $weight)
3548
    {
3549
        $maxNote = api_get_setting('exercise_max_score');
3550
        $minNote = api_get_setting('exercise_min_score');
3551
3552
        if ($maxNote != '' && $minNote != '') {
3553
            if (!empty($weight) && (float) $weight !== (float) 0) {
3554
                $score = $minNote + ($maxNote - $minNote) * $score / $weight;
3555
            } else {
3556
                $score = $minNote;
3557
            }
3558
            $weight = $maxNote;
3559
        }
3560
3561
        return ['score' => $score, 'weight' => $weight];
3562
    }
3563
3564
    /**
3565
     * Converts the score with the exercise_max_note and exercise_min_score
3566
     * the platform settings + formats the results using the float_format function.
3567
     *
3568
     * @param float  $score
3569
     * @param float  $weight
3570
     * @param bool   $show_percentage       show percentage or not
3571
     * @param bool   $use_platform_settings use or not the platform settings
3572
     * @param bool   $show_only_percentage
3573
     * @param bool   $hidePercentageSign    hide "%" sign
3574
     * @param string $decimalSeparator
3575
     * @param string $thousandSeparator
3576
     * @param bool   $roundValues           This option rounds the float values into a int using ceil()
3577
     * @param bool   $removeEmptyDecimals
3578
     *
3579
     * @return string an html with the score modified
3580
     */
3581
    public static function show_score(
3582
        $score,
3583
        $weight,
3584
        $show_percentage = true,
3585
        $use_platform_settings = true,
3586
        $show_only_percentage = false,
3587
        $hidePercentageSign = false,
3588
        $decimalSeparator = '.',
3589
        $thousandSeparator = ',',
3590
        $roundValues = false,
3591
        $removeEmptyDecimals = false
3592
    ) {
3593
        if (is_null($score) && is_null($weight)) {
3594
            return '-';
3595
        }
3596
3597
        $decimalSeparator = empty($decimalSeparator) ? '.' : $decimalSeparator;
3598
        $thousandSeparator = empty($thousandSeparator) ? ',' : $thousandSeparator;
3599
3600
        if ($use_platform_settings) {
3601
            $result = self::convertScoreToPlatformSetting($score, $weight);
3602
            $score = $result['score'];
3603
            $weight = $result['weight'];
3604
        }
3605
3606
        $percentage = (100 * $score) / ($weight != 0 ? $weight : 1);
3607
3608
        // Formats values
3609
        $percentage = float_format($percentage, 1);
3610
        $score = float_format($score, 1);
3611
        $weight = float_format($weight, 1);
3612
3613
        if ($roundValues) {
3614
            $whole = floor($percentage); // 1
3615
            $fraction = $percentage - $whole; // .25
3616
3617
            // Formats values
3618
            if ($fraction >= 0.5) {
3619
                $percentage = ceil($percentage);
3620
            } else {
3621
                $percentage = round($percentage);
3622
            }
3623
3624
            $whole = floor($score); // 1
3625
            $fraction = $score - $whole; // .25
3626
            if ($fraction >= 0.5) {
3627
                $score = ceil($score);
3628
            } else {
3629
                $score = round($score);
3630
            }
3631
3632
            $whole = floor($weight); // 1
3633
            $fraction = $weight - $whole; // .25
3634
            if ($fraction >= 0.5) {
3635
                $weight = ceil($weight);
3636
            } else {
3637
                $weight = round($weight);
3638
            }
3639
        } else {
3640
            // Formats values
3641
            $percentage = float_format($percentage, 1, $decimalSeparator, $thousandSeparator);
3642
            $score = float_format($score, 1, $decimalSeparator, $thousandSeparator);
3643
            $weight = float_format($weight, 1, $decimalSeparator, $thousandSeparator);
3644
        }
3645
3646
        if ($show_percentage) {
3647
            $percentageSign = ' %';
3648
            if ($hidePercentageSign) {
3649
                $percentageSign = '';
3650
            }
3651
            $html = $percentage."$percentageSign ($score / $weight)";
3652
            if ($show_only_percentage) {
3653
                $html = $percentage.$percentageSign;
3654
            }
3655
        } else {
3656
            if ($removeEmptyDecimals) {
3657
                if (ScoreDisplay::hasEmptyDecimals($weight)) {
3658
                    $weight = round($weight);
3659
                }
3660
            }
3661
            $html = $score.' / '.$weight;
3662
        }
3663
3664
        // Over write score
3665
        $scoreBasedInModel = self::convertScoreToModel($percentage);
3666
        if (!empty($scoreBasedInModel)) {
3667
            $html = $scoreBasedInModel;
3668
        }
3669
3670
        // Ignore other formats and use the configuration['exercise_score_format'] value
3671
        // But also keep the round values settings.
3672
        $format = api_get_configuration_value('exercise_score_format');
3673
        if (!empty($format)) {
3674
            $html = ScoreDisplay::instance()->display_score([$score, $weight], $format);
3675
        }
3676
3677
        return Display::span($html, ['class' => 'score_exercise']);
3678
    }
3679
3680
    /**
3681
     * @param array $model
3682
     * @param float $percentage
3683
     *
3684
     * @return string
3685
     */
3686
    public static function getModelStyle($model, $percentage)
3687
    {
3688
        return '<span class="'.$model['css_class'].'">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>';
3689
    }
3690
3691
    /**
3692
     * @param float $percentage value between 0 and 100
3693
     *
3694
     * @return string
3695
     */
3696
    public static function convertScoreToModel($percentage)
3697
    {
3698
        $model = self::getCourseScoreModel();
3699
        if (!empty($model)) {
3700
            $scoreWithGrade = [];
3701
            foreach ($model['score_list'] as $item) {
3702
                if ($percentage >= $item['min'] && $percentage <= $item['max']) {
3703
                    $scoreWithGrade = $item;
3704
                    break;
3705
                }
3706
            }
3707
3708
            if (!empty($scoreWithGrade)) {
3709
                return self::getModelStyle($scoreWithGrade, $percentage);
3710
            }
3711
        }
3712
3713
        return '';
3714
    }
3715
3716
    /**
3717
     * @return array
3718
     */
3719
    public static function getCourseScoreModel()
3720
    {
3721
        $modelList = self::getScoreModels();
3722
        if (empty($modelList)) {
3723
            return [];
3724
        }
3725
3726
        $courseInfo = api_get_course_info();
3727
        if (!empty($courseInfo)) {
3728
            $scoreModelId = api_get_course_setting('score_model_id');
3729
            if (-1 != $scoreModelId) {
3730
                $modelIdList = array_column($modelList['models'], 'id');
3731
                if (in_array($scoreModelId, $modelIdList)) {
3732
                    foreach ($modelList['models'] as $item) {
3733
                        if ($item['id'] == $scoreModelId) {
3734
                            return $item;
3735
                        }
3736
                    }
3737
                }
3738
            }
3739
        }
3740
3741
        return [];
3742
    }
3743
3744
    /**
3745
     * @return array
3746
     */
3747
    public static function getScoreModels()
3748
    {
3749
        return api_get_configuration_value('score_grade_model');
3750
    }
3751
3752
    /**
3753
     * @param float  $score
3754
     * @param float  $weight
3755
     * @param string $passPercentage
3756
     *
3757
     * @return bool
3758
     */
3759
    public static function isSuccessExerciseResult($score, $weight, $passPercentage)
3760
    {
3761
        $percentage = float_format(
3762
            ($score / (0 != $weight ? $weight : 1)) * 100,
3763
            1
3764
        );
3765
        if (isset($passPercentage) && !empty($passPercentage)) {
3766
            if ($percentage >= $passPercentage) {
3767
                return true;
3768
            }
3769
        }
3770
3771
        return false;
3772
    }
3773
3774
    /**
3775
     * @param string $name
3776
     * @param $weight
3777
     * @param $selected
3778
     *
3779
     * @return bool
3780
     */
3781
    public static function addScoreModelInput(
3782
        FormValidator $form,
3783
        $name,
3784
        $weight,
3785
        $selected
3786
    ) {
3787
        $model = self::getCourseScoreModel();
3788
        if (empty($model)) {
3789
            return false;
3790
        }
3791
3792
        /** @var HTML_QuickForm_select $element */
3793
        $element = $form->createElement(
3794
            'select',
3795
            $name,
3796
            get_lang('Qualification'),
3797
            [],
3798
            ['class' => 'exercise_mark_select']
3799
        );
3800
3801
        foreach ($model['score_list'] as $item) {
3802
            $i = api_number_format($item['score_to_qualify'] / 100 * $weight, 2);
3803
            $label = self::getModelStyle($item, $i);
3804
            $attributes = [
3805
                'class' => $item['css_class'],
3806
            ];
3807
            if ($selected == $i) {
3808
                $attributes['selected'] = 'selected';
3809
            }
3810
            $element->addOption($label, $i, $attributes);
3811
        }
3812
        $form->addElement($element);
3813
    }
3814
3815
    /**
3816
     * @return string
3817
     */
3818
    public static function getJsCode()
3819
    {
3820
        // Filling the scores with the right colors.
3821
        $models = self::getCourseScoreModel();
3822
        $cssListToString = '';
3823
        if (!empty($models)) {
3824
            $cssList = array_column($models['score_list'], 'css_class');
3825
            $cssListToString = implode(' ', $cssList);
3826
        }
3827
3828
        if (empty($cssListToString)) {
3829
            return '';
3830
        }
3831
        $js = <<<EOT
3832
3833
        function updateSelect(element) {
3834
            var spanTag = element.parent().find('span.filter-option');
3835
            var value = element.val();
3836
            var selectId = element.attr('id');
3837
            var optionClass = $('#' + selectId + ' option[value="'+value+'"]').attr('class');
3838
            spanTag.removeClass('$cssListToString');
3839
            spanTag.addClass(optionClass);
3840
        }
3841
3842
        $(function() {
3843
            // Loading values
3844
            $('.exercise_mark_select').on('loaded.bs.select', function() {
3845
                updateSelect($(this));
3846
            });
3847
            // On change
3848
            $('.exercise_mark_select').on('changed.bs.select', function() {
3849
                updateSelect($(this));
3850
            });
3851
        });
3852
EOT;
3853
3854
        return $js;
3855
    }
3856
3857
    /**
3858
     * @param float  $score
3859
     * @param float  $weight
3860
     * @param string $pass_percentage
3861
     *
3862
     * @return string
3863
     */
3864
    public static function showSuccessMessage($score, $weight, $pass_percentage)
3865
    {
3866
        $res = '';
3867
        if (self::isPassPercentageEnabled($pass_percentage)) {
3868
            $isSuccess = self::isSuccessExerciseResult(
3869
                $score,
3870
                $weight,
3871
                $pass_percentage
3872
            );
3873
3874
            if ($isSuccess) {
3875
                $html = get_lang('CongratulationsYouPassedTheTest');
3876
                $icon = Display::return_icon(
3877
                    'completed.png',
3878
                    get_lang('Correct'),
3879
                    [],
3880
                    ICON_SIZE_MEDIUM
3881
                );
3882
            } else {
3883
                $html = get_lang('YouDidNotReachTheMinimumScore');
3884
                $icon = Display::return_icon(
3885
                    'warning.png',
3886
                    get_lang('Wrong'),
3887
                    [],
3888
                    ICON_SIZE_MEDIUM
3889
                );
3890
            }
3891
            $html = Display::tag('h4', $html);
3892
            $html .= Display::tag(
3893
                'h5',
3894
                $icon,
3895
                ['style' => 'width:40px; padding:2px 10px 0px 0px']
3896
            );
3897
            $res = $html;
3898
        }
3899
3900
        return $res;
3901
    }
3902
3903
    /**
3904
     * Return true if pass_pourcentage activated (we use the pass pourcentage feature
3905
     * return false if pass_percentage = 0 (we don't use the pass pourcentage feature.
3906
     *
3907
     * @param $value
3908
     *
3909
     * @return bool
3910
     *              In this version, pass_percentage and show_success_message are disabled if
3911
     *              pass_percentage is set to 0
3912
     */
3913
    public static function isPassPercentageEnabled($value)
3914
    {
3915
        return $value > 0;
3916
    }
3917
3918
    /**
3919
     * Converts a numeric value in a percentage example 0.66666 to 66.67 %.
3920
     *
3921
     * @param $value
3922
     *
3923
     * @return float Converted number
3924
     */
3925
    public static function convert_to_percentage($value)
3926
    {
3927
        $return = '-';
3928
        if ($value != '') {
3929
            $return = float_format($value * 100, 1).' %';
3930
        }
3931
3932
        return $return;
3933
    }
3934
3935
    /**
3936
     * Getting all active exercises from a course from a session
3937
     * (if a session_id is provided we will show all the exercises in the course +
3938
     * all exercises in the session).
3939
     *
3940
     * @param array  $course_info
3941
     * @param int    $session_id
3942
     * @param bool   $check_publication_dates
3943
     * @param string $search                  Search exercise name
3944
     * @param bool   $search_all_sessions     Search exercises in all sessions
3945
     * @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...
3946
     *                  1 = only active exercises,
3947
     *                  2 = all exercises
3948
     *                  3 = active <> -1
3949
     *
3950
     * @return array array with exercise data
3951
     */
3952
    public static function get_all_exercises(
3953
        $course_info = null,
3954
        $session_id = 0,
3955
        $check_publication_dates = false,
3956
        $search = '',
3957
        $search_all_sessions = false,
3958
        $active = 2
3959
    ) {
3960
        $course_id = api_get_course_int_id();
3961
3962
        if (!empty($course_info) && !empty($course_info['real_id'])) {
3963
            $course_id = $course_info['real_id'];
3964
        }
3965
3966
        if ($session_id == -1) {
3967
            $session_id = 0;
3968
        }
3969
3970
        $now = api_get_utc_datetime();
3971
        $timeConditions = '';
3972
        if ($check_publication_dates) {
3973
            // Start and end are set
3974
            $timeConditions = " AND ((start_time <> '' AND start_time < '$now' AND end_time <> '' AND end_time > '$now' )  OR ";
3975
            // only start is set
3976
            $timeConditions .= " (start_time <> '' AND start_time < '$now' AND end_time is NULL) OR ";
3977
            // only end is set
3978
            $timeConditions .= " (start_time IS NULL AND end_time <> '' AND end_time > '$now') OR ";
3979
            // nothing is set
3980
            $timeConditions .= ' (start_time IS NULL AND end_time IS NULL)) ';
3981
        }
3982
3983
        $needle_where = !empty($search) ? " AND title LIKE '?' " : '';
3984
        $needle = !empty($search) ? "%".$search."%" : '';
3985
3986
        // Show courses by active status
3987
        $active_sql = '';
3988
        if ($active == 3) {
3989
            $active_sql = ' active <> -1 AND';
3990
        } else {
3991
            if ($active != 2) {
3992
                $active_sql = sprintf(' active = %d AND', $active);
3993
            }
3994
        }
3995
3996
        if ($search_all_sessions == true) {
3997
            $conditions = [
3998
                'where' => [
3999
                    $active_sql.' c_id = ? '.$needle_where.$timeConditions => [
4000
                        $course_id,
4001
                        $needle,
4002
                    ],
4003
                ],
4004
                'order' => 'title',
4005
            ];
4006
        } else {
4007
            if (empty($session_id)) {
4008
                $conditions = [
4009
                    'where' => [
4010
                        $active_sql.' (session_id = 0 OR session_id IS NULL) AND c_id = ? '.$needle_where.$timeConditions => [
4011
                            $course_id,
4012
                            $needle,
4013
                        ],
4014
                    ],
4015
                    'order' => 'title',
4016
                ];
4017
            } else {
4018
                $conditions = [
4019
                    'where' => [
4020
                        $active_sql.' (session_id = 0 OR session_id IS NULL OR session_id = ? ) AND c_id = ? '.$needle_where.$timeConditions => [
4021
                            $session_id,
4022
                            $course_id,
4023
                            $needle,
4024
                        ],
4025
                    ],
4026
                    'order' => 'title',
4027
                ];
4028
            }
4029
        }
4030
4031
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
4032
4033
        return Database::select('*', $table, $conditions);
4034
    }
4035
4036
    /**
4037
     * Getting all exercises (active only or all)
4038
     * from a course from a session
4039
     * (if a session_id is provided we will show all the exercises in the
4040
     * course + all exercises in the session).
4041
     *
4042
     * @param   array   course data
4043
     * @param   int     session id
4044
     * @param    int        course c_id
4045
     * @param bool $only_active_exercises
4046
     *
4047
     * @return array array with exercise data
4048
     *               modified by Hubert Borderiou
4049
     */
4050
    public static function get_all_exercises_for_course_id(
4051
        $course_info = null,
4052
        $session_id = 0,
4053
        $course_id = 0,
4054
        $only_active_exercises = true
4055
    ) {
4056
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
4057
4058
        if ($only_active_exercises) {
4059
            // Only active exercises.
4060
            $sql_active_exercises = "active = 1 AND ";
4061
        } else {
4062
            // Not only active means visible and invisible NOT deleted (-2)
4063
            $sql_active_exercises = "active IN (1, 0) AND ";
4064
        }
4065
4066
        if ($session_id == -1) {
4067
            $session_id = 0;
4068
        }
4069
4070
        $params = [
4071
            $session_id,
4072
            $course_id,
4073
        ];
4074
4075
        if (empty($session_id)) {
4076
            $conditions = [
4077
                'where' => ["$sql_active_exercises (session_id = 0 OR session_id IS NULL) AND c_id = ?" => [$course_id]],
4078
                'order' => 'title',
4079
            ];
4080
        } else {
4081
            // All exercises
4082
            $conditions = [
4083
                'where' => ["$sql_active_exercises (session_id = 0 OR session_id IS NULL OR session_id = ? ) AND c_id = ?" => $params],
4084
                'order' => 'title',
4085
            ];
4086
        }
4087
4088
        return Database::select('*', $table, $conditions);
4089
    }
4090
4091
    /**
4092
     * Gets the position of the score based in a given score (result/weight)
4093
     * and the exe_id based in the user list
4094
     * (NO Exercises in LPs ).
4095
     *
4096
     * @param float  $my_score      user score to be compared *attention*
4097
     *                              $my_score = score/weight and not just the score
4098
     * @param int    $my_exe_id     exe id of the exercise
4099
     *                              (this is necessary because if 2 students have the same score the one
4100
     *                              with the minor exe_id will have a best position, just to be fair and FIFO)
4101
     * @param int    $exercise_id
4102
     * @param string $course_code
4103
     * @param int    $session_id
4104
     * @param array  $user_list
4105
     * @param bool   $return_string
4106
     *
4107
     * @return int the position of the user between his friends in a course
4108
     *             (or course within a session)
4109
     */
4110
    public static function get_exercise_result_ranking(
4111
        $my_score,
4112
        $my_exe_id,
4113
        $exercise_id,
4114
        $course_code,
4115
        $session_id = 0,
4116
        $user_list = [],
4117
        $return_string = true,
4118
        $skipLpResults = true
4119
    ) {
4120
        //No score given we return
4121
        if (is_null($my_score)) {
4122
            return '-';
4123
        }
4124
        if (empty($user_list)) {
4125
            return '-';
4126
        }
4127
4128
        $best_attempts = [];
4129
        foreach ($user_list as $user_data) {
4130
            $user_id = $user_data['user_id'];
4131
            $best_attempts[$user_id] = self::get_best_attempt_by_user(
4132
                $user_id,
4133
                $exercise_id,
4134
                $course_code,
4135
                $session_id,
4136
                $skipLpResults
4137
            );
4138
        }
4139
4140
        if (empty($best_attempts)) {
4141
            return 1;
4142
        } else {
4143
            $position = 1;
4144
            $my_ranking = [];
4145
            foreach ($best_attempts as $user_id => $result) {
4146
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
4147
                    $my_ranking[$user_id] = $result['exe_result'] / $result['exe_weighting'];
4148
                } else {
4149
                    $my_ranking[$user_id] = 0;
4150
                }
4151
            }
4152
            //if (!empty($my_ranking)) {
4153
            asort($my_ranking);
4154
            $position = count($my_ranking);
4155
            if (!empty($my_ranking)) {
4156
                foreach ($my_ranking as $user_id => $ranking) {
4157
                    if ($my_score >= $ranking) {
4158
                        if ($my_score == $ranking && isset($best_attempts[$user_id]['exe_id'])) {
4159
                            $exe_id = $best_attempts[$user_id]['exe_id'];
4160
                            if ($my_exe_id < $exe_id) {
4161
                                $position--;
4162
                            }
4163
                        } else {
4164
                            $position--;
4165
                        }
4166
                    }
4167
                }
4168
            }
4169
            //}
4170
            $return_value = [
4171
                'position' => $position,
4172
                'count' => count($my_ranking),
4173
            ];
4174
4175
            if ($return_string) {
4176
                if (!empty($position) && !empty($my_ranking)) {
4177
                    $return_value = $position.'/'.count($my_ranking);
4178
                } else {
4179
                    $return_value = '-';
4180
                }
4181
            }
4182
4183
            return $return_value;
4184
        }
4185
    }
4186
4187
    /**
4188
     * Gets the position of the score based in a given score (result/weight) and the exe_id based in all attempts
4189
     * (NO Exercises in LPs ) old functionality by attempt.
4190
     *
4191
     * @param   float   user score to be compared attention => score/weight
4192
     * @param   int     exe id of the exercise
4193
     * (this is necessary because if 2 students have the same score the one
4194
     * with the minor exe_id will have a best position, just to be fair and FIFO)
4195
     * @param   int     exercise id
4196
     * @param   string  course code
4197
     * @param   int     session id
4198
     * @param bool $return_string
4199
     *
4200
     * @return int the position of the user between his friends in a course (or course within a session)
4201
     */
4202
    public static function get_exercise_result_ranking_by_attempt(
4203
        $my_score,
4204
        $my_exe_id,
4205
        $exercise_id,
4206
        $courseId,
4207
        $session_id = 0,
4208
        $return_string = true
4209
    ) {
4210
        if (empty($session_id)) {
4211
            $session_id = 0;
4212
        }
4213
        if (is_null($my_score)) {
4214
            return '-';
4215
        }
4216
        $user_results = Event::get_all_exercise_results(
4217
            $exercise_id,
4218
            $courseId,
4219
            $session_id,
4220
            false
4221
        );
4222
        $position_data = [];
4223
        if (empty($user_results)) {
4224
            return 1;
4225
        } else {
4226
            $position = 1;
4227
            $my_ranking = [];
4228
            foreach ($user_results as $result) {
4229
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
4230
                    $my_ranking[$result['exe_id']] = $result['exe_result'] / $result['exe_weighting'];
4231
                } else {
4232
                    $my_ranking[$result['exe_id']] = 0;
4233
                }
4234
            }
4235
            asort($my_ranking);
4236
            $position = count($my_ranking);
4237
            if (!empty($my_ranking)) {
4238
                foreach ($my_ranking as $exe_id => $ranking) {
4239
                    if ($my_score >= $ranking) {
4240
                        if ($my_score == $ranking) {
4241
                            if ($my_exe_id < $exe_id) {
4242
                                $position--;
4243
                            }
4244
                        } else {
4245
                            $position--;
4246
                        }
4247
                    }
4248
                }
4249
            }
4250
            $return_value = [
4251
                'position' => $position,
4252
                'count' => count($my_ranking),
4253
            ];
4254
4255
            if ($return_string) {
4256
                if (!empty($position) && !empty($my_ranking)) {
4257
                    return $position.'/'.count($my_ranking);
4258
                }
4259
            }
4260
4261
            return $return_value;
4262
        }
4263
    }
4264
4265
    /**
4266
     * Get the best attempt in a exercise (NO Exercises in LPs ).
4267
     *
4268
     * @param int $exercise_id
4269
     * @param int $courseId
4270
     * @param int $session_id
4271
     *
4272
     * @return array
4273
     */
4274
    public static function get_best_attempt_in_course($exercise_id, $courseId, $session_id, $skipLpResults = true)
4275
    {
4276
        $user_results = Event::get_all_exercise_results(
4277
            $exercise_id,
4278
            $courseId,
4279
            $session_id,
4280
            false,
4281
            null,
4282
            0,
4283
            $skipLpResults
4284
        );
4285
4286
        $best_score_data = [];
4287
        $best_score = 0;
4288
        if (!empty($user_results)) {
4289
            foreach ($user_results as $result) {
4290
                if (!empty($result['exe_weighting']) &&
4291
                    intval($result['exe_weighting']) != 0
4292
                ) {
4293
                    $score = $result['exe_result'] / $result['exe_weighting'];
4294
                    if ($score >= $best_score) {
4295
                        $best_score = $score;
4296
                        $best_score_data = $result;
4297
                    }
4298
                }
4299
            }
4300
        }
4301
4302
        return $best_score_data;
4303
    }
4304
4305
    /**
4306
     * Get the best score in a exercise (NO Exercises in LPs ).
4307
     *
4308
     * @param int $user_id
4309
     * @param int $exercise_id
4310
     * @param int $courseId
4311
     * @param int $session_id
4312
     *
4313
     * @return array
4314
     */
4315
    public static function get_best_attempt_by_user(
4316
        $user_id,
4317
        $exercise_id,
4318
        $courseId,
4319
        $session_id,
4320
        $skipLpResults = true
4321
    ) {
4322
        $user_results = Event::get_all_exercise_results(
4323
            $exercise_id,
4324
            $courseId,
4325
            $session_id,
4326
            false,
4327
            $user_id,
4328
            0,
4329
            $skipLpResults
4330
        );
4331
        $best_score_data = [];
4332
        $best_score = 0;
4333
        if (!empty($user_results)) {
4334
            foreach ($user_results as $result) {
4335
                if (!empty($result['exe_weighting']) && (float) $result['exe_weighting'] != 0) {
4336
                    $score = $result['exe_result'] / $result['exe_weighting'];
4337
                    if ($score >= $best_score) {
4338
                        $best_score = $score;
4339
                        $best_score_data = $result;
4340
                    }
4341
                }
4342
            }
4343
        }
4344
4345
        return $best_score_data;
4346
    }
4347
4348
    /**
4349
     * Get average score (NO Exercises in LPs ).
4350
     *
4351
     * @param int $exerciseId
4352
     * @param int $courseId
4353
     * @param int $sessionId
4354
     *
4355
     * @return float Average score
4356
     */
4357
    public static function get_average_score($exerciseId, $courseId, $sessionId, $groupId = 0)
4358
    {
4359
        $user_results = Event::get_all_exercise_results(
4360
            $exerciseId,
4361
            $courseId,
4362
            $sessionId,
4363
            true,
4364
            null,
4365
            $groupId
4366
        );
4367
        $avg_score = 0;
4368
        if (!empty($user_results)) {
4369
            foreach ($user_results as $result) {
4370
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
4371
                    $score = $result['exe_result'] / $result['exe_weighting'];
4372
                    $avg_score += $score;
4373
                }
4374
            }
4375
            $avg_score = float_format($avg_score / count($user_results), 1);
4376
        }
4377
4378
        return $avg_score;
4379
    }
4380
4381
    /**
4382
     * Get average quiz score by course (Only exercises not added in a LP).
4383
     *
4384
     * @param int $courseId
4385
     * @param int $sessionId
4386
     *
4387
     * @return float Average score
4388
     */
4389
    public static function get_average_score_by_course($courseId, $sessionId)
4390
    {
4391
        $user_results = Event::get_all_exercise_results_by_course(
4392
            $courseId,
4393
            $sessionId,
4394
            false
4395
        );
4396
        $avg_score = 0;
4397
        if (!empty($user_results)) {
4398
            foreach ($user_results as $result) {
4399
                if (!empty($result['exe_weighting']) && intval(
4400
                        $result['exe_weighting']
4401
                    ) != 0
4402
                ) {
4403
                    $score = $result['exe_result'] / $result['exe_weighting'];
4404
                    $avg_score += $score;
4405
                }
4406
            }
4407
            // We assume that all exe_weighting
4408
            $avg_score = $avg_score / count($user_results);
4409
        }
4410
4411
        return $avg_score;
4412
    }
4413
4414
    /**
4415
     * @param int $user_id
4416
     * @param int $courseId
4417
     * @param int $session_id
4418
     *
4419
     * @return float|int
4420
     */
4421
    public static function get_average_score_by_course_by_user(
4422
        $user_id,
4423
        $courseId,
4424
        $session_id
4425
    ) {
4426
        $user_results = Event::get_all_exercise_results_by_user(
4427
            $user_id,
4428
            $courseId,
4429
            $session_id
4430
        );
4431
        $avg_score = 0;
4432
        if (!empty($user_results)) {
4433
            foreach ($user_results as $result) {
4434
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
4435
                    $score = $result['exe_result'] / $result['exe_weighting'];
4436
                    $avg_score += $score;
4437
                }
4438
            }
4439
            // We assume that all exe_weighting
4440
            $avg_score = ($avg_score / count($user_results));
4441
        }
4442
4443
        return $avg_score;
4444
    }
4445
4446
    /**
4447
     * Get average score by score (NO Exercises in LPs ).
4448
     *
4449
     * @param int $exercise_id
4450
     * @param int $courseId
4451
     * @param int $session_id
4452
     * @param int $user_count
4453
     *
4454
     * @return float Best average score
4455
     */
4456
    public static function get_best_average_score_by_exercise(
4457
        $exercise_id,
4458
        $courseId,
4459
        $session_id,
4460
        $user_count
4461
    ) {
4462
        $user_results = Event::get_best_exercise_results_by_user(
4463
            $exercise_id,
4464
            $courseId,
4465
            $session_id
4466
        );
4467
        $avg_score = 0;
4468
        if (!empty($user_results)) {
4469
            foreach ($user_results as $result) {
4470
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
4471
                    $score = $result['exe_result'] / $result['exe_weighting'];
4472
                    $avg_score += $score;
4473
                }
4474
            }
4475
            // We asumme that all exe_weighting
4476
            if (!empty($user_count)) {
4477
                $avg_score = float_format($avg_score / $user_count, 1) * 100;
4478
            } else {
4479
                $avg_score = 0;
4480
            }
4481
        }
4482
4483
        return $avg_score;
4484
    }
4485
4486
    /**
4487
     * Get average score by score (NO Exercises in LPs ).
4488
     *
4489
     * @param int $exercise_id
4490
     * @param int $courseId
4491
     * @param int $session_id
4492
     *
4493
     * @return float Best average score
4494
     */
4495
    public static function getBestScoreByExercise(
4496
        $exercise_id,
4497
        $courseId,
4498
        $session_id
4499
    ) {
4500
        $user_results = Event::get_best_exercise_results_by_user(
4501
            $exercise_id,
4502
            $courseId,
4503
            $session_id
4504
        );
4505
        $avg_score = 0;
4506
        if (!empty($user_results)) {
4507
            foreach ($user_results as $result) {
4508
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
4509
                    $score = $result['exe_result'] / $result['exe_weighting'];
4510
                    $avg_score += $score;
4511
                }
4512
            }
4513
        }
4514
4515
        return $avg_score;
4516
    }
4517
4518
    /**
4519
     * @param string $course_code
4520
     * @param int    $session_id
4521
     *
4522
     * @return array
4523
     */
4524
    public static function get_exercises_to_be_taken($course_code, $session_id)
4525
    {
4526
        $course_info = api_get_course_info($course_code);
4527
        $exercises = self::get_all_exercises($course_info, $session_id);
4528
        $result = [];
4529
        $now = time() + 15 * 24 * 60 * 60;
4530
        foreach ($exercises as $exercise_item) {
4531
            if (isset($exercise_item['end_time']) &&
4532
                !empty($exercise_item['end_time']) &&
4533
                api_strtotime($exercise_item['end_time'], 'UTC') < $now
4534
            ) {
4535
                $result[] = $exercise_item;
4536
            }
4537
        }
4538
4539
        return $result;
4540
    }
4541
4542
    /**
4543
     * Get student results (only in completed exercises) stats by question.
4544
     *
4545
     * @param int  $question_id
4546
     * @param int  $exercise_id
4547
     * @param int  $courseId
4548
     * @param int  $session_id
4549
     * @param bool $onlyStudent Filter only enrolled students
4550
     *
4551
     * @return array
4552
     */
4553
    public static function get_student_stats_by_question(
4554
        $question_id,
4555
        $exercise_id,
4556
        $courseId,
4557
        $session_id,
4558
        $onlyStudent = false
4559
    ) {
4560
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4561
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4562
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4563
4564
        $question_id = (int) $question_id;
4565
        $exercise_id = (int) $exercise_id;
4566
        $session_id = (int) $session_id;
4567
        $courseId = (int) $courseId;
4568
4569
        $sql = "SELECT MAX(marks) as max, MIN(marks) as min, AVG(marks) as average
4570
                FROM $track_exercises e ";
4571
        if ($onlyStudent) {
4572
            if (empty($session_id)) {
4573
                $courseCondition = "
4574
                    INNER JOIN $courseUser c
4575
                    ON (
4576
                        e.exe_user_id = c.user_id AND
4577
                        e.c_id = c.c_id AND
4578
                        c.status = ".STUDENT."
4579
                        AND relation_type <> 2
4580
                    )";
4581
            } else {
4582
                $sessionRelCourse = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4583
                $courseCondition = "
4584
                    INNER JOIN $sessionRelCourse sc
4585
                    ON (
4586
                        e.exe_user_id = sc.user_id AND
4587
                        e.c_id = sc.c_id AND
4588
                        e.session_id = sc.session_id AND
4589
                        sc.status = 0
4590
                    ) ";
4591
            }
4592
            $sql .= $courseCondition;
4593
        }
4594
4595
        $sql .= "
4596
            INNER JOIN $track_attempt a
4597
    		ON (
4598
    		    a.exe_id = e.exe_id AND
4599
    		    e.c_id = a.c_id AND
4600
    		    e.session_id  = a.session_id
4601
            )
4602
    		WHERE
4603
    		    exe_exo_id 	= $exercise_id AND
4604
                a.c_id = $courseId AND
4605
                e.session_id = $session_id AND
4606
                question_id = $question_id AND
4607
                e.status = ''
4608
            LIMIT 1";
4609
        $result = Database::query($sql);
4610
        $return = [];
4611
        if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
4612
            $return = Database::fetch_array($result, 'ASSOC');
4613
        }
4614
4615
        return $return;
4616
    }
4617
4618
    /**
4619
     * Get the correct answer count for a fill blanks question.
4620
     *
4621
     * @param int $question_id
4622
     * @param int $exercise_id
4623
     *
4624
     * @return array
4625
     */
4626
    public static function getNumberStudentsFillBlanksAnswerCount(
4627
        $question_id,
4628
        $exercise_id
4629
    ) {
4630
        $listStudentsId = [];
4631
        $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
4632
            api_get_course_id(),
4633
            true
4634
        );
4635
        foreach ($listAllStudentInfo as $i => $listStudentInfo) {
4636
            $listStudentsId[] = $listStudentInfo['user_id'];
4637
        }
4638
4639
        $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
4640
            $exercise_id,
4641
            $question_id,
4642
            $listStudentsId,
4643
            '1970-01-01',
4644
            '3000-01-01'
4645
        );
4646
4647
        $arrayCount = [];
4648
4649
        foreach ($listFillTheBlankResult as $resultCount) {
4650
            foreach ($resultCount as $index => $count) {
4651
                //this is only for declare the array index per answer
4652
                $arrayCount[$index] = 0;
4653
            }
4654
        }
4655
4656
        foreach ($listFillTheBlankResult as $resultCount) {
4657
            foreach ($resultCount as $index => $count) {
4658
                $count = ($count === 0) ? 1 : 0;
4659
                $arrayCount[$index] += $count;
4660
            }
4661
        }
4662
4663
        return $arrayCount;
4664
    }
4665
4666
    /**
4667
     * Get the number of questions with answers.
4668
     *
4669
     * @param int    $question_id
4670
     * @param int    $exercise_id
4671
     * @param string $course_code
4672
     * @param int    $session_id
4673
     * @param string $questionType
4674
     *
4675
     * @return int
4676
     */
4677
    public static function get_number_students_question_with_answer_count(
4678
        $question_id,
4679
        $exercise_id,
4680
        $course_code,
4681
        $session_id,
4682
        $questionType = ''
4683
    ) {
4684
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4685
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4686
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4687
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4688
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4689
4690
        $question_id = intval($question_id);
4691
        $exercise_id = intval($exercise_id);
4692
        $courseId = api_get_course_int_id($course_code);
4693
        $session_id = intval($session_id);
4694
4695
        if (in_array($questionType, [FILL_IN_BLANKS, FILL_IN_BLANKS_COMBINATION])) {
4696
            $listStudentsId = [];
4697
            $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
4698
                api_get_course_id(),
4699
                true
4700
            );
4701
            foreach ($listAllStudentInfo as $i => $listStudentInfo) {
4702
                $listStudentsId[] = $listStudentInfo['user_id'];
4703
            }
4704
4705
            $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
4706
                $exercise_id,
4707
                $question_id,
4708
                $listStudentsId,
4709
                '1970-01-01',
4710
                '3000-01-01'
4711
            );
4712
4713
            return FillBlanks::getNbResultFillBlankAll($listFillTheBlankResult);
4714
        }
4715
4716
        if (empty($session_id)) {
4717
            $courseCondition = "
4718
            INNER JOIN $courseUser cu
4719
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
4720
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4721
        } else {
4722
            $courseCondition = "
4723
            INNER JOIN $courseUserSession cu
4724
            ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4725
            $courseConditionWhere = " AND cu.status = 0 ";
4726
        }
4727
4728
        $sql = "SELECT DISTINCT exe_user_id
4729
    		FROM $track_exercises e
4730
    		INNER JOIN $track_attempt a
4731
    		ON (
4732
    		    a.exe_id = e.exe_id AND
4733
    		    e.c_id = a.c_id AND
4734
    		    e.session_id  = a.session_id
4735
            )
4736
            INNER JOIN $courseTable c
4737
            ON (c.id = a.c_id)
4738
    		$courseCondition
4739
    		WHERE
4740
    		    exe_exo_id = $exercise_id AND
4741
                a.c_id = $courseId AND
4742
                e.session_id = $session_id AND
4743
                question_id = $question_id AND
4744
                answer <> '0' AND
4745
                e.status = ''
4746
                $courseConditionWhere
4747
            ";
4748
        $result = Database::query($sql);
4749
        $return = 0;
4750
        if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
4751
            $return = Database::num_rows($result);
4752
        }
4753
4754
        return $return;
4755
    }
4756
4757
    /**
4758
     * Get number of answers to hotspot questions.
4759
     *
4760
     * @param int    $answer_id
4761
     * @param int    $question_id
4762
     * @param int    $exercise_id
4763
     * @param string $course_code
4764
     * @param int    $session_id
4765
     *
4766
     * @return int
4767
     */
4768
    public static function get_number_students_answer_hotspot_count(
4769
        $answer_id,
4770
        $question_id,
4771
        $exercise_id,
4772
        $course_code,
4773
        $session_id
4774
    ) {
4775
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4776
        $track_hotspot = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4777
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4778
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4779
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4780
4781
        $question_id = (int) $question_id;
4782
        $answer_id = (int) $answer_id;
4783
        $exercise_id = (int) $exercise_id;
4784
        $course_code = Database::escape_string($course_code);
4785
        $session_id = (int) $session_id;
4786
4787
        if (empty($session_id)) {
4788
            $courseCondition = "
4789
            INNER JOIN $courseUser cu
4790
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
4791
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4792
        } else {
4793
            $courseCondition = "
4794
            INNER JOIN $courseUserSession cu
4795
            ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4796
            $courseConditionWhere = ' AND cu.status = 0 ';
4797
        }
4798
4799
        $sql = "SELECT DISTINCT exe_user_id
4800
    		FROM $track_exercises e
4801
    		INNER JOIN $track_hotspot a
4802
    		ON (a.hotspot_exe_id = e.exe_id)
4803
    		INNER JOIN $courseTable c
4804
    		ON (hotspot_course_code = c.code)
4805
    		$courseCondition
4806
    		WHERE
4807
    		    exe_exo_id              = $exercise_id AND
4808
                a.hotspot_course_code 	= '$course_code' AND
4809
                e.session_id            = $session_id AND
4810
                hotspot_answer_id       = $answer_id AND
4811
                hotspot_question_id     = $question_id AND
4812
                hotspot_correct         =  1 AND
4813
                e.status                = ''
4814
                $courseConditionWhere
4815
            ";
4816
4817
        $result = Database::query($sql);
4818
        $return = 0;
4819
        if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
4820
            $return = Database::num_rows($result);
4821
        }
4822
4823
        return $return;
4824
    }
4825
4826
    /**
4827
     * @param int    $answer_id
4828
     * @param int    $question_id
4829
     * @param int    $exercise_id
4830
     * @param int    $courseId
4831
     * @param int    $session_id
4832
     * @param string $question_type
4833
     * @param string $correct_answer
4834
     * @param string $current_answer
4835
     *
4836
     * @return int
4837
     */
4838
    public static function get_number_students_answer_count(
4839
        $answer_id,
4840
        $question_id,
4841
        $exercise_id,
4842
        $courseId,
4843
        $session_id,
4844
        $question_type = null,
4845
        $correct_answer = null,
4846
        $current_answer = null
4847
    ) {
4848
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4849
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4850
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4851
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4852
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4853
4854
        $question_id = (int) $question_id;
4855
        $answer_id = (int) $answer_id;
4856
        $exercise_id = (int) $exercise_id;
4857
        $courseId = (int) $courseId;
4858
        $session_id = (int) $session_id;
4859
4860
        switch ($question_type) {
4861
            case FILL_IN_BLANKS:
4862
            case FILL_IN_BLANKS_COMBINATION:
4863
                $answer_condition = '';
4864
                $select_condition = ' e.exe_id, answer ';
4865
                break;
4866
            case MATCHING:
4867
            case MATCHING_COMBINATION:
4868
            case MATCHING_DRAGGABLE:
4869
            case MATCHING_DRAGGABLE_COMBINATION:
4870
            default:
4871
                $answer_condition = " answer = $answer_id AND ";
4872
                $select_condition = ' DISTINCT exe_user_id ';
4873
        }
4874
4875
        if (empty($session_id)) {
4876
            $courseCondition = "
4877
            INNER JOIN $courseUser cu
4878
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
4879
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4880
        } else {
4881
            $courseCondition = "
4882
            INNER JOIN $courseUserSession cu
4883
            ON (cu.c_id = a.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4884
            $courseConditionWhere = ' AND cu.status = 0 ';
4885
        }
4886
4887
        $sql = "SELECT $select_condition
4888
    		FROM $track_exercises e
4889
    		INNER JOIN $track_attempt a
4890
    		ON (
4891
    		    a.exe_id = e.exe_id AND
4892
    		    e.c_id = a.c_id AND
4893
    		    e.session_id  = a.session_id
4894
            )
4895
            INNER JOIN $courseTable c
4896
            ON c.id = a.c_id
4897
    		$courseCondition
4898
    		WHERE
4899
    		    exe_exo_id = $exercise_id AND
4900
                a.c_id = $courseId AND
4901
                e.session_id = $session_id AND
4902
                $answer_condition
4903
                question_id = $question_id AND
4904
                e.status = ''
4905
                $courseConditionWhere
4906
            ";
4907
        $result = Database::query($sql);
4908
        $return = 0;
4909
        if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
4910
            $good_answers = 0;
4911
            switch ($question_type) {
4912
                case FILL_IN_BLANKS:
4913
                case FILL_IN_BLANKS_COMBINATION:
4914
                    while ($row = Database::fetch_array($result, 'ASSOC')) {
4915
                        $fill_blank = self::check_fill_in_blanks(
4916
                            $correct_answer,
4917
                            $row['answer'],
4918
                            $current_answer
4919
                        );
4920
                        if (isset($fill_blank[$current_answer]) && $fill_blank[$current_answer] == 1) {
4921
                            $good_answers++;
4922
                        }
4923
                    }
4924
4925
                    return $good_answers;
4926
                    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...
4927
                case MATCHING:
4928
                case MATCHING_COMBINATION:
4929
                case MATCHING_DRAGGABLE:
4930
                case MATCHING_DRAGGABLE_COMBINATION:
4931
                default:
4932
                    $return = Database::num_rows($result);
4933
            }
4934
        }
4935
4936
        return $return;
4937
    }
4938
4939
    /**
4940
     * @param array  $answer
4941
     * @param string $user_answer
4942
     *
4943
     * @return array
4944
     */
4945
    public static function check_fill_in_blanks($answer, $user_answer, $current_answer)
4946
    {
4947
        // the question is encoded like this
4948
        // [A] B [C] D [E] F::10,10,10@1
4949
        // number 1 before the "@" means that is a switchable fill in blank question
4950
        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4951
        // means that is a normal fill blank question
4952
        // first we explode the "::"
4953
        $pre_array = explode('::', $answer);
4954
        // is switchable fill blank or not
4955
        $last = count($pre_array) - 1;
4956
        $is_set_switchable = explode('@', $pre_array[$last]);
4957
        $switchable_answer_set = false;
4958
        if (isset($is_set_switchable[1]) && $is_set_switchable[1] == 1) {
4959
            $switchable_answer_set = true;
4960
        }
4961
        $answer = '';
4962
        for ($k = 0; $k < $last; $k++) {
4963
            $answer .= $pre_array[$k];
4964
        }
4965
        // splits weightings that are joined with a comma
4966
        $answerWeighting = explode(',', $is_set_switchable[0]);
4967
4968
        // we save the answer because it will be modified
4969
        //$temp = $answer;
4970
        $temp = $answer;
4971
4972
        $answer = '';
4973
        $j = 0;
4974
        //initialise answer tags
4975
        $user_tags = $correct_tags = $real_text = [];
4976
        // the loop will stop at the end of the text
4977
        while (1) {
4978
            // quits the loop if there are no more blanks (detect '[')
4979
            if (($pos = api_strpos($temp, '[')) === false) {
4980
                // adds the end of the text
4981
                $answer = $temp;
4982
                $real_text[] = $answer;
4983
                break; //no more "blanks", quit the loop
4984
            }
4985
            // adds the piece of text that is before the blank
4986
            //and ends with '[' into a general storage array
4987
            $real_text[] = api_substr($temp, 0, $pos + 1);
4988
            $answer .= api_substr($temp, 0, $pos + 1);
4989
            //take the string remaining (after the last "[" we found)
4990
            $temp = api_substr($temp, $pos + 1);
4991
            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4992
            if (($pos = api_strpos($temp, ']')) === false) {
4993
                // adds the end of the text
4994
                $answer .= $temp;
4995
                break;
4996
            }
4997
4998
            $str = $user_answer;
4999
5000
            preg_match_all('#\[([^[]*)\]#', $str, $arr);
5001
            $str = str_replace('\r\n', '', $str);
5002
            $choices = $arr[1];
5003
            $choice = [];
5004
            $check = false;
5005
            $i = 0;
5006
            foreach ($choices as $item) {
5007
                if ($current_answer === $item) {
5008
                    $check = true;
5009
                }
5010
                if ($check) {
5011
                    $choice[] = $item;
5012
                    $i++;
5013
                }
5014
                if ($i == 3) {
5015
                    break;
5016
                }
5017
            }
5018
            $tmp = api_strrpos($choice[$j], ' / ');
5019
5020
            if ($tmp !== false) {
5021
                $choice[$j] = api_substr($choice[$j], 0, $tmp);
5022
            }
5023
5024
            $choice[$j] = trim($choice[$j]);
5025
5026
            //Needed to let characters ' and " to work as part of an answer
5027
            $choice[$j] = stripslashes($choice[$j]);
5028
5029
            $user_tags[] = api_strtolower($choice[$j]);
5030
            //put the contents of the [] answer tag into correct_tags[]
5031
            $correct_tags[] = api_strtolower(api_substr($temp, 0, $pos));
5032
            $j++;
5033
            $temp = api_substr($temp, $pos + 1);
5034
        }
5035
5036
        $answer = '';
5037
        $real_correct_tags = $correct_tags;
5038
        $chosen_list = [];
5039
        $good_answer = [];
5040
5041
        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...
5042
            if (!$switchable_answer_set) {
5043
                //needed to parse ' and " characters
5044
                $user_tags[$i] = stripslashes($user_tags[$i]);
5045
                if ($correct_tags[$i] == $user_tags[$i]) {
5046
                    $good_answer[$correct_tags[$i]] = 1;
5047
                } elseif (!empty($user_tags[$i])) {
5048
                    $good_answer[$correct_tags[$i]] = 0;
5049
                } else {
5050
                    $good_answer[$correct_tags[$i]] = 0;
5051
                }
5052
            } else {
5053
                // switchable fill in the blanks
5054
                if (in_array($user_tags[$i], $correct_tags)) {
5055
                    $correct_tags = array_diff($correct_tags, $chosen_list);
5056
                    $good_answer[$correct_tags[$i]] = 1;
5057
                } elseif (!empty($user_tags[$i])) {
5058
                    $good_answer[$correct_tags[$i]] = 0;
5059
                } else {
5060
                    $good_answer[$correct_tags[$i]] = 0;
5061
                }
5062
            }
5063
            // adds the correct word, followed by ] to close the blank
5064
            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
5065
            if (isset($real_text[$i + 1])) {
5066
                $answer .= $real_text[$i + 1];
5067
            }
5068
        }
5069
5070
        return $good_answer;
5071
    }
5072
5073
    /**
5074
     * It gets the number of users who finishing the exercise.
5075
     *
5076
     * @param int $exerciseId
5077
     * @param int $courseId
5078
     * @param int $sessionId
5079
     *
5080
     * @return int
5081
     */
5082
    public static function getNumberStudentsFinishExercise(
5083
        $exerciseId,
5084
        $courseId,
5085
        $sessionId
5086
    ) {
5087
        $tblTrackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5088
        $tblTrackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5089
5090
        $exerciseId = (int) $exerciseId;
5091
        $courseId = (int) $courseId;
5092
        $sessionId = (int) $sessionId;
5093
5094
        $sql = "SELECT DISTINCT exe_user_id
5095
                FROM $tblTrackExercises e
5096
                INNER JOIN $tblTrackAttempt a
5097
                ON (a.exe_id = e.exe_id)
5098
                WHERE
5099
                    exe_exo_id 	 = $exerciseId AND
5100
                    e.c_id  = $courseId AND
5101
                    e.session_id = $sessionId AND
5102
                    status = ''";
5103
        $result = Database::query($sql);
5104
        $return = 0;
5105
        if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
5106
            $return = Database::num_rows($result);
5107
        }
5108
5109
        return $return;
5110
    }
5111
5112
    /**
5113
     * Return an HTML select menu with the student groups.
5114
     *
5115
     * @param string $name     is the name and the id of the <select>
5116
     * @param string $default  default value for option
5117
     * @param string $onchange
5118
     *
5119
     * @return string the html code of the <select>
5120
     */
5121
    public static function displayGroupMenu($name, $default, $onchange = "")
5122
    {
5123
        // check the default value of option
5124
        $tabSelected = [$default => " selected='selected' "];
5125
        $res = "";
5126
        $res .= "<select name='$name' id='$name' onchange='".$onchange."' >";
5127
        $res .= "<option value='-1'".$tabSelected["-1"].">-- ".get_lang(
5128
                'AllGroups'
5129
            )." --</option>";
5130
        $res .= "<option value='0'".$tabSelected["0"].">- ".get_lang(
5131
                'NotInAGroup'
5132
            )." -</option>";
5133
        $tabGroups = GroupManager::get_group_list();
5134
        $currentCatId = 0;
5135
        $countGroups = count($tabGroups);
5136
        for ($i = 0; $i < $countGroups; $i++) {
5137
            $tabCategory = GroupManager::get_category_from_group(
5138
                $tabGroups[$i]['iid']
5139
            );
5140
            if ($tabCategory['iid'] != $currentCatId) {
5141
                $res .= "<option value='-1' disabled='disabled'>".$tabCategory['title']."</option>";
5142
                $currentCatId = $tabCategory['iid'];
5143
            }
5144
            $res .= "<option ".$tabSelected[$tabGroups[$i]['iid']]."style='margin-left:40px' value='".
5145
                $tabGroups[$i]['iid']."'>".
5146
                $tabGroups[$i]['name'].
5147
                "</option>";
5148
        }
5149
        $res .= "</select>";
5150
5151
        return $res;
5152
    }
5153
5154
    /**
5155
     * @param int $exe_id
5156
     */
5157
    public static function create_chat_exercise_session($exe_id)
5158
    {
5159
        if (!isset($_SESSION['current_exercises'])) {
5160
            $_SESSION['current_exercises'] = [];
5161
        }
5162
        $_SESSION['current_exercises'][$exe_id] = true;
5163
    }
5164
5165
    /**
5166
     * @param int $exe_id
5167
     */
5168
    public static function delete_chat_exercise_session($exe_id)
5169
    {
5170
        if (isset($_SESSION['current_exercises'])) {
5171
            $_SESSION['current_exercises'][$exe_id] = false;
5172
        }
5173
    }
5174
5175
    /**
5176
     * Display the exercise results.
5177
     *
5178
     * @param Exercise $objExercise
5179
     * @param int      $exeId
5180
     * @param bool     $save_user_result save users results (true) or just show the results (false)
5181
     * @param string   $remainingMessage
5182
     * @param bool     $allowSignature
5183
     * @param bool     $allowExportPdf
5184
     * @param bool     $isExport
5185
     */
5186
    public static function displayQuestionListByAttempt(
5187
        $objExercise,
5188
        $exeId,
5189
        $save_user_result = false,
5190
        $remainingMessage = '',
5191
        $allowSignature = false,
5192
        $allowExportPdf = false,
5193
        $isExport = false
5194
    ) {
5195
        $origin = api_get_origin();
5196
        $courseId = api_get_course_int_id();
5197
        $courseCode = api_get_course_id();
5198
        $sessionId = api_get_session_id();
5199
5200
        // Getting attempt info
5201
        $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
5202
5203
        // Getting question list
5204
        $question_list = [];
5205
        $studentInfo = [];
5206
        if (!empty($exercise_stat_info['data_tracking'])) {
5207
            $studentInfo = api_get_user_info($exercise_stat_info['exe_user_id']);
5208
            $question_list = explode(',', $exercise_stat_info['data_tracking']);
5209
        } else {
5210
            // Try getting the question list only if save result is off
5211
            if ($save_user_result == false) {
5212
                $question_list = $objExercise->get_validated_question_list();
5213
            }
5214
            if (in_array(
5215
                $objExercise->getFeedbackType(),
5216
                [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
5217
            )) {
5218
                $question_list = $objExercise->get_validated_question_list();
5219
            }
5220
        }
5221
5222
        if ($objExercise->getResultAccess()) {
5223
            if ($objExercise->hasResultsAccess($exercise_stat_info) === false) {
5224
                echo Display::return_message(
5225
                    sprintf(get_lang('YouPassedTheLimitOfXMinutesToSeeTheResults'), $objExercise->getResultsAccess())
5226
                );
5227
5228
                return false;
5229
            }
5230
5231
            if (!empty($objExercise->getResultAccess())) {
5232
                $url = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$objExercise->iid;
5233
                echo $objExercise->returnTimeLeftDiv();
5234
                echo $objExercise->showSimpleTimeControl(
5235
                    $objExercise->getResultAccessTimeDiff($exercise_stat_info),
5236
                    $url
5237
                );
5238
            }
5239
        }
5240
5241
        $counter = 1;
5242
        $total_score = $total_weight = 0;
5243
        $exercise_content = null;
5244
        // Hide results
5245
        $show_results = false;
5246
        $show_only_score = false;
5247
        if (in_array($objExercise->results_disabled,
5248
            [
5249
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
5250
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
5251
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
5252
            ]
5253
        )) {
5254
            $show_results = true;
5255
        }
5256
5257
        if (in_array(
5258
            $objExercise->results_disabled,
5259
            [
5260
                RESULT_DISABLE_SHOW_SCORE_ONLY,
5261
                RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES,
5262
                RESULT_DISABLE_RANKING,
5263
            ]
5264
        )
5265
        ) {
5266
            $show_only_score = true;
5267
        }
5268
5269
        // Not display expected answer, but score, and feedback
5270
        $show_all_but_expected_answer = false;
5271
        if ($objExercise->results_disabled == RESULT_DISABLE_SHOW_SCORE_ONLY &&
5272
            $objExercise->getFeedbackType() == EXERCISE_FEEDBACK_TYPE_END
5273
        ) {
5274
            $show_all_but_expected_answer = true;
5275
            $show_results = true;
5276
            $show_only_score = false;
5277
        }
5278
5279
        $showTotalScoreAndUserChoicesInLastAttempt = true;
5280
        $showTotalScore = true;
5281
        $showQuestionScore = true;
5282
        $attemptResult = [];
5283
5284
        if (in_array(
5285
            $objExercise->results_disabled,
5286
            [
5287
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
5288
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
5289
                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
5290
            ])
5291
        ) {
5292
            $show_only_score = true;
5293
            $show_results = true;
5294
            $numberAttempts = 0;
5295
            if ($objExercise->attempts > 0) {
5296
                $attempts = Event::getExerciseResultsByUser(
5297
                    api_get_user_id(),
5298
                    $objExercise->iid,
5299
                    $courseId,
5300
                    $sessionId,
5301
                    $exercise_stat_info['orig_lp_id'],
5302
                    $exercise_stat_info['orig_lp_item_id'],
5303
                    'desc'
5304
                );
5305
                if ($attempts) {
5306
                    $numberAttempts = count($attempts);
5307
                }
5308
5309
                if ($save_user_result) {
5310
                    $numberAttempts++;
5311
                }
5312
5313
                $showTotalScore = false;
5314
                if ($objExercise->results_disabled == RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT) {
5315
                    $showTotalScore = true;
5316
                }
5317
                $showTotalScoreAndUserChoicesInLastAttempt = false;
5318
                if ($numberAttempts >= $objExercise->attempts) {
5319
                    $showTotalScore = true;
5320
                    $show_results = true;
5321
                    $show_only_score = false;
5322
                    $showTotalScoreAndUserChoicesInLastAttempt = true;
5323
                }
5324
5325
                if ($objExercise->results_disabled == RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK) {
5326
                    $showTotalScore = true;
5327
                    $show_results = true;
5328
                    $show_only_score = false;
5329
                    $showTotalScoreAndUserChoicesInLastAttempt = false;
5330
                    if ($numberAttempts >= $objExercise->attempts) {
5331
                        $showTotalScoreAndUserChoicesInLastAttempt = true;
5332
                    }
5333
5334
                    // Check if the current attempt is the last.
5335
                    /*if (false === $save_user_result && !empty($attempts)) {
5336
                        $showTotalScoreAndUserChoicesInLastAttempt = false;
5337
                        $position = 1;
5338
                        foreach ($attempts as $attempt) {
5339
                            if ($exeId == $attempt['exe_id']) {
5340
                                break;
5341
                            }
5342
                            $position++;
5343
                        }
5344
5345
                        if ($position == $objExercise->attempts) {
5346
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
5347
                        }
5348
                    }*/
5349
                }
5350
            }
5351
5352
            if ($objExercise->results_disabled ==
5353
                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK
5354
            ) {
5355
                $show_only_score = false;
5356
                $show_results = true;
5357
                $show_all_but_expected_answer = false;
5358
                $showTotalScore = false;
5359
                $showQuestionScore = false;
5360
                if ($numberAttempts >= $objExercise->attempts) {
5361
                    $showTotalScore = true;
5362
                    $showQuestionScore = true;
5363
                }
5364
            }
5365
        }
5366
5367
        // When exporting to PDF hide feedback/comment/score show warning in hotspot.
5368
        if ($allowExportPdf && $isExport) {
5369
            $showTotalScore = false;
5370
            $showQuestionScore = false;
5371
            $objExercise->feedback_type = 2;
5372
            $objExercise->hideComment = true;
5373
            $objExercise->hideNoAnswer = true;
5374
            $objExercise->results_disabled = 0;
5375
            $objExercise->hideExpectedAnswer = true;
5376
            $show_results = true;
5377
        }
5378
5379
        if ('embeddable' !== $origin &&
5380
            !empty($exercise_stat_info['exe_user_id']) &&
5381
            !empty($studentInfo)
5382
        ) {
5383
            // Shows exercise header.
5384
            echo $objExercise->showExerciseResultHeader(
5385
                $studentInfo,
5386
                $exercise_stat_info,
5387
                $save_user_result,
5388
                $allowSignature,
5389
                $allowExportPdf
5390
            );
5391
        }
5392
5393
        $question_list_answers = [];
5394
        $category_list = [];
5395
        $loadChoiceFromSession = false;
5396
        $fromDatabase = true;
5397
        $exerciseResult = null;
5398
        $exerciseResultCoordinates = null;
5399
        $delineationResults = null;
5400
        if (true === $save_user_result && in_array(
5401
            $objExercise->getFeedbackType(),
5402
            [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
5403
        )) {
5404
            $loadChoiceFromSession = true;
5405
            $fromDatabase = false;
5406
            $exerciseResult = Session::read('exerciseResult');
5407
            $exerciseResultCoordinates = Session::read('exerciseResultCoordinates');
5408
            $delineationResults = Session::read('hotspot_delineation_result');
5409
            $delineationResults = $delineationResults[$objExercise->iid] ?? null;
5410
        }
5411
5412
        $countPendingQuestions = 0;
5413
        $result = [];
5414
        // Loop over all question to show results for each of them, one by one
5415
        if (!empty($question_list)) {
5416
            foreach ($question_list as $questionId) {
5417
                // Creates a temporary Question object
5418
                $objQuestionTmp = Question::read($questionId, $objExercise->course);
5419
                // This variable came from exercise_submit_modal.php
5420
                ob_start();
5421
                $choice = null;
5422
                $delineationChoice = null;
5423
                if ($loadChoiceFromSession) {
5424
                    $choice = $exerciseResult[$questionId] ?? null;
5425
                    $delineationChoice = $delineationResults[$questionId] ?? null;
5426
                }
5427
5428
                // We're inside *one* question. Go through each possible answer for this question
5429
                $result = $objExercise->manage_answer(
5430
                    $exeId,
5431
                    $questionId,
5432
                    $choice,
5433
                    'exercise_result',
5434
                    $exerciseResultCoordinates,
5435
                    $save_user_result,
5436
                    $fromDatabase,
5437
                    $show_results,
5438
                    $objExercise->selectPropagateNeg(),
5439
                    $delineationChoice,
5440
                    $showTotalScoreAndUserChoicesInLastAttempt
5441
                );
5442
5443
                if (empty($result)) {
5444
                    continue;
5445
                }
5446
5447
                $total_score += (float) $result['score'];
5448
                $total_weight += (float) $result['weight'];
5449
5450
                $question_list_answers[] = [
5451
                    'question' => $result['open_question'],
5452
                    'answer' => $result['open_answer'],
5453
                    'answer_type' => $result['answer_type'],
5454
                    'generated_oral_file' => $result['generated_oral_file'],
5455
                ];
5456
5457
                $my_total_score = $result['score'];
5458
                $my_total_weight = $result['weight'];
5459
                $scorePassed = self::scorePassed($my_total_score, $my_total_weight);
5460
5461
                // Category report
5462
                $category_was_added_for_this_test = false;
5463
                if (!empty($objQuestionTmp->category)) {
5464
                    if (!isset($category_list[$objQuestionTmp->category]['score'])) {
5465
                        $category_list[$objQuestionTmp->category]['score'] = 0;
5466
                    }
5467
                    if (!isset($category_list[$objQuestionTmp->category]['total'])) {
5468
                        $category_list[$objQuestionTmp->category]['total'] = 0;
5469
                    }
5470
                    if (!isset($category_list[$objQuestionTmp->category]['total_questions'])) {
5471
                        $category_list[$objQuestionTmp->category]['total_questions'] = 0;
5472
                    }
5473
                    if (!isset($category_list[$objQuestionTmp->category]['passed'])) {
5474
                        $category_list[$objQuestionTmp->category]['passed'] = 0;
5475
                    }
5476
                    if (!isset($category_list[$objQuestionTmp->category]['wrong'])) {
5477
                        $category_list[$objQuestionTmp->category]['wrong'] = 0;
5478
                    }
5479
                    if (!isset($category_list[$objQuestionTmp->category]['no_answer'])) {
5480
                        $category_list[$objQuestionTmp->category]['no_answer'] = 0;
5481
                    }
5482
5483
                    $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
5484
                    $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
5485
                    if ($scorePassed) {
5486
                        // Only count passed if score is not empty
5487
                        if (!empty($my_total_score)) {
5488
                            $category_list[$objQuestionTmp->category]['passed']++;
5489
                        }
5490
                    } else {
5491
                        if ($result['user_answered']) {
5492
                            $category_list[$objQuestionTmp->category]['wrong']++;
5493
                        } else {
5494
                            $category_list[$objQuestionTmp->category]['no_answer']++;
5495
                        }
5496
                    }
5497
5498
                    $category_list[$objQuestionTmp->category]['total_questions']++;
5499
                    $category_was_added_for_this_test = true;
5500
                }
5501
                if (!empty($objQuestionTmp->category_list)) {
5502
                    foreach ($objQuestionTmp->category_list as $category_id) {
5503
                        $category_list[$category_id]['score'] += $my_total_score;
5504
                        $category_list[$category_id]['total'] += $my_total_weight;
5505
                        $category_was_added_for_this_test = true;
5506
                    }
5507
                }
5508
5509
                // No category for this question!
5510
                if (!$category_was_added_for_this_test) {
5511
                    if (!isset($category_list['none']['score'])) {
5512
                        $category_list['none']['score'] = 0;
5513
                    }
5514
                    if (!isset($category_list['none']['total'])) {
5515
                        $category_list['none']['total'] = 0;
5516
                    }
5517
5518
                    $category_list['none']['score'] += $my_total_score;
5519
                    $category_list['none']['total'] += $my_total_weight;
5520
                }
5521
5522
                if ($objExercise->selectPropagateNeg() == 0 && $my_total_score < 0) {
5523
                    $my_total_score = 0;
5524
                }
5525
5526
                $comnt = null;
5527
                if ($show_results) {
5528
                    $comnt = Event::get_comments($exeId, $questionId);
5529
                    $teacherAudio = self::getOralFeedbackAudio(
5530
                        $exeId,
5531
                        $questionId,
5532
                        api_get_user_id()
5533
                    );
5534
5535
                    if (!empty($comnt) || $teacherAudio) {
5536
                        echo '<b>'.get_lang('Feedback').'</b>';
5537
                    }
5538
5539
                    if (!empty($comnt)) {
5540
                        echo self::getFeedbackText($comnt);
5541
                    }
5542
5543
                    if ($teacherAudio) {
5544
                        echo $teacherAudio;
5545
                    }
5546
                }
5547
5548
                $calculatedScore = [
5549
                    'result' => self::show_score(
5550
                        $my_total_score,
5551
                        $my_total_weight,
5552
                        false
5553
                    ),
5554
                    'pass' => $scorePassed,
5555
                    'score' => $my_total_score,
5556
                    'weight' => $my_total_weight,
5557
                    'comments' => $comnt,
5558
                    'user_answered' => $result['user_answered'],
5559
                ];
5560
5561
                $score = [];
5562
                if ($show_results) {
5563
                    $score = $calculatedScore;
5564
                }
5565
                if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC])) {
5566
                    $reviewScore = [
5567
                        'score' => $my_total_score,
5568
                        'comments' => Event::get_comments($exeId, $questionId),
5569
                    ];
5570
                    $check = $objQuestionTmp->isQuestionWaitingReview($reviewScore);
5571
                    if (false === $check) {
5572
                        $countPendingQuestions++;
5573
                    }
5574
                }
5575
5576
                $contents = ob_get_clean();
5577
5578
                // Hide correct answers.
5579
                if ($scorePassed && false === $objExercise->disableHideCorrectAnsweredQuestions) {
5580
                    // Skip correct answers.
5581
                    $hide = (int) $objExercise->getPageConfigurationAttribute('hide_correct_answered_questions');
5582
                    if (1 === $hide) {
5583
                        continue;
5584
                    }
5585
                }
5586
5587
                $question_content = '';
5588
                if ($show_results) {
5589
                    $question_content = '<div class="question_row_answer">';
5590
                    if (false === $showQuestionScore) {
5591
                        $score = [];
5592
                    }
5593
5594
                    // Shows question title an description
5595
                    $question_content .= $objQuestionTmp->return_header(
5596
                        $objExercise,
5597
                        $counter,
5598
                        $score
5599
                    );
5600
                }
5601
                $counter++;
5602
                $question_content .= $contents;
5603
                if ($show_results) {
5604
                    $question_content .= '</div>';
5605
                }
5606
5607
                $calculatedScore['question_content'] = $question_content;
5608
                $attemptResult[] = $calculatedScore;
5609
5610
                if ($objExercise->showExpectedChoice()) {
5611
                    $exercise_content .= Display::div(
5612
                        Display::panel($question_content),
5613
                        ['class' => 'question-panel']
5614
                    );
5615
                } else {
5616
                    // $show_all_but_expected_answer should not happen at
5617
                    // the same time as $show_results
5618
                    if ($show_results && !$show_only_score) {
5619
                        $exercise_content .= Display::div(
5620
                            Display::panel($question_content),
5621
                            ['class' => 'question-panel']
5622
                        );
5623
                    }
5624
                }
5625
            }
5626
        }
5627
5628
        // Display text when test is finished #4074 and for LP #4227
5629
        // Allows to do a remove_XSS for end text result of exercise with
5630
        // user status COURSEMANAGERLOWSECURITY BT#20194
5631
        $finishMessage = $objExercise->getFinishText($total_score, $total_weight);
5632
        if (true === api_get_configuration_value('exercise_result_end_text_html_strict_filtering')) {
5633
            $endOfMessage = Security::remove_XSS($finishMessage, COURSEMANAGERLOWSECURITY);
5634
        } else {
5635
            $endOfMessage = Security::remove_XSS($finishMessage);
5636
        }
5637
        if (!empty($endOfMessage)) {
5638
            echo Display::div(
5639
                $endOfMessage,
5640
                ['id' => 'quiz_end_message']
5641
            );
5642
        }
5643
5644
        $totalScoreText = null;
5645
        $certificateBlock = '';
5646
        if (($show_results || $show_only_score) && $showTotalScore) {
5647
            if ($result['answer_type'] == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
5648
                echo '<h1 style="text-align : center; margin : 20px 0;">'.get_lang('YourResults').'</h1><br />';
5649
            }
5650
            $totalScoreText .= '<div class="question_row_score">';
5651
            if ($result['answer_type'] == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
5652
                $totalScoreText .= self::getQuestionDiagnosisRibbon(
5653
                    $objExercise,
5654
                    $total_score,
5655
                    $total_weight,
5656
                    true
5657
                );
5658
            } else {
5659
                $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
5660
                if ('true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)) {
5661
                    $formula = $pluginEvaluation->getFormulaForExercise($objExercise->selectId());
5662
5663
                    if (!empty($formula)) {
5664
                        $total_score = $pluginEvaluation->getResultWithFormula($exeId, $formula);
5665
                        $total_weight = $pluginEvaluation->getMaxScore();
5666
                    }
5667
                }
5668
5669
                $totalScoreText .= self::getTotalScoreRibbon(
5670
                    $objExercise,
5671
                    $total_score,
5672
                    $total_weight,
5673
                    true,
5674
                    $countPendingQuestions
5675
                );
5676
            }
5677
            $totalScoreText .= '</div>';
5678
5679
            if (!empty($studentInfo)) {
5680
                $certificateBlock = self::generateAndShowCertificateBlock(
5681
                    $total_score,
5682
                    $total_weight,
5683
                    $objExercise,
5684
                    $studentInfo['id'],
5685
                    $courseCode,
5686
                    $sessionId
5687
                );
5688
            }
5689
        }
5690
5691
        if ($result['answer_type'] == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
5692
            $chartMultiAnswer = MultipleAnswerTrueFalseDegreeCertainty::displayStudentsChartResults(
5693
                $exeId,
5694
                $objExercise
5695
            );
5696
            echo $chartMultiAnswer;
5697
        }
5698
5699
        if (!empty($category_list) &&
5700
            ($show_results || $show_only_score || RESULT_DISABLE_RADAR == $objExercise->results_disabled)
5701
        ) {
5702
            // Adding total
5703
            $category_list['total'] = [
5704
                'score' => $total_score,
5705
                'total' => $total_weight,
5706
            ];
5707
            echo TestCategory::get_stats_table_by_attempt($objExercise, $category_list);
5708
        }
5709
5710
        if ($show_all_but_expected_answer) {
5711
            $exercise_content .= Display::return_message(get_lang('ExerciseWithFeedbackWithoutCorrectionComment'));
5712
        }
5713
5714
        // Remove audio auto play from questions on results page - refs BT#7939
5715
        $exercise_content = preg_replace(
5716
            ['/autoplay[\=\".+\"]+/', '/autostart[\=\".+\"]+/'],
5717
            '',
5718
            $exercise_content
5719
        );
5720
5721
        echo $totalScoreText;
5722
        echo $certificateBlock;
5723
5724
        // Ofaj change BT#11784
5725
        if (api_get_configuration_value('quiz_show_description_on_results_page') &&
5726
            !empty($objExercise->description)
5727
        ) {
5728
            echo Display::div(Security::remove_XSS($objExercise->description), ['class' => 'exercise_description']);
5729
        }
5730
5731
        echo $exercise_content;
5732
        if (!$show_only_score) {
5733
            echo $totalScoreText;
5734
        }
5735
5736
        if ($save_user_result) {
5737
            // Tracking of results
5738
            if ($exercise_stat_info) {
5739
                $learnpath_id = $exercise_stat_info['orig_lp_id'];
5740
                $learnpath_item_id = $exercise_stat_info['orig_lp_item_id'];
5741
                $learnpath_item_view_id = $exercise_stat_info['orig_lp_item_view_id'];
5742
5743
                if (api_is_allowed_to_session_edit()) {
5744
                    Event::updateEventExercise(
5745
                        $exercise_stat_info['exe_id'],
5746
                        $objExercise->selectId(),
5747
                        $total_score,
5748
                        $total_weight,
5749
                        $sessionId,
5750
                        $learnpath_id,
5751
                        $learnpath_item_id,
5752
                        $learnpath_item_view_id,
5753
                        $exercise_stat_info['exe_duration'],
5754
                        $question_list
5755
                    );
5756
5757
                    $allowStats = api_get_configuration_value('allow_gradebook_stats');
5758
                    if ($allowStats) {
5759
                        $objExercise->generateStats(
5760
                            $objExercise->selectId(),
5761
                            api_get_course_info(),
5762
                            $sessionId
5763
                        );
5764
                    }
5765
                }
5766
            }
5767
5768
            // Send notification at the end
5769
            if (!api_is_allowed_to_edit(null, true) &&
5770
                !api_is_excluded_user_type()
5771
            ) {
5772
                $objExercise->send_mail_notification_for_exam(
5773
                    'end',
5774
                    $question_list_answers,
5775
                    $origin,
5776
                    $exeId,
5777
                    $total_score,
5778
                    $total_weight
5779
                );
5780
            }
5781
        }
5782
5783
        if (in_array(
5784
            $objExercise->selectResultsDisabled(),
5785
            [RESULT_DISABLE_RANKING, RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING]
5786
        )) {
5787
            echo Display::page_header(get_lang('Ranking'), null, 'h4');
5788
            echo self::displayResultsInRanking(
5789
                $objExercise,
5790
                api_get_user_id(),
5791
                $courseId,
5792
                $sessionId
5793
            );
5794
        }
5795
5796
        if (!empty($remainingMessage)) {
5797
            echo Display::return_message($remainingMessage, 'normal', false);
5798
        }
5799
5800
        $failedAnswersCount = 0;
5801
        $wrongQuestionHtml = '';
5802
        $all = '';
5803
        foreach ($attemptResult as $item) {
5804
            if (false === $item['pass']) {
5805
                $failedAnswersCount++;
5806
                $wrongQuestionHtml .= $item['question_content'].'<br />';
5807
            }
5808
            $all .= $item['question_content'].'<br />';
5809
        }
5810
5811
        $passed = self::isPassPercentageAttemptPassed(
5812
            $objExercise,
5813
            $total_score,
5814
            $total_weight
5815
        );
5816
5817
        if ($save_user_result
5818
            && !$passed
5819
            && true === api_get_configuration_value('exercise_subscribe_session_when_finished_failure')
5820
        ) {
5821
            self::subscribeSessionWhenFinishedFailure($objExercise->iid);
5822
        }
5823
5824
        $percentage = 0;
5825
        if (!empty($total_weight)) {
5826
            $percentage = ($total_score / $total_weight) * 100;
5827
        }
5828
5829
        return [
5830
            'category_list' => $category_list,
5831
            'attempts_result_list' => $attemptResult, // array of results
5832
            'exercise_passed' => $passed, // boolean
5833
            'total_answers_count' => count($attemptResult), // int
5834
            'failed_answers_count' => $failedAnswersCount, // int
5835
            'failed_answers_html' => $wrongQuestionHtml,
5836
            'all_answers_html' => $all,
5837
            'total_score' => $total_score,
5838
            'total_weight' => $total_weight,
5839
            'total_percentage' => $percentage,
5840
            'count_pending_questions' => $countPendingQuestions,
5841
        ];
5842
    }
5843
5844
    public static function getSessionWhenFinishedFailure(int $exerciseId): ?SessionEntity
5845
    {
5846
        $objExtraField = new ExtraField('exercise');
5847
        $objExtraFieldValue = new ExtraFieldValue('exercise');
5848
5849
        $subsSessionWhenFailureField = $objExtraField->get_handler_field_info_by_field_variable(
5850
            'subscribe_session_when_finished_failure'
5851
        );
5852
        $subsSessionWhenFailureValue = $objExtraFieldValue->get_values_by_handler_and_field_id(
5853
            $exerciseId,
5854
            $subsSessionWhenFailureField['id']
5855
        );
5856
5857
        if (!empty($subsSessionWhenFailureValue['value'])) {
5858
            return api_get_session_entity((int) $subsSessionWhenFailureValue['value']);
5859
        }
5860
5861
        return null;
5862
    }
5863
5864
    /**
5865
     * It validates unique score when all user answers are correct by question.
5866
     * It is used for global questions.
5867
     *
5868
     * @param       $answerType
5869
     * @param       $listCorrectAnswers
5870
     * @param       $exeId
5871
     * @param       $questionId
5872
     * @param       $questionWeighting
5873
     * @param array $choice
5874
     * @param int   $nbrAnswers
5875
     *
5876
     * @return int|mixed
5877
     */
5878
    public static function getUserQuestionScoreGlobal(
5879
        $answerType,
5880
        $listCorrectAnswers,
5881
        $exeId,
5882
        $questionId,
5883
        $questionWeighting,
5884
        $choice = [],
5885
        $nbrAnswers = 0
5886
    ) {
5887
        $nbrCorrect = 0;
5888
        $nbrOptions = 0;
5889
        switch ($answerType) {
5890
            case FILL_IN_BLANKS_COMBINATION:
5891
                if (!empty($listCorrectAnswers)) {
5892
                    foreach ($listCorrectAnswers['student_score'] as $idx => $val) {
5893
                        if (1 === (int) $val) {
5894
                            $nbrCorrect++;
5895
                        }
5896
                    }
5897
                    $nbrOptions = (int) $listCorrectAnswers['words_count'];
5898
                }
5899
                break;
5900
            case HOT_SPOT_COMBINATION:
5901
                if (!empty($listCorrectAnswers)) {
5902
                    foreach ($listCorrectAnswers as $idx => $val) {
5903
                        if (1 === (int) $choice[$idx]) {
5904
                            $nbrCorrect++;
5905
                        }
5906
                    }
5907
                } else {
5908
                    // We get the user answers from database
5909
                    $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
5910
                    $sql = "SELECT count(hotspot_id) as ct
5911
                                FROM $TBL_TRACK_HOTSPOT
5912
                                WHERE
5913
                                    hotspot_exe_id = '".Database::escape_string($exeId)."' AND
5914
                                    hotspot_question_id = '".Database::escape_string($questionId)."' AND
5915
                                    hotspot_correct = 1";
5916
                    $result = Database::query($sql);
5917
                    $nbrCorrect = (int) Database::result($result, 0, 0);
5918
                }
5919
                $nbrOptions = $nbrAnswers;
5920
                break;
5921
            case MATCHING_COMBINATION:
5922
            case MATCHING_DRAGGABLE_COMBINATION:
5923
                if (isset($listCorrectAnswers['form_values'])) {
5924
                    if (isset($listCorrectAnswers['form_values']['correct'])) {
5925
                        $nbrCorrect = count($listCorrectAnswers['form_values']['correct']);
5926
                        $nbrOptions = (int) $listCorrectAnswers['form_values']['count_options'];
5927
                    }
5928
                } else {
5929
                    if (isset($listCorrectAnswers['from_database'])) {
5930
                        if (isset($listCorrectAnswers['from_database']['correct'])) {
5931
                            $nbrCorrect = count($listCorrectAnswers['from_database']['correct']);
5932
                            $nbrOptions = (int) $listCorrectAnswers['from_database']['count_options'];
5933
                        }
5934
                    }
5935
                }
5936
                break;
5937
        }
5938
5939
        $questionScore = 0;
5940
        if ($nbrCorrect > 0 && $nbrCorrect == $nbrOptions) {
5941
            $questionScore = $questionWeighting;
5942
        }
5943
5944
        return $questionScore;
5945
    }
5946
5947
    /**
5948
     * Display the ranking of results in a exercise.
5949
     *
5950
     * @param Exercise $exercise
5951
     * @param int      $currentUserId
5952
     * @param int      $courseId
5953
     * @param int      $sessionId
5954
     *
5955
     * @return string
5956
     */
5957
    public static function displayResultsInRanking($exercise, $currentUserId, $courseId, $sessionId = 0)
5958
    {
5959
        $exerciseId = $exercise->iid;
5960
        $data = self::exerciseResultsInRanking($exerciseId, $courseId, $sessionId);
5961
5962
        $table = new HTML_Table(['class' => 'table table-hover table-striped table-bordered']);
5963
        $table->setHeaderContents(0, 0, get_lang('Position'), ['class' => 'text-right']);
5964
        $table->setHeaderContents(0, 1, get_lang('Username'));
5965
        $table->setHeaderContents(0, 2, get_lang('Score'), ['class' => 'text-right']);
5966
        $table->setHeaderContents(0, 3, get_lang('Date'), ['class' => 'text-center']);
5967
5968
        foreach ($data as $r => $item) {
5969
            if (!isset($item[1])) {
5970
                continue;
5971
            }
5972
            $selected = $item[1]->getId() == $currentUserId;
5973
5974
            foreach ($item as $c => $value) {
5975
                $table->setCellContents($r + 1, $c, $value);
5976
5977
                $attrClass = '';
5978
5979
                if (in_array($c, [0, 2])) {
5980
                    $attrClass = 'text-right';
5981
                } elseif (3 == $c) {
5982
                    $attrClass = 'text-center';
5983
                }
5984
5985
                if ($selected) {
5986
                    $attrClass .= ' warning';
5987
                }
5988
5989
                $table->setCellAttributes($r + 1, $c, ['class' => $attrClass]);
5990
            }
5991
        }
5992
5993
        return $table->toHtml();
5994
    }
5995
5996
    /**
5997
     * Get the ranking for results in a exercise.
5998
     * Function used internally by ExerciseLib::displayResultsInRanking.
5999
     *
6000
     * @param int $exerciseId
6001
     * @param int $courseId
6002
     * @param int $sessionId
6003
     *
6004
     * @return array
6005
     */
6006
    public static function exerciseResultsInRanking($exerciseId, $courseId, $sessionId = 0)
6007
    {
6008
        $em = Database::getManager();
6009
6010
        $dql = 'SELECT DISTINCT te.exeUserId FROM ChamiloCoreBundle:TrackEExercises te WHERE te.exeExoId = :id AND te.cId = :cId';
6011
        $dql .= api_get_session_condition($sessionId, true, false, 'te.sessionId');
6012
6013
        $result = $em
6014
            ->createQuery($dql)
6015
            ->setParameters(['id' => $exerciseId, 'cId' => $courseId])
6016
            ->getScalarResult();
6017
6018
        $data = [];
6019
        /** @var TrackEExercises $item */
6020
        foreach ($result as $item) {
6021
            $data[] = self::get_best_attempt_by_user($item['exeUserId'], $exerciseId, $courseId, $sessionId);
6022
        }
6023
6024
        usort(
6025
            $data,
6026
            function ($a, $b) {
6027
                if ($a['exe_result'] != $b['exe_result']) {
6028
                    return $a['exe_result'] > $b['exe_result'] ? -1 : 1;
6029
                }
6030
6031
                if ($a['exe_date'] != $b['exe_date']) {
6032
                    return $a['exe_date'] < $b['exe_date'] ? -1 : 1;
6033
                }
6034
6035
                return 0;
6036
            }
6037
        );
6038
6039
        // flags to display the same position in case of tie
6040
        $lastScore = $data[0]['exe_result'];
6041
        $position = 1;
6042
        $data = array_map(
6043
            function ($item) use (&$lastScore, &$position) {
6044
                if ($item['exe_result'] < $lastScore) {
6045
                    $position++;
6046
                }
6047
6048
                $lastScore = $item['exe_result'];
6049
6050
                return [
6051
                    $position,
6052
                    api_get_user_entity($item['exe_user_id']),
6053
                    self::show_score($item['exe_result'], $item['exe_weighting'], true, true, true),
6054
                    api_convert_and_format_date($item['exe_date'], DATE_TIME_FORMAT_SHORT),
6055
                ];
6056
            },
6057
            $data
6058
        );
6059
6060
        return $data;
6061
    }
6062
6063
    /**
6064
     * Get a special ribbon on top of "degree of certainty" questions (
6065
     * variation from getTotalScoreRibbon() for other question types).
6066
     *
6067
     * @param Exercise $objExercise
6068
     * @param float    $score
6069
     * @param float    $weight
6070
     * @param bool     $checkPassPercentage
6071
     *
6072
     * @return string
6073
     */
6074
    public static function getQuestionDiagnosisRibbon($objExercise, $score, $weight, $checkPassPercentage = false)
6075
    {
6076
        $displayChartDegree = true;
6077
        $ribbon = $displayChartDegree ? '<div class="ribbon">' : '';
6078
6079
        if ($checkPassPercentage) {
6080
            $passPercentage = $objExercise->selectPassPercentage();
6081
            $isSuccess = self::isSuccessExerciseResult($score, $weight, $passPercentage);
6082
            // Color the final test score if pass_percentage activated
6083
            $ribbonTotalSuccessOrError = '';
6084
            if (self::isPassPercentageEnabled($passPercentage)) {
6085
                if ($isSuccess) {
6086
                    $ribbonTotalSuccessOrError = ' ribbon-total-success';
6087
                } else {
6088
                    $ribbonTotalSuccessOrError = ' ribbon-total-error';
6089
                }
6090
            }
6091
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total '.$ribbonTotalSuccessOrError.'">' : '';
6092
        } else {
6093
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total">' : '';
6094
        }
6095
6096
        if ($displayChartDegree) {
6097
            $ribbon .= '<h3>'.get_lang('YourTotalScore').':&nbsp;';
6098
            $ribbon .= self::show_score($score, $weight, false, true);
6099
            $ribbon .= '</h3>';
6100
            $ribbon .= '</div>';
6101
        }
6102
6103
        if ($checkPassPercentage) {
6104
            $ribbon .= self::showSuccessMessage(
6105
                $score,
6106
                $weight,
6107
                $objExercise->selectPassPercentage()
6108
            );
6109
        }
6110
6111
        $ribbon .= $displayChartDegree ? '</div>' : '';
6112
6113
        return $ribbon;
6114
    }
6115
6116
    public static function isPassPercentageAttemptPassed($objExercise, $score, $weight)
6117
    {
6118
        $passPercentage = $objExercise->selectPassPercentage();
6119
6120
        return self::isSuccessExerciseResult($score, $weight, $passPercentage);
6121
    }
6122
6123
    /**
6124
     * @param float $score
6125
     * @param float $weight
6126
     * @param bool  $checkPassPercentage
6127
     * @param int   $countPendingQuestions
6128
     *
6129
     * @return string
6130
     */
6131
    public static function getTotalScoreRibbon(
6132
        Exercise $objExercise,
6133
        $score,
6134
        $weight,
6135
        $checkPassPercentage = false,
6136
        $countPendingQuestions = 0
6137
    ) {
6138
        $hide = (int) $objExercise->getPageConfigurationAttribute('hide_total_score');
6139
        if (1 === $hide) {
6140
            return '';
6141
        }
6142
6143
        $passPercentage = $objExercise->selectPassPercentage();
6144
        $ribbon = '<div class="title-score">';
6145
        if ($checkPassPercentage) {
6146
            $isSuccess = self::isSuccessExerciseResult(
6147
                $score,
6148
                $weight,
6149
                $passPercentage
6150
            );
6151
            // Color the final test score if pass_percentage activated
6152
            $class = '';
6153
            if (self::isPassPercentageEnabled($passPercentage)) {
6154
                if ($isSuccess) {
6155
                    $class = ' ribbon-total-success';
6156
                } else {
6157
                    $class = ' ribbon-total-error';
6158
                }
6159
            }
6160
            $ribbon .= '<div class="total '.$class.'">';
6161
        } else {
6162
            $ribbon .= '<div class="total">';
6163
        }
6164
        $ribbon .= '<h3>'.get_lang('YourTotalScore').':&nbsp;';
6165
        $ribbon .= self::show_score($score, $weight, false, true);
6166
        $ribbon .= '</h3>';
6167
        $ribbon .= '</div>';
6168
        if ($checkPassPercentage) {
6169
            $ribbon .= self::showSuccessMessage(
6170
                $score,
6171
                $weight,
6172
                $passPercentage
6173
            );
6174
        }
6175
        $ribbon .= '</div>';
6176
6177
        if (!empty($countPendingQuestions)) {
6178
            $ribbon .= '<br />';
6179
            $ribbon .= Display::return_message(
6180
                sprintf(
6181
                    get_lang('TempScoreXQuestionsNotCorrectedYet'),
6182
                    $countPendingQuestions
6183
                ),
6184
                'warning'
6185
            );
6186
        }
6187
6188
        return $ribbon;
6189
    }
6190
6191
    /**
6192
     * @param int $countLetter
6193
     *
6194
     * @return mixed
6195
     */
6196
    public static function detectInputAppropriateClass($countLetter)
6197
    {
6198
        $limits = [
6199
            0 => 'input-mini',
6200
            10 => 'input-mini',
6201
            15 => 'input-medium',
6202
            20 => 'input-xlarge',
6203
            40 => 'input-xlarge',
6204
            60 => 'input-xxlarge',
6205
            100 => 'input-xxlarge',
6206
            200 => 'input-xxlarge',
6207
        ];
6208
6209
        foreach ($limits as $size => $item) {
6210
            if ($countLetter <= $size) {
6211
                return $item;
6212
            }
6213
        }
6214
6215
        return $limits[0];
6216
    }
6217
6218
    /**
6219
     * @param int    $senderId
6220
     * @param array  $course_info
6221
     * @param string $test
6222
     * @param string $url
6223
     *
6224
     * @return string
6225
     */
6226
    public static function getEmailNotification($senderId, $course_info, $test, $url)
6227
    {
6228
        $teacher_info = api_get_user_info($senderId);
6229
        $from_name = api_get_person_name(
6230
            $teacher_info['firstname'],
6231
            $teacher_info['lastname'],
6232
            null,
6233
            PERSON_NAME_EMAIL_ADDRESS
6234
        );
6235
6236
        $view = new Template('', false, false, false, false, false, false);
6237
        $view->assign('course_title', Security::remove_XSS($course_info['name']));
6238
        $view->assign('test_title', Security::remove_XSS($test));
6239
        $view->assign('url', $url);
6240
        $view->assign('teacher_name', $from_name);
6241
        $template = $view->get_template('mail/exercise_result_alert_body.tpl');
6242
6243
        return $view->fetch($template);
6244
    }
6245
6246
    /**
6247
     * @return string
6248
     */
6249
    public static function getNotCorrectedYetText()
6250
    {
6251
        return Display::return_message(get_lang('notCorrectedYet'), 'warning');
6252
    }
6253
6254
    /**
6255
     * @param string $message
6256
     *
6257
     * @return string
6258
     */
6259
    public static function getFeedbackText($message)
6260
    {
6261
        $message = Security::remove_XSS($message);
6262
6263
        return Display::return_message($message, 'warning', false);
6264
    }
6265
6266
    /**
6267
     * Get the recorder audio component for save a teacher audio feedback.
6268
     *
6269
     * @param Template $template
6270
     * @param int      $attemptId
6271
     * @param int      $questionId
6272
     * @param int      $userId
6273
     *
6274
     * @return string
6275
     */
6276
    public static function getOralFeedbackForm($template, $attemptId, $questionId, $userId)
6277
    {
6278
        $template->assign('user_id', $userId);
6279
        $template->assign('question_id', $questionId);
6280
        $template->assign('directory', "/../exercises/teacher_audio/$attemptId/");
6281
        $template->assign('file_name', "{$questionId}_{$userId}");
6282
6283
        return $template->fetch($template->get_template('exercise/oral_expression.tpl'));
6284
    }
6285
6286
    /**
6287
     * Get the audio componen for a teacher audio feedback.
6288
     *
6289
     * @param int $attemptId
6290
     * @param int $questionId
6291
     * @param int $userId
6292
     *
6293
     * @return string
6294
     */
6295
    public static function getOralFeedbackAudio($attemptId, $questionId, $userId)
6296
    {
6297
        $courseInfo = api_get_course_info();
6298
        $sessionId = api_get_session_id();
6299
        $groupId = api_get_group_id();
6300
        $sysCourseDir = api_get_path(SYS_COURSE_PATH).$courseInfo['path'];
6301
        $webCourseDir = api_get_path(WEB_COURSE_PATH).$courseInfo['path'];
6302
        $fileName = "{$questionId}_{$userId}".DocumentManager::getDocumentSuffix($courseInfo, $sessionId, $groupId);
6303
        $filePath = null;
6304
6305
        $relFilePath = "/exercises/teacher_audio/$attemptId/$fileName";
6306
6307
        if (file_exists($sysCourseDir.$relFilePath.'.ogg')) {
6308
            $filePath = $webCourseDir.$relFilePath.'.ogg';
6309
        } elseif (file_exists($sysCourseDir.$relFilePath.'.wav.wav')) {
6310
            $filePath = $webCourseDir.$relFilePath.'.wav.wav';
6311
        } elseif (file_exists($sysCourseDir.$relFilePath.'.wav')) {
6312
            $filePath = $webCourseDir.$relFilePath.'.wav';
6313
        }
6314
6315
        if (!$filePath) {
6316
            return '';
6317
        }
6318
6319
        return Display::tag(
6320
            'audio',
6321
            null,
6322
            ['src' => $filePath]
6323
        );
6324
    }
6325
6326
    /**
6327
     * @return array
6328
     */
6329
    public static function getNotificationSettings()
6330
    {
6331
        $emailAlerts = [
6332
            2 => get_lang('SendEmailToTeacherWhenStudentStartQuiz'),
6333
            1 => get_lang('SendEmailToTeacherWhenStudentEndQuiz'), // default
6334
            3 => get_lang('SendEmailToTeacherWhenStudentEndQuizOnlyIfOpenQuestion'),
6335
            4 => get_lang('SendEmailToTeacherWhenStudentEndQuizOnlyIfOralQuestion'),
6336
        ];
6337
6338
        return $emailAlerts;
6339
    }
6340
6341
    /**
6342
     * Get the additional actions added in exercise_additional_teacher_modify_actions configuration.
6343
     *
6344
     * @param int $exerciseId
6345
     * @param int $iconSize
6346
     *
6347
     * @return string
6348
     */
6349
    public static function getAdditionalTeacherActions($exerciseId, $iconSize = ICON_SIZE_SMALL)
6350
    {
6351
        $additionalActions = api_get_configuration_value('exercise_additional_teacher_modify_actions') ?: [];
6352
        $actions = [];
6353
6354
        foreach ($additionalActions as $additionalAction) {
6355
            $actions[] = call_user_func(
6356
                $additionalAction,
6357
                $exerciseId,
6358
                $iconSize
6359
            );
6360
        }
6361
6362
        return implode(PHP_EOL, $actions);
6363
    }
6364
6365
    /**
6366
     * @param int $userId
6367
     * @param int $courseId
6368
     * @param int $sessionId
6369
     *
6370
     * @throws \Doctrine\ORM\Query\QueryException
6371
     *
6372
     * @return int
6373
     */
6374
    public static function countAnsweredQuestionsByUserAfterTime(DateTime $time, $userId, $courseId, $sessionId)
6375
    {
6376
        $em = Database::getManager();
6377
6378
        $time = api_get_utc_datetime($time->format('Y-m-d H:i:s'), false, true);
6379
6380
        $result = $em
6381
            ->createQuery('
6382
                SELECT COUNT(ea) FROM ChamiloCoreBundle:TrackEAttempt ea
6383
                WHERE ea.userId = :user AND ea.cId = :course AND ea.sessionId = :session
6384
                    AND ea.tms > :time
6385
            ')
6386
            ->setParameters(['user' => $userId, 'course' => $courseId, 'session' => $sessionId, 'time' => $time])
6387
            ->getSingleScalarResult();
6388
6389
        return $result;
6390
    }
6391
6392
    /**
6393
     * @param int $userId
6394
     * @param int $numberOfQuestions
6395
     * @param int $courseId
6396
     * @param int $sessionId
6397
     *
6398
     * @throws \Doctrine\ORM\Query\QueryException
6399
     *
6400
     * @return bool
6401
     */
6402
    public static function isQuestionsLimitPerDayReached($userId, $numberOfQuestions, $courseId, $sessionId)
6403
    {
6404
        $questionsLimitPerDay = (int) api_get_course_setting('quiz_question_limit_per_day');
6405
6406
        if ($questionsLimitPerDay <= 0) {
6407
            return false;
6408
        }
6409
6410
        $midnightTime = ChamiloApi::getServerMidnightTime();
6411
6412
        $answeredQuestionsCount = self::countAnsweredQuestionsByUserAfterTime(
6413
            $midnightTime,
6414
            $userId,
6415
            $courseId,
6416
            $sessionId
6417
        );
6418
6419
        return ($answeredQuestionsCount + $numberOfQuestions) > $questionsLimitPerDay;
6420
    }
6421
6422
    /**
6423
     * By default, allowed types are unique-answer (and image) or multiple-answer questions.
6424
     * Types can be extended by the configuration setting "exercise_embeddable_extra_types".
6425
     */
6426
    public static function getEmbeddableTypes(): array
6427
    {
6428
        $allowedTypes = [
6429
            UNIQUE_ANSWER,
6430
            MULTIPLE_ANSWER,
6431
            FILL_IN_BLANKS,
6432
            MATCHING,
6433
            FREE_ANSWER,
6434
            MULTIPLE_ANSWER_COMBINATION,
6435
            UNIQUE_ANSWER_NO_OPTION,
6436
            MULTIPLE_ANSWER_TRUE_FALSE,
6437
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE,
6438
            ORAL_EXPRESSION,
6439
            GLOBAL_MULTIPLE_ANSWER,
6440
            CALCULATED_ANSWER,
6441
            UNIQUE_ANSWER_IMAGE,
6442
            READING_COMPREHENSION,
6443
            MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY,
6444
            UPLOAD_ANSWER,
6445
            ANSWER_IN_OFFICE_DOC,
6446
            MATCHING_COMBINATION,
6447
            FILL_IN_BLANKS_COMBINATION,
6448
            MULTIPLE_ANSWER_DROPDOWN,
6449
            MULTIPLE_ANSWER_DROPDOWN_COMBINATION,
6450
        ];
6451
        $defaultTypes = [UNIQUE_ANSWER, MULTIPLE_ANSWER, UNIQUE_ANSWER_IMAGE];
6452
        $types = $defaultTypes;
6453
6454
        $extraTypes = api_get_configuration_value('exercise_embeddable_extra_types');
6455
6456
        if (false !== $extraTypes && !empty($extraTypes['types'])) {
6457
            $types = array_merge($defaultTypes, $extraTypes['types']);
6458
        }
6459
6460
        return array_filter(
6461
            array_unique($types),
6462
            function ($type) use ($allowedTypes) {
6463
                return in_array($type, $allowedTypes);
6464
            }
6465
        );
6466
    }
6467
6468
    /**
6469
     * Check if an exercise complies with the requirements to be embedded in the mobile app or a video.
6470
     * By making sure it is set on one question per page, and that the exam does not have immediate feedback,
6471
     * and it only contains allowed types.
6472
     *
6473
     * @see Exercise::getEmbeddableTypes()
6474
     */
6475
    public static function isQuizEmbeddable(array $exercise): bool
6476
    {
6477
        $exercise['iid'] = isset($exercise['iid']) ? (int) $exercise['iid'] : 0;
6478
6479
        if (ONE_PER_PAGE != $exercise['type'] ||
6480
            in_array($exercise['feedback_type'], [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])
6481
        ) {
6482
            return false;
6483
        }
6484
6485
        $questionRepository = Database::getManager()->getRepository(CQuizQuestion::class);
6486
6487
        $countAll = $questionRepository->countQuestionsInExercise($exercise['iid']);
6488
        $countAllowed = $questionRepository->countEmbeddableQuestionsInExercise($exercise['iid']);
6489
6490
        return $countAll === $countAllowed;
6491
    }
6492
6493
    /**
6494
     * Generate a certificate linked to current quiz and.
6495
     * Return the HTML block with links to download and view the certificate.
6496
     *
6497
     * @param float  $totalScore
6498
     * @param float  $totalWeight
6499
     * @param int    $studentId
6500
     * @param string $courseCode
6501
     * @param int    $sessionId
6502
     *
6503
     * @return string
6504
     */
6505
    public static function generateAndShowCertificateBlock(
6506
        $totalScore,
6507
        $totalWeight,
6508
        Exercise $objExercise,
6509
        $studentId,
6510
        $courseCode,
6511
        $sessionId = 0
6512
    ) {
6513
        if (!api_get_configuration_value('quiz_generate_certificate_ending') ||
6514
            !self::isSuccessExerciseResult($totalScore, $totalWeight, $objExercise->selectPassPercentage())
6515
        ) {
6516
            return '';
6517
        }
6518
6519
        /** @var Category $category */
6520
        $category = Category::load(null, null, $courseCode, null, null, $sessionId, 'ORDER By id');
6521
6522
        if (empty($category)) {
6523
            return '';
6524
        }
6525
6526
        /** @var Category $category */
6527
        $category = $category[0];
6528
        $categoryId = $category->get_id();
6529
        $link = LinkFactory::load(
6530
            null,
6531
            null,
6532
            $objExercise->selectId(),
6533
            null,
6534
            $courseCode,
6535
            $categoryId
6536
        );
6537
6538
        if (empty($link)) {
6539
            return '';
6540
        }
6541
6542
        $resourceDeletedMessage = $category->show_message_resource_delete($courseCode);
6543
6544
        if (false !== $resourceDeletedMessage || api_is_allowed_to_edit() || api_is_excluded_user_type()) {
6545
            return '';
6546
        }
6547
6548
        $certificate = Category::generateUserCertificate($categoryId, $studentId);
6549
6550
        if (!is_array($certificate)) {
6551
            return '';
6552
        }
6553
6554
        return Category::getDownloadCertificateBlock($certificate);
6555
    }
6556
6557
    /**
6558
     * @param int $exerciseId
6559
     */
6560
    public static function getExerciseTitleById($exerciseId)
6561
    {
6562
        $em = Database::getManager();
6563
6564
        return $em
6565
            ->createQuery('SELECT cq.title
6566
                FROM ChamiloCourseBundle:CQuiz cq
6567
                WHERE cq.iid = :iid'
6568
            )
6569
            ->setParameter('iid', $exerciseId)
6570
            ->getSingleScalarResult();
6571
    }
6572
6573
    /**
6574
     * @param int $exeId      ID from track_e_exercises
6575
     * @param int $userId     User ID
6576
     * @param int $exerciseId Exercise ID
6577
     * @param int $courseId   Optional. Coure ID.
6578
     *
6579
     * @return TrackEExercises|null
6580
     */
6581
    public static function recalculateResult($exeId, $userId, $exerciseId, $courseId = 0)
6582
    {
6583
        if (empty($userId) || empty($exerciseId)) {
6584
            return null;
6585
        }
6586
6587
        $em = Database::getManager();
6588
        /** @var TrackEExercises $trackedExercise */
6589
        $trackedExercise = $em->getRepository('ChamiloCoreBundle:TrackEExercises')->find($exeId);
6590
6591
        if (empty($trackedExercise)) {
6592
            return null;
6593
        }
6594
6595
        if ($trackedExercise->getExeUserId() != $userId ||
6596
            $trackedExercise->getExeExoId() != $exerciseId
6597
        ) {
6598
            return null;
6599
        }
6600
6601
        $questionList = $trackedExercise->getDataTracking();
6602
6603
        if (empty($questionList)) {
6604
            return null;
6605
        }
6606
6607
        $questionList = explode(',', $questionList);
6608
6609
        $exercise = new Exercise($courseId);
6610
        $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : [];
6611
6612
        if ($exercise->read($exerciseId) === false) {
6613
            return null;
6614
        }
6615
6616
        $totalScore = 0;
6617
        $totalWeight = 0;
6618
6619
        $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
6620
6621
        $formula = 'true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)
6622
            ? $pluginEvaluation->getFormulaForExercise($exerciseId)
6623
            : 0;
6624
6625
        if (empty($formula)) {
6626
            foreach ($questionList as $questionId) {
6627
                $question = Question::read($questionId, $courseInfo);
6628
6629
                if (false === $question) {
6630
                    continue;
6631
                }
6632
6633
                $totalWeight += $question->selectWeighting();
6634
6635
                // We're inside *one* question. Go through each possible answer for this question
6636
                $result = $exercise->manage_answer(
6637
                    $exeId,
6638
                    $questionId,
6639
                    [],
6640
                    'exercise_result',
6641
                    [],
6642
                    false,
6643
                    true,
6644
                    false,
6645
                    $exercise->selectPropagateNeg(),
6646
                    [],
6647
                    [],
6648
                    true
6649
                );
6650
6651
                //  Adding the new score.
6652
                $totalScore += $result['score'];
6653
            }
6654
        } else {
6655
            $totalScore = $pluginEvaluation->getResultWithFormula($exeId, $formula);
6656
            $totalWeight = $pluginEvaluation->getMaxScore();
6657
        }
6658
6659
        $trackedExercise
6660
            ->setExeResult($totalScore)
6661
            ->setExeWeighting($totalWeight);
6662
6663
        $em->persist($trackedExercise);
6664
        $em->flush();
6665
6666
        return $trackedExercise;
6667
    }
6668
6669
    public static function getTotalQuestionAnswered($courseId, $exerciseId, $questionId, $sessionId = 0, $groups = [], $users = [])
6670
    {
6671
        $courseId = (int) $courseId;
6672
        $exerciseId = (int) $exerciseId;
6673
        $questionId = (int) $questionId;
6674
        $sessionId = (int) $sessionId;
6675
6676
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
6677
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6678
6679
        $userCondition = '';
6680
        $allUsers = [];
6681
        if (!empty($groups)) {
6682
            foreach ($groups as $groupId) {
6683
                $groupUsers = GroupManager::get_users($groupId, null, null, null, false, $courseId);
6684
                if (!empty($groupUsers)) {
6685
                    $allUsers = array_merge($allUsers, $groupUsers);
6686
                }
6687
            }
6688
        }
6689
6690
        if (!empty($users)) {
6691
            $allUsers = array_merge($allUsers, $users);
6692
        }
6693
6694
        if (!empty($allUsers)) {
6695
            $allUsers = array_map('intval', $allUsers);
6696
            $usersToString = implode("', '", $allUsers);
6697
            $userCondition = " AND user_id IN ('$usersToString') ";
6698
        }
6699
6700
        $sessionCondition = '';
6701
        if (!empty($sessionId)) {
6702
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
6703
        }
6704
6705
        $sql = "SELECT count(te.exe_id) total
6706
                FROM $attemptTable t
6707
                INNER JOIN $trackTable te
6708
                ON (te.c_id = t.c_id AND t.exe_id = te.exe_id)
6709
                WHERE
6710
                    t.c_id = $courseId AND
6711
                    exe_exo_id = $exerciseId AND
6712
                    t.question_id = $questionId AND
6713
                    status != 'incomplete'
6714
                    $sessionCondition
6715
                    $userCondition
6716
        ";
6717
        $queryTotal = Database::query($sql);
6718
        $totalRow = Database::fetch_array($queryTotal, 'ASSOC');
6719
        $total = 0;
6720
        if ($totalRow) {
6721
            $total = (int) $totalRow['total'];
6722
        }
6723
6724
        return $total;
6725
    }
6726
6727
    public static function getWrongQuestionResults($courseId, $exerciseId, $sessionId = 0, $groups = [], $users = [])
6728
    {
6729
        $courseId = (int) $courseId;
6730
        $exerciseId = (int) $exerciseId;
6731
6732
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
6733
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
6734
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6735
6736
        $sessionCondition = '';
6737
        if (!empty($sessionId)) {
6738
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
6739
        }
6740
6741
        $userCondition = '';
6742
        $allUsers = [];
6743
        if (!empty($groups)) {
6744
            foreach ($groups as $groupId) {
6745
                $groupUsers = GroupManager::get_users($groupId, null, null, null, false, $courseId);
6746
                if (!empty($groupUsers)) {
6747
                    $allUsers = array_merge($allUsers, $groupUsers);
6748
                }
6749
            }
6750
        }
6751
6752
        if (!empty($users)) {
6753
            $allUsers = array_merge($allUsers, $users);
6754
        }
6755
6756
        if (!empty($allUsers)) {
6757
            $allUsers = array_map('intval', $allUsers);
6758
            $usersToString = implode("', '", $allUsers);
6759
            $userCondition .= " AND user_id IN ('$usersToString') ";
6760
        }
6761
6762
        $sql = "SELECT q.question, question_id, count(q.iid) count
6763
                FROM $attemptTable t
6764
                INNER JOIN $questionTable q
6765
                ON q.iid = t.question_id
6766
                INNER JOIN $trackTable te
6767
                ON t.exe_id = te.exe_id
6768
                WHERE
6769
                    t.c_id = $courseId AND
6770
                    t.marks != q.ponderation AND
6771
                    exe_exo_id = $exerciseId AND
6772
                    status != 'incomplete'
6773
                    $sessionCondition
6774
                    $userCondition
6775
                GROUP BY q.iid
6776
                ORDER BY count DESC
6777
        ";
6778
6779
        $result = Database::query($sql);
6780
6781
        return Database::store_result($result, 'ASSOC');
6782
    }
6783
6784
    public static function getExerciseResultsCount($type, $courseId, Exercise $exercise, $sessionId = 0)
6785
    {
6786
        $courseId = (int) $courseId;
6787
        $exerciseId = (int) $exercise->iid;
6788
6789
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6790
6791
        $sessionCondition = '';
6792
        if (!empty($sessionId)) {
6793
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
6794
        }
6795
6796
        $passPercentage = $exercise->selectPassPercentage();
6797
        $minPercentage = 100;
6798
        if (!empty($passPercentage)) {
6799
            $minPercentage = $passPercentage;
6800
        }
6801
6802
        $selectCount = 'count(DISTINCT te.exe_id)';
6803
        $scoreCondition = '';
6804
        switch ($type) {
6805
            case 'correct_student':
6806
                $selectCount = 'count(DISTINCT te.exe_user_id)';
6807
                $scoreCondition = " AND (exe_result/exe_weighting*100) >= $minPercentage ";
6808
                break;
6809
            case 'wrong_student':
6810
                $selectCount = 'count(DISTINCT te.exe_user_id)';
6811
                $scoreCondition = " AND (exe_result/exe_weighting*100) < $minPercentage ";
6812
                break;
6813
            case 'correct':
6814
                $scoreCondition = " AND (exe_result/exe_weighting*100) >= $minPercentage ";
6815
                break;
6816
            case 'wrong':
6817
                $scoreCondition = " AND (exe_result/exe_weighting*100) < $minPercentage ";
6818
                break;
6819
        }
6820
6821
        $sql = "SELECT $selectCount count
6822
                FROM $trackTable te
6823
                WHERE
6824
                    c_id = $courseId AND
6825
                    exe_exo_id = $exerciseId AND
6826
                    status != 'incomplete'
6827
                    $scoreCondition
6828
                    $sessionCondition
6829
        ";
6830
        $result = Database::query($sql);
6831
        $totalRow = Database::fetch_array($result, 'ASSOC');
6832
        $total = 0;
6833
        if ($totalRow) {
6834
            $total = (int) $totalRow['count'];
6835
        }
6836
6837
        return $total;
6838
    }
6839
6840
    public static function parseContent($content, $stats, Exercise $exercise, $trackInfo, $currentUserId = 0)
6841
    {
6842
        $wrongAnswersCount = $stats['failed_answers_count'];
6843
        $attemptDate = substr($trackInfo['exe_date'], 0, 10);
6844
        $exeId = $trackInfo['exe_id'];
6845
        $resultsStudentUrl = api_get_path(WEB_CODE_PATH).
6846
            'exercise/result.php?id='.$exeId.'&'.api_get_cidreq();
6847
        $resultsTeacherUrl = api_get_path(WEB_CODE_PATH).
6848
            'exercise/exercise_show.php?action=edit&id='.$exeId.'&'.api_get_cidreq(true, true, 'teacher');
6849
6850
        $content = str_replace(
6851
            [
6852
                '((exercise_error_count))',
6853
                '((all_answers_html))',
6854
                '((all_answers_teacher_html))',
6855
                '((exercise_title))',
6856
                '((exercise_attempt_date))',
6857
                '((link_to_test_result_page_student))',
6858
                '((link_to_test_result_page_teacher))',
6859
            ],
6860
            [
6861
                $wrongAnswersCount,
6862
                $stats['all_answers_html'],
6863
                $stats['all_answers_teacher_html'],
6864
                $exercise->get_formated_title(),
6865
                $attemptDate,
6866
                $resultsStudentUrl,
6867
                $resultsTeacherUrl,
6868
            ],
6869
            $content
6870
        );
6871
6872
        $currentUserId = empty($currentUserId) ? api_get_user_id() : (int) $currentUserId;
6873
6874
        $content = AnnouncementManager::parseContent(
6875
            $currentUserId,
6876
            $content,
6877
            api_get_course_id(),
6878
            api_get_session_id()
6879
        );
6880
6881
        return $content;
6882
    }
6883
6884
    public static function sendNotification(
6885
        $currentUserId,
6886
        $objExercise,
6887
        $exercise_stat_info,
6888
        $courseInfo,
6889
        $attemptCountToSend,
6890
        $stats,
6891
        $statsTeacher
6892
    ) {
6893
        $notifications = api_get_configuration_value('exercise_finished_notification_settings');
6894
        if (empty($notifications)) {
6895
            return false;
6896
        }
6897
6898
        $studentId = $exercise_stat_info['exe_user_id'];
6899
        $exerciseExtraFieldValue = new ExtraFieldValue('exercise');
6900
        $wrongAnswersCount = $stats['failed_answers_count'];
6901
        $exercisePassed = $stats['exercise_passed'];
6902
        $countPendingQuestions = $stats['count_pending_questions'];
6903
        $stats['all_answers_teacher_html'] = $statsTeacher['all_answers_html'];
6904
6905
        // If there are no pending questions (Open questions).
6906
        if (0 === $countPendingQuestions) {
6907
            /*$extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6908
                $objExercise->iid,
6909
                'signature_mandatory'
6910
            );
6911
6912
            if ($extraFieldData && isset($extraFieldData['value']) && 1 === (int) $extraFieldData['value']) {
6913
                if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($objExercise)) {
6914
                    $signature = ExerciseSignaturePlugin::getSignature($studentId, $exercise_stat_info);
6915
                    if (false !== $signature) {
6916
                        //return false;
6917
                    }
6918
                }
6919
            }*/
6920
6921
            // Notifications.
6922
            $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6923
                $objExercise->iid,
6924
                'notifications'
6925
            );
6926
            $exerciseNotification = '';
6927
            if ($extraFieldData && isset($extraFieldData['value'])) {
6928
                $exerciseNotification = $extraFieldData['value'];
6929
            }
6930
6931
            $subject = sprintf(get_lang('WrongAttemptXInCourseX'), $attemptCountToSend, $courseInfo['title']);
6932
            if ($exercisePassed) {
6933
                $subject = sprintf(get_lang('ExerciseValidationInCourseX'), $courseInfo['title']);
6934
            }
6935
6936
            if ($exercisePassed) {
6937
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6938
                    $objExercise->iid,
6939
                    'MailSuccess'
6940
                );
6941
            } else {
6942
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6943
                    $objExercise->iid,
6944
                    'MailAttempt'.$attemptCountToSend
6945
                );
6946
            }
6947
6948
            // Blocking exercise.
6949
            $blockPercentageExtra = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6950
                $objExercise->iid,
6951
                'blocking_percentage'
6952
            );
6953
            $blockPercentage = false;
6954
            if ($blockPercentageExtra && isset($blockPercentageExtra['value']) && $blockPercentageExtra['value']) {
6955
                $blockPercentage = $blockPercentageExtra['value'];
6956
            }
6957
            if ($blockPercentage) {
6958
                $passBlock = $stats['total_percentage'] > $blockPercentage;
6959
                if (false === $passBlock) {
6960
                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6961
                        $objExercise->iid,
6962
                        'MailIsBlockByPercentage'
6963
                    );
6964
                }
6965
            }
6966
6967
            $extraFieldValueUser = new ExtraFieldValue('user');
6968
6969
            if ($extraFieldData && isset($extraFieldData['value'])) {
6970
                $content = $extraFieldData['value'];
6971
                $content = self::parseContent($content, $stats, $objExercise, $exercise_stat_info, $studentId);
6972
                //if (false === $exercisePassed) {
6973
                if (0 !== $wrongAnswersCount) {
6974
                    $content .= $stats['failed_answers_html'];
6975
                }
6976
6977
                $sendMessage = true;
6978
                if (!empty($exerciseNotification)) {
6979
                    foreach ($notifications as $name => $notificationList) {
6980
                        if ($exerciseNotification !== $name) {
6981
                            continue;
6982
                        }
6983
                        foreach ($notificationList as $notificationName => $attemptData) {
6984
                            if ('student_check' === $notificationName) {
6985
                                $sendMsgIfInList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : '';
6986
                                if (!empty($sendMsgIfInList)) {
6987
                                    foreach ($sendMsgIfInList as $skipVariable => $skipValues) {
6988
                                        $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
6989
                                            $studentId,
6990
                                            $skipVariable
6991
                                        );
6992
6993
                                        if (empty($userExtraFieldValue)) {
6994
                                            $sendMessage = false;
6995
                                            break;
6996
                                        } else {
6997
                                            $sendMessage = false;
6998
                                            if (isset($userExtraFieldValue['value']) &&
6999
                                                in_array($userExtraFieldValue['value'], $skipValues)
7000
                                            ) {
7001
                                                $sendMessage = true;
7002
                                                break;
7003
                                            }
7004
                                        }
7005
                                    }
7006
                                }
7007
                                break;
7008
                            }
7009
                        }
7010
                    }
7011
                }
7012
7013
                // Send to student.
7014
                if ($sendMessage) {
7015
                    MessageManager::send_message($currentUserId, $subject, $content);
7016
                }
7017
            }
7018
7019
            if (!empty($exerciseNotification)) {
7020
                foreach ($notifications as $name => $notificationList) {
7021
                    if ($exerciseNotification !== $name) {
7022
                        continue;
7023
                    }
7024
                    foreach ($notificationList as $attemptData) {
7025
                        $skipNotification = false;
7026
                        $skipNotificationList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : [];
7027
                        if (!empty($skipNotificationList)) {
7028
                            foreach ($skipNotificationList as $skipVariable => $skipValues) {
7029
                                $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
7030
                                    $studentId,
7031
                                    $skipVariable
7032
                                );
7033
7034
                                if (empty($userExtraFieldValue)) {
7035
                                    $skipNotification = true;
7036
                                    break;
7037
                                } else {
7038
                                    if (isset($userExtraFieldValue['value'])) {
7039
                                        if (!in_array($userExtraFieldValue['value'], $skipValues)) {
7040
                                            $skipNotification = true;
7041
                                            break;
7042
                                        }
7043
                                    } else {
7044
                                        $skipNotification = true;
7045
                                        break;
7046
                                    }
7047
                                }
7048
                            }
7049
                        }
7050
7051
                        if ($skipNotification) {
7052
                            continue;
7053
                        }
7054
7055
                        $email = isset($attemptData['email']) ? $attemptData['email'] : '';
7056
                        $emailList = explode(',', $email);
7057
                        if (empty($emailList)) {
7058
                            continue;
7059
                        }
7060
                        $attempts = isset($attemptData['attempts']) ? $attemptData['attempts'] : [];
7061
                        foreach ($attempts as $attempt) {
7062
                            $sendMessage = false;
7063
                            if (isset($attempt['attempt']) && $attemptCountToSend !== (int) $attempt['attempt']) {
7064
                                continue;
7065
                            }
7066
7067
                            if (!isset($attempt['status'])) {
7068
                                continue;
7069
                            }
7070
7071
                            if ($blockPercentage && isset($attempt['is_block_by_percentage'])) {
7072
                                if ($attempt['is_block_by_percentage']) {
7073
                                    if ($passBlock) {
7074
                                        continue;
7075
                                    }
7076
                                } else {
7077
                                    if (false === $passBlock) {
7078
                                        continue;
7079
                                    }
7080
                                }
7081
                            }
7082
7083
                            switch ($attempt['status']) {
7084
                                case 'passed':
7085
                                    if ($exercisePassed) {
7086
                                        $sendMessage = true;
7087
                                    }
7088
                                    break;
7089
                                case 'failed':
7090
                                    if (false === $exercisePassed) {
7091
                                        $sendMessage = true;
7092
                                    }
7093
                                    break;
7094
                                case 'all':
7095
                                    $sendMessage = true;
7096
                                    break;
7097
                            }
7098
7099
                            if ($sendMessage) {
7100
                                $attachments = [];
7101
                                if (isset($attempt['add_pdf']) && $attempt['add_pdf']) {
7102
                                    // Get pdf content
7103
                                    $pdfExtraData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
7104
                                        $objExercise->iid,
7105
                                        $attempt['add_pdf']
7106
                                    );
7107
7108
                                    if ($pdfExtraData && isset($pdfExtraData['value'])) {
7109
                                        $pdfContent = self::parseContent(
7110
                                            $pdfExtraData['value'],
7111
                                            $stats,
7112
                                            $objExercise,
7113
                                            $exercise_stat_info,
7114
                                            $studentId
7115
                                        );
7116
7117
                                        @$pdf = new PDF();
7118
                                        $filename = get_lang('Exercise');
7119
                                        $cssFile = api_get_path(SYS_CSS_PATH).'themes/chamilo/default.css';
7120
                                        $pdfPath = @$pdf->content_to_pdf(
7121
                                            "<html><body>$pdfContent</body></html>",
7122
                                            file_get_contents($cssFile),
7123
                                            $filename,
7124
                                            api_get_course_id(),
7125
                                            'F',
7126
                                            false,
7127
                                            null,
7128
                                            false,
7129
                                            true
7130
                                        );
7131
                                        $attachments[] = ['filename' => $filename, 'path' => $pdfPath];
7132
                                    }
7133
                                }
7134
7135
                                $content = isset($attempt['content_default']) ? $attempt['content_default'] : '';
7136
                                if (isset($attempt['content'])) {
7137
                                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
7138
                                        $objExercise->iid,
7139
                                        $attempt['content']
7140
                                    );
7141
                                    if ($extraFieldData && isset($extraFieldData['value']) && !empty($extraFieldData['value'])) {
7142
                                        $content = $extraFieldData['value'];
7143
                                    }
7144
                                }
7145
7146
                                if (!empty($content)) {
7147
                                    $content = self::parseContent(
7148
                                        $content,
7149
                                        $stats,
7150
                                        $objExercise,
7151
                                        $exercise_stat_info,
7152
                                        $studentId
7153
                                    );
7154
                                    $extraParameters = [];
7155
                                    if (api_get_configuration_value('mail_header_from_custom_course_logo') == true) {
7156
                                        $extraParameters = ['logo' => CourseManager::getCourseEmailPicture($courseInfo)];
7157
                                    }
7158
                                    foreach ($emailList as $email) {
7159
                                        if (empty($email)) {
7160
                                            continue;
7161
                                        }
7162
7163
                                        api_mail_html(
7164
                                            null,
7165
                                            $email,
7166
                                            $subject,
7167
                                            $content,
7168
                                            null,
7169
                                            null,
7170
                                            [],
7171
                                            $attachments,
7172
                                            false,
7173
                                            $extraParameters,
7174
                                            ''
7175
                                        );
7176
                                    }
7177
                                }
7178
7179
                                if (isset($attempt['post_actions'])) {
7180
                                    foreach ($attempt['post_actions'] as $action => $params) {
7181
                                        switch ($action) {
7182
                                            case 'subscribe_student_to_courses':
7183
                                                foreach ($params as $code) {
7184
                                                    CourseManager::subscribeUser($currentUserId, $code);
7185
                                                    break;
7186
                                                }
7187
                                                break;
7188
                                        }
7189
                                    }
7190
                                }
7191
                            }
7192
                        }
7193
                    }
7194
                }
7195
            }
7196
        }
7197
    }
7198
7199
    /**
7200
     * Delete an exercise attempt.
7201
     *
7202
     * Log the exe_id deleted with the exe_user_id related.
7203
     *
7204
     * @param int $exeId
7205
     */
7206
    public static function deleteExerciseAttempt($exeId)
7207
    {
7208
        $exeId = (int) $exeId;
7209
7210
        $trackExerciseInfo = self::get_exercise_track_exercise_info($exeId);
7211
7212
        if (empty($trackExerciseInfo)) {
7213
            return;
7214
        }
7215
7216
        $tblTrackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7217
        $tblTrackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
7218
7219
        Database::query("DELETE FROM $tblTrackExercises WHERE exe_id = $exeId");
7220
        Database::query("DELETE FROM $tblTrackAttempt WHERE exe_id = $exeId");
7221
7222
        Event::addEvent(
7223
            LOG_EXERCISE_ATTEMPT_DELETE,
7224
            LOG_EXERCISE_ATTEMPT,
7225
            $exeId,
7226
            api_get_utc_datetime()
7227
        );
7228
        Event::addEvent(
7229
            LOG_EXERCISE_ATTEMPT_DELETE,
7230
            LOG_EXERCISE_AND_USER_ID,
7231
            $exeId.'-'.$trackExerciseInfo['exe_user_id'],
7232
            api_get_utc_datetime()
7233
        );
7234
    }
7235
7236
    public static function scorePassed($score, $total)
7237
    {
7238
        $compareResult = bccomp($score, $total, 3);
7239
        $scorePassed = 1 === $compareResult || 0 === $compareResult;
7240
        if (false === $scorePassed) {
7241
            $epsilon = 0.00001;
7242
            if (abs($score - $total) < $epsilon) {
7243
                $scorePassed = true;
7244
            }
7245
        }
7246
7247
        return $scorePassed;
7248
    }
7249
7250
    public static function logPingForCheckingConnection()
7251
    {
7252
        $action = $_REQUEST['a'] ?? '';
7253
7254
        if ('ping' !== $action) {
7255
            return;
7256
        }
7257
7258
        if (!empty(api_get_user_id())) {
7259
            return;
7260
        }
7261
7262
        $exeId = $_REQUEST['exe_id'] ?? 0;
7263
7264
        error_log("Exercise ping received: exe_id = $exeId. _user not found in session.");
7265
    }
7266
7267
    public static function saveFileExerciseResultPdf(
7268
        int $exeId,
7269
        int $courseId,
7270
        int $sessionId
7271
    ) {
7272
        $courseInfo = api_get_course_info_by_id($courseId);
7273
        $courseCode = $courseInfo['code'];
7274
        $cidReq = 'cidReq='.$courseCode.'&id_session='.$sessionId.'&gidReq=0&gradebook=0';
7275
7276
        $url = api_get_path(WEB_PATH).'main/exercise/exercise_show.php?'.$cidReq.'&id='.$exeId.'&action=export&export_type=all_results';
7277
        $ch = curl_init();
7278
        curl_setopt($ch, CURLOPT_URL, $url);
7279
        curl_setopt($ch, CURLOPT_COOKIE, session_id());
7280
        curl_setopt($ch, CURLOPT_AUTOREFERER, true);
7281
        curl_setopt($ch, CURLOPT_COOKIESESSION, true);
7282
        curl_setopt($ch, CURLOPT_FAILONERROR, false);
7283
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
7284
        curl_setopt($ch, CURLOPT_HEADER, true);
7285
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
7286
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
7287
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
7288
7289
        $result = curl_exec($ch);
7290
7291
        if (false === $result) {
7292
            error_log('saveFileExerciseResultPdf error: '.curl_error($ch));
7293
        }
7294
7295
        curl_close($ch);
7296
    }
7297
7298
    /**
7299
     * Export all results of all exercises to a ZIP file (including one zip for each exercise).
7300
     *
7301
     * @return false|void
7302
     */
7303
    public static function exportAllExercisesResultsZip(
7304
        int $sessionId,
7305
        int $courseId,
7306
        array $filterDates = []
7307
    ) {
7308
        $exercises = self::get_all_exercises_for_course_id(
7309
            null,
7310
            $sessionId,
7311
            $courseId,
7312
            false
7313
        );
7314
7315
        $exportOk = false;
7316
        if (!empty($exercises)) {
7317
            $exportName = 'S'.$sessionId.'-C'.$courseId.'-ALL';
7318
            $baseDir = api_get_path(SYS_ARCHIVE_PATH);
7319
            $folderName = 'pdfexport-'.$exportName;
7320
            $exportFolderPath = $baseDir.$folderName;
7321
7322
            if (!is_dir($exportFolderPath)) {
7323
                @mkdir($exportFolderPath);
7324
            }
7325
7326
            foreach ($exercises as $exercise) {
7327
                $exerciseId = $exercise['iid'];
7328
                self::exportExerciseAllResultsZip($sessionId, $courseId, $exerciseId, $filterDates, $exportFolderPath);
7329
            }
7330
7331
            // If export folder is not empty will be zipped.
7332
            $isFolderPathEmpty = (file_exists($exportFolderPath) && 2 == count(scandir($exportFolderPath)));
7333
            if (is_dir($exportFolderPath) && !$isFolderPathEmpty) {
7334
                $exportOk = true;
7335
                $exportFilePath = $baseDir.$exportName.'.zip';
7336
                $zip = new \PclZip($exportFilePath);
7337
                $zip->create($exportFolderPath, PCLZIP_OPT_REMOVE_PATH, $exportFolderPath);
7338
                rmdirr($exportFolderPath);
7339
7340
                DocumentManager::file_send_for_download($exportFilePath, true, $exportName.'.zip');
7341
                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...
7342
            }
7343
        }
7344
7345
        if (!$exportOk) {
7346
            Display::addFlash(
7347
                Display::return_message(
7348
                    get_lang('ExportExerciseNoResult'),
7349
                    'warning',
7350
                    false
7351
                )
7352
            );
7353
        }
7354
7355
        return false;
7356
    }
7357
7358
    /**
7359
     * Export all results of *one* exercise to a ZIP file containing individual PDFs.
7360
     *
7361
     * @return false|void
7362
     */
7363
    public static function exportExerciseAllResultsZip(
7364
        int $sessionId,
7365
        int $courseId,
7366
        int $exerciseId,
7367
        array $filterDates = [],
7368
        string $mainPath = ''
7369
    ) {
7370
        $objExerciseTmp = new Exercise($courseId);
7371
        $exeResults = $objExerciseTmp->getExerciseAndResult(
7372
            $courseId,
7373
            $sessionId,
7374
            $exerciseId,
7375
            true,
7376
            $filterDates
7377
        );
7378
7379
        $exportOk = false;
7380
        if (!empty($exeResults)) {
7381
            $exportName = 'S'.$sessionId.'-C'.$courseId.'-T'.$exerciseId;
7382
            $baseDir = api_get_path(SYS_ARCHIVE_PATH);
7383
            $folderName = 'pdfexport-'.$exportName;
7384
            $exportFolderPath = $baseDir.$folderName;
7385
7386
            // 1. Cleans the export folder if it exists.
7387
            if (is_dir($exportFolderPath)) {
7388
                rmdirr($exportFolderPath);
7389
            }
7390
7391
            // 2. Create the pdfs inside a new export folder path.
7392
            if (!empty($exeResults)) {
7393
                foreach ($exeResults as $exeResult) {
7394
                    $exeId = (int) $exeResult['exe_id'];
7395
                    ExerciseLib::saveFileExerciseResultPdf($exeId, $courseId, $sessionId);
7396
                }
7397
            }
7398
7399
            // 3. If export folder is not empty will be zipped.
7400
            $isFolderPathEmpty = (file_exists($exportFolderPath) && 2 == count(scandir($exportFolderPath)));
7401
            if (is_dir($exportFolderPath) && !$isFolderPathEmpty) {
7402
                $exportOk = true;
7403
                $exportFilePath = $baseDir.$exportName.'.zip';
7404
                $zip = new \PclZip($exportFilePath);
7405
                $zip->create($exportFolderPath, PCLZIP_OPT_REMOVE_PATH, $exportFolderPath);
7406
                rmdirr($exportFolderPath);
7407
7408
                if (!empty($mainPath) && file_exists($exportFilePath)) {
7409
                    @rename($exportFilePath, $mainPath.'/'.$exportName.'.zip');
7410
                } else {
7411
                    DocumentManager::file_send_for_download($exportFilePath, true, $exportName.'.zip');
7412
                    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...
7413
                }
7414
            }
7415
        }
7416
7417
        if (empty($mainPath) && !$exportOk) {
7418
            Display::addFlash(
7419
                Display::return_message(
7420
                    get_lang('ExportExerciseNoResult'),
7421
                    'warning',
7422
                    false
7423
                )
7424
            );
7425
        }
7426
7427
        return false;
7428
    }
7429
7430
    /**
7431
     * Get formatted feedback comments for an exam attempt.
7432
     */
7433
    public static function getFeedbackComments(int $examId): string
7434
    {
7435
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
7436
        $TBL_QUIZ_QUESTION = Database::get_course_table(TABLE_QUIZ_QUESTION);
7437
7438
        $sql = "SELECT ta.question_id, ta.teacher_comment, q.question AS title
7439
            FROM $TBL_TRACK_ATTEMPT ta
7440
            INNER JOIN $TBL_QUIZ_QUESTION q ON ta.question_id = q.iid
7441
            WHERE ta.exe_id = $examId
7442
            AND ta.teacher_comment IS NOT NULL
7443
            AND ta.teacher_comment != ''
7444
            GROUP BY ta.question_id
7445
            ORDER BY q.position ASC, ta.id ASC";
7446
7447
        $result = Database::query($sql);
7448
        $commentsByQuestion = [];
7449
7450
        while ($row = Database::fetch_array($result)) {
7451
            $questionId = $row['question_id'];
7452
            $questionTitle = Security::remove_XSS($row['title']);
7453
            $comment = Security::remove_XSS(trim(strip_tags($row['teacher_comment'])));
7454
7455
            if (!empty($comment)) {
7456
                if (!isset($commentsByQuestion[$questionId])) {
7457
                    $commentsByQuestion[$questionId] = [
7458
                        'title' => $questionTitle,
7459
                        'comments' => [],
7460
                    ];
7461
                }
7462
                $commentsByQuestion[$questionId]['comments'][] = $comment;
7463
            }
7464
        }
7465
7466
        if (empty($commentsByQuestion)) {
7467
            return "<p>".get_lang('NoAdditionalComments')."</p>";
7468
        }
7469
7470
        $output = "<h3>".get_lang('TeacherFeedback')."</h3>";
7471
        $output .= "<table border='1' cellpadding='5' cellspacing='0' width='100%' style='border-collapse: collapse;'>";
7472
7473
        foreach ($commentsByQuestion as $questionId => $data) {
7474
            $output .= "<tr>
7475
                        <td><b>".get_lang('Question')." #$questionId:</b> ".$data['title']."</td>
7476
                    </tr>";
7477
            foreach ($data['comments'] as $comment) {
7478
                $output .= "<tr>
7479
                            <td style='padding-left: 20px;'><i>".get_lang('Feedback').":</i> $comment</td>
7480
                        </tr>";
7481
            }
7482
        }
7483
7484
        $output .= "</table>";
7485
7486
        return $output;
7487
    }
7488
7489
    private static function subscribeSessionWhenFinishedFailure(int $exerciseId): void
7490
    {
7491
        $failureSession = self::getSessionWhenFinishedFailure($exerciseId);
7492
7493
        if ($failureSession) {
7494
            SessionManager::subscribeUsersToSession(
7495
                $failureSession->getId(),
7496
                [api_get_user_id()],
7497
                SESSION_VISIBLE_READ_ONLY,
7498
                false
7499
            );
7500
        }
7501
    }
7502
}
7503