Passed
Push — 1.11.x ( aa07f8...e7427e )
by Angel Fernando Quiroz
09:34
created

ExerciseLib::get_session_time_control_key()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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