Passed
Push — 1.11.x ( 982bbf...358089 )
by Angel Fernando Quiroz
09:47
created

ExerciseLib::replaceTermsInContent()   B

Complexity

Conditions 10
Paths 8

Size

Total Lines 72
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 49
c 0
b 0
f 0
dl 0
loc 72
rs 7.246
cc 10
nc 8
nop 2

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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