Passed
Push — 1.11.x ( 4623f0...57d8cc )
by Yannick
14:19 queued 04:47
created

ExerciseLib   F

Complexity

Total Complexity 1010

Size/Duplication

Total Lines 7457
Duplicated Lines 0 %

Importance

Changes 10
Bugs 3 Features 1
Metric Value
wmc 1010
eloc 4019
c 10
b 3
f 1
dl 0
loc 7457
rs 0.8

84 Methods

Rating   Name   Duplication   Size   Complexity  
D check_fill_in_blanks() 0 126 19
A getBestScoreByExercise() 0 21 5
A get_count_exam_hotpotatoes_results() 0 10 1
A countAnsweredQuestionsByUserAfterTime() 0 16 1
A logPingForCheckingConnection() 0 15 3
D get_number_students_answer_count() 0 99 18
A displayGroupMenu() 0 31 3
A isPassPercentageEnabled() 0 3 1
A getJsCode() 0 37 3
A getSessionWhenFinishedFailure() 0 18 2
A subscribeSessionWhenFinishedFailure() 0 10 2
A getFeedbackText() 0 3 1
C getUserQuestionScoreGlobal() 0 67 17
B get_number_students_question_with_answer_count() 0 78 5
A isSuccessExerciseResult() 0 13 5
B get_exam_results_hotpotatoes_data() 0 77 6
A get_best_average_score_by_exercise() 0 28 6
C get_all_exercises() 0 82 11
A getAdditionalTeacherActions() 0 14 3
A getLatestHotPotatoResult() 0 25 2
A get_session_time_control_key() 0 16 3
F get_exam_results_data() 0 993 146
C recalculateResult() 0 86 13
A exercise_time_control_is_valid() 0 36 4
F displayQuestionListByAttempt() 0 655 101
A isPassPercentageAttemptPassed() 0 5 1
B getExerciseResultsCount() 0 54 8
A delete_chat_exercise_session() 0 4 2
A parseContent() 0 42 2
F show_score() 0 97 18
A getScoreModels() 0 3 1
A deleteExerciseAttempt() 0 27 2
B getTotalQuestionAnswered() 0 56 8
A convertScoreToModel() 0 18 6
A isQuizEmbeddable() 0 16 4
A scorePassed() 0 12 4
A isQuestionsLimitPerDayReached() 0 18 2
A getNumberStudentsFinishExercise() 0 28 2
A get_average_score() 0 22 5
B generateAndShowCertificateBlock() 0 50 9
A getOralFeedbackAudio() 0 28 5
A showTestsWhereQuestionIsUsed() 0 65 5
A getExerciseTitleById() 0 11 1
A get_exercises_to_be_taken() 0 16 5
C get_exercise_result_ranking() 0 74 17
B exerciseResultsInRanking() 0 55 7
A exercise_time_control_delete() 0 11 1
A getEmbeddableTypes() 0 37 3
C exportPendingAttemptsToExcel() 0 95 12
B exportAllExercisesResultsZip() 0 53 8
A getModelStyle() 0 3 1
A convert_to_percentage() 0 8 2
A saveFileExerciseResultPdf() 0 29 2
F sendNotification() 0 304 73
A detectInputAppropriateClass() 0 20 3
A get_count_exam_results() 0 31 1
B getWrongQuestionResults() 0 55 7
B getNumberStudentsFillBlanksAnswerCount() 0 38 7
B displayResultsInRanking() 0 37 7
B getTotalScoreRibbon() 0 58 7
A get_average_score_by_course() 0 23 5
A convertScoreToPlatformSetting() 0 15 5
C exportExerciseAllResultsZip() 0 65 12
F showQuestion() 0 1810 257
A get_exercise_track_exercise_info() 0 29 4
A get_time_control_key() 0 16 1
A get_number_students_answer_hotspot_count() 0 56 3
A get_all_exercises_for_course_id() 0 39 4
A getNotificationSettings() 0 10 1
B getQuestionDiagnosisRibbon() 0 40 10
A get_average_score_by_course_by_user() 0 23 5
A getNotCorrectedYetText() 0 3 1
A addScoreModelInput() 0 32 4
A create_chat_exercise_session() 0 6 2
A getOralFeedbackForm() 0 8 1
C get_exercise_result_ranking_by_attempt() 0 60 15
A get_best_attempt_in_course() 0 29 6
A get_student_stats_by_question() 0 63 4
A get_best_attempt_by_user() 0 31 6
C getTrackExerciseAttemptsTable() 0 120 17
B getCourseScoreModel() 0 23 7
A getEmailNotification() 0 18 1
A showSuccessMessage() 0 37 3
B getFeedbackComments() 0 54 7

How to fix   Complexity   

Complex Class

Complex classes like ExerciseLib often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ExerciseLib, and based on these observations, apply Extract Interface, too.

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

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
7318
            }
7319
        }
7320
7321
        if (!$exportOk) {
7322
            Display::addFlash(
7323
                Display::return_message(
7324
                    get_lang('ExportExerciseNoResult'),
7325
                    'warning',
7326
                    false
7327
                )
7328
            );
7329
        }
7330
7331
        return false;
7332
    }
7333
7334
    /**
7335
     * Export all results of *one* exercise to a ZIP file containing individual PDFs.
7336
     *
7337
     * @return false|void
7338
     */
