Passed
Pull Request — 1.11.x (#4272)
by Angel Fernando Quiroz
16:11 queued 05:16
created

ExerciseLib::deleteExerciseAttempt()   A

Complexity

Conditions 2

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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