ExerciseLib   F
last analyzed

Complexity

Total Complexity 842

Size/Duplication

Total Lines 6219
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 3333
dl 0
loc 6219
rs 0.8
c 0
b 0
f 0
wmc 842

75 Methods

Rating   Name   Duplication   Size   Complexity  
F showQuestion() 0 1555 222
D check_fill_in_blanks() 0 126 19
A getBestScoreByExercise() 0 21 5
A countAnsweredQuestionsByUserAfterTime() 0 20 2
C get_number_students_answer_count() 0 92 12
A getNumberStudentsAnswerHotspotCount() 0 57 3
A displayGroupMenu() 0 24 3
A isPassPercentageEnabled() 0 3 1
A getJsCode() 0 37 3
A getEmailContentForAttempt() 0 39 4
A getFeedbackText() 0 3 1
B get_number_students_question_with_answer_count() 0 77 5
A isSuccessExerciseResult() 0 13 5
A get_best_average_score_by_exercise() 0 28 6
A get_all_exercises() 0 28 5
A getAdditionalTeacherActions() 0 16 4
A get_session_time_control_key() 0 16 3
F get_exam_results_data() 0 717 95
D recalculateResult() 0 100 19
A exercise_time_control_is_valid() 0 29 4
F displayQuestionListByAttempt() 0 625 102
A isPassPercentageAttemptPassed() 0 5 1
B getExerciseResultsCount() 0 48 7
A delete_chat_exercise_session() 0 4 2
A parseContent() 0 42 2
D show_score() 0 96 18
A getScoreModels() 0 3 1
A deleteExerciseAttempt() 0 27 2
A getTotalQuestionAnswered() 0 38 3
A convertScoreToModel() 0 18 6
B getOralFileAudio() 0 39 8
A isQuizEmbeddable() 0 36 3
A scorePassed() 0 12 4
A isQuestionsLimitPerDayReached() 0 18 2
A getStudentStatsByQuestion() 0 64 4
A get_average_score() 0 19 5
B generateAndShowCertificateBlock() 0 53 9
A getOralFeedbackAudio() 0 32 4
A showTestsWhereQuestionIsUsed() 0 52 3
A getExerciseTitleById() 0 11 1
C get_exercise_result_ranking() 0 72 17
B exerciseResultsInRanking() 0 62 11
A exercise_time_control_delete() 0 11 1
A getModelStyle() 0 3 1
A convert_to_percentage() 0 8 2
F sendNotification() 0 299 72
A saveFileExerciseResultPdf() 0 27 2
A get_count_exam_results() 0 15 1
A detectInputAppropriateClass() 0 20 3
A getWrongQuestionResults() 0 35 2
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
A sendExerciseResultByEmail() 0 13 2
C exportExerciseAllResultsZip() 0 79 14
A get_exercise_track_exercise_info() 0 27 4
A get_time_control_key() 0 16 1
A get_all_exercises_for_course_id() 0 36 4
A getNotificationSettings() 0 7 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 9 1
C get_exercise_result_ranking_by_attempt() 0 60 15
A getReviewedAttemptsInfo() 0 19 1
A get_best_attempt_in_course() 0 26 6
A get_best_attempt_by_user() 0 28 6
B getCourseScoreModel() 0 24 7
A getEmailNotification() 0 20 1
A getCountOfAnswers() 0 64 3
A showSuccessMessage() 0 27 3

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