7339
    public static function exportExerciseAllResultsZip(
7340
        int $sessionId,
7341
        int $courseId,
7342
        int $exerciseId,
7343
        array $filterDates = [],
7344
        string $mainPath = ''
7345
    ) {
7346
        $objExerciseTmp = new Exercise($courseId);
7347
        $exeResults = $objExerciseTmp->getExerciseAndResult(
7348
            $courseId,
7349
            $sessionId,
7350
            $exerciseId,
7351
            true,
7352
            $filterDates
7353
        );
7354
7355
        $exportOk = false;
7356
        if (!empty($exeResults)) {
7357
            $exportName = 'S'.$sessionId.'-C'.$courseId.'-T'.$exerciseId;
7358
            $baseDir = api_get_path(SYS_ARCHIVE_PATH);
7359
            $folderName = 'pdfexport-'.$exportName;
7360
            $exportFolderPath = $baseDir.$folderName;
7361
7362
            // 1. Cleans the export folder if it exists.
7363
            if (is_dir($exportFolderPath)) {
7364
                rmdirr($exportFolderPath);
7365
            }
7366
7367
            // 2. Create the pdfs inside a new export folder path.
7368
            if (!empty($exeResults)) {
7369
                foreach ($exeResults as $exeResult) {
7370
                    $exeId = (int) $exeResult['exe_id'];
7371
                    ExerciseLib::saveFileExerciseResultPdf($exeId, $courseId, $sessionId);
7372
                }
7373
            }
7374
7375
            // 3. If export folder is not empty will be zipped.
7376
            $isFolderPathEmpty = (file_exists($exportFolderPath) && 2 == count(scandir($exportFolderPath)));
7377
            if (is_dir($exportFolderPath) && !$isFolderPathEmpty) {
7378
                $exportOk = true;
7379
                $exportFilePath = $baseDir.$exportName.'.zip';
7380
                $zip = new \PclZip($exportFilePath);
7381
                $zip->create($exportFolderPath, PCLZIP_OPT_REMOVE_PATH, $exportFolderPath);
7382
                rmdirr($exportFolderPath);
7383
7384
                if (!empty($mainPath) && file_exists($exportFilePath)) {
7385
                    @rename($exportFilePath, $mainPath.'/'.$exportName.'.zip');
7386
                } else {
7387
                    DocumentManager::file_send_for_download($exportFilePath, true, $exportName.'.zip');
7388
                    exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
7389
                }
7390
            }
7391
        }
7392
7393
        if (empty($mainPath) && !$exportOk) {
7394
            Display::addFlash(
7395
                Display::return_message(
7396
                    get_lang('ExportExerciseNoResult'),
7397
                    'warning',
7398
                    false
7399
                )
7400
            );
7401
        }
7402
7403
        return false;
7404
    }
7405
7406
    private static function subscribeSessionWhenFinishedFailure(int $exerciseId): void
7407
    {
7408
        $failureSession = self::getSessionWhenFinishedFailure($exerciseId);
7409
7410
        if ($failureSession) {
7411
            SessionManager::subscribeUsersToSession(
7412
                $failureSession->getId(),
7413
                [api_get_user_id()],
7414
                SESSION_VISIBLE_READ_ONLY,
7415
                false
7416
            );
7417
        }
7418
    }
7419
7420
    /**
7421
     * Get formatted feedback comments for an exam attempt.
7422
     */
7423
    public static function getFeedbackComments(int $examId): string
7424
    {
7425
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
7426
        $TBL_QUIZ_QUESTION = Database::get_course_table(TABLE_QUIZ_QUESTION);
7427
7428
        $sql = "SELECT ta.question_id, ta.teacher_comment, q.question AS title
7429
            FROM $TBL_TRACK_ATTEMPT ta
7430
            INNER JOIN $TBL_QUIZ_QUESTION q ON ta.question_id = q.iid
7431
            WHERE ta.exe_id = $examId
7432
            AND ta.teacher_comment IS NOT NULL
7433
            AND ta.teacher_comment != ''
7434
            GROUP BY ta.question_id
7435
            ORDER BY q.position ASC, ta.id ASC";
7436
7437
        $result = Database::query($sql);
7438
        $commentsByQuestion = [];
7439
7440
        while ($row = Database::fetch_array($result)) {
7441
            $questionId = $row['question_id'];
7442
            $questionTitle = Security::remove_XSS($row['title']);
7443
            $comment = Security::remove_XSS(trim(strip_tags($row['teacher_comment'])));
7444
7445
            if (!empty($comment)) {
7446
                if (!isset($commentsByQuestion[$questionId])) {
7447
                    $commentsByQuestion[$questionId] = [
7448
                        'title' => $questionTitle,
7449
                        'comments' => [],
7450
                    ];
7451
                }
7452
                $commentsByQuestion[$questionId]['comments'][] = $comment;
7453
            }
7454
        }
7455
7456
        if (empty($commentsByQuestion)) {
7457
            return "<p>" . get_lang('NoAdditionalComments') . "</p>";
7458
        }
7459
7460
        $output = "<h3>" . get_lang('TeacherFeedback') . "</h3>";
7461
        $output .= "<table border='1' cellpadding='5' cellspacing='0' width='100%' style='border-collapse: collapse;'>";
7462
7463
        foreach ($commentsByQuestion as $questionId => $data) {
7464
            $output .= "<tr>
7465
                        <td><b>" . get_lang('Question') . " #$questionId:</b> " . $data['title'] . "</td>
7466
                    </tr>";
7467
            foreach ($data['comments'] as $comment) {
7468
                $output .= "<tr>
7469
                            <td style='padding-left: 20px;'><i>" . get_lang('Feedback') . ":</i> $comment</td>
7470
                        </tr>";
7471
            }
7472
        }
7473
7474
        $output .= "</table>";
7475
7476
        return $output;
7477
    }
7478
}
7479