Passed
Push — 1.11.x ( 3f0490...8bf24b )
by Julito
10:31
created

ExerciseLib::displayQuestionListByAttempt()   F

Complexity

Conditions 95
Paths 5

Size

Total Lines 605
Code Lines 395

Duplication

Lines 0
Ratio 0 %

Importance

Changes 10
Bugs 0 Features 1
Metric Value
cc 95
eloc 395
c 10
b 0
f 1
nc 5
nop 7
dl 0
loc 605
rs 3.3333

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
6
use Chamilo\CoreBundle\Entity\TrackEExercises;
7
use ChamiloSession as Session;
8
9
/**
10
 * Class ExerciseLib
11
 * shows a question and its answers.
12
 *
13
 * @author Olivier Brouckaert <[email protected]> 2003-2004
14
 * @author Hubert Borderiou 2011-10-21
15
 * @author ivantcholakov2009-07-20
16
 * @author Julio Montoya
17
 */
18
class ExerciseLib
19
{
20
    /**
21
     * Shows a question.
22
     *
23
     * @param Exercise $exercise
24
     * @param int      $questionId     $questionId question id
25
     * @param bool     $only_questions if true only show the questions, no exercise title
26
     * @param bool     $origin         i.e = learnpath
27
     * @param string   $current_item   current item from the list of questions
28
     * @param bool     $show_title
29
     * @param bool     $freeze
30
     * @param array    $user_choice
31
     * @param bool     $show_comment
32
     * @param bool     $show_answers
33
     *
34
     * @throws \Exception
35
     *
36
     * @return bool|int
37
     */
38
    public static function showQuestion(
39
        $exercise,
40
        $questionId,
41
        $only_questions = false,
42
        $origin = false,
43
        $current_item = '',
44
        $show_title = true,
45
        $freeze = false,
46
        $user_choice = [],
47
        $show_comment = false,
48
        $show_answers = false,
49
        $show_icon = false
50
    ) {
51
        $course_id = $exercise->course_id;
52
        $exerciseId = $exercise->iId;
53
54
        if (empty($course_id)) {
55
            return '';
56
        }
57
        $course = $exercise->course;
58
59
        // Change false to true in the following line to enable answer hinting
60
        $debug_mark_answer = $show_answers;
61
        // Reads question information
62
        if (!$objQuestionTmp = Question::read($questionId, $course)) {
63
            // Question not found
64
            return false;
65
        }
66
67
        $questionRequireAuth = WhispeakAuthPlugin::questionRequireAuthentify($questionId);
68
69
        if ($exercise->getFeedbackType() != EXERCISE_FEEDBACK_TYPE_END) {
70
            $show_comment = false;
71
        }
72
73
        $answerType = $objQuestionTmp->selectType();
74
        $pictureName = $objQuestionTmp->getPictureFilename();
75
        $s = '';
76
        if ($answerType != HOT_SPOT &&
77
            $answerType != HOT_SPOT_DELINEATION &&
78
            $answerType != ANNOTATION
79
        ) {
80
            // Question is not a hotspot
81
            if (!$only_questions) {
82
                $questionDescription = $objQuestionTmp->selectDescription();
83
                if ($show_title) {
84
                    if ($exercise->display_category_name) {
85
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->id);
86
                    }
87
                    $titleToDisplay = $objQuestionTmp->getTitleToDisplay($current_item);
88
                    if ($answerType == READING_COMPREHENSION) {
89
                        // In READING_COMPREHENSION, the title of the question
90
                        // contains the question itself, which can only be
91
                        // shown at the end of the given time, so hide for now
92
                        $titleToDisplay = Display::div(
93
                            $current_item.'. '.get_lang('ReadingComprehension'),
94
                            ['class' => 'question_title']
95
                        );
96
                    }
97
                    echo $titleToDisplay;
98
                }
99
100
                if ($questionRequireAuth) {
101
                    WhispeakAuthPlugin::quizQuestionAuthentify($questionId, $exercise);
102
103
                    return false;
104
                }
105
106
                if (!empty($questionDescription) && $answerType != READING_COMPREHENSION) {
107
                    echo Display::div(
108
                        $questionDescription,
109
                        ['class' => 'question_description']
110
                    );
111
                }
112
            }
113
114
            if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION]) && $freeze) {
115
                return '';
116
            }
117
118
            echo '<div class="question_options">';
119
            // construction of the Answer object (also gets all answers details)
120
            $objAnswerTmp = new Answer($questionId, $course_id, $exercise);
121
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
122
            $quizQuestionOptions = Question::readQuestionOption($questionId, $course_id);
123
124
            // For "matching" type here, we need something a little bit special
125
            // because the match between the suggestions and the answers cannot be
126
            // done easily (suggestions and answers are in the same table), so we
127
            // have to go through answers first (elems with "correct" value to 0).
128
            $select_items = [];
129
            //This will contain the number of answers on the left side. We call them
130
            // suggestions here, for the sake of comprehensions, while the ones
131
            // on the right side are called answers
132
            $num_suggestions = 0;
133
            switch ($answerType) {
134
                case MATCHING:
135
                case DRAGGABLE:
136
                case MATCHING_DRAGGABLE:
137
                    if ($answerType == DRAGGABLE) {
138
                        $isVertical = $objQuestionTmp->extra == 'v';
139
                        $s .= '
140
                            <div class="row">
141
                                <div class="col-md-12">
142
                                    <p class="small">'.get_lang('DraggableQuestionIntro').'</p>
143
                                    <ul class="exercise-draggable-answer list-unstyled '
144
                            .($isVertical ? '' : 'list-inline').'" id="question-'.$questionId.'" data-question="'
145
                            .$questionId.'">
146
                        ';
147
                    } else {
148
                        $s .= '<div id="drag'.$questionId.'_question" class="drag_question">
149
                               <table class="table table-hover table-striped data_table">';
150
                    }
151
152
                    // Iterate through answers.
153
                    $x = 1;
154
                    // Mark letters for each answer.
155
                    $letter = 'A';
156
                    $answer_matching = [];
157
                    $cpt1 = [];
158
                    for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
159
                        $answerCorrect = $objAnswerTmp->isCorrect($answerId);
160
                        $numAnswer = $objAnswerTmp->selectAutoId($answerId);
161
                        if ($answerCorrect == 0) {
162
                            // options (A, B, C, ...) that will be put into the list-box
163
                            // have the "correct" field set to 0 because they are answer
164
                            $cpt1[$x] = $letter;
165
                            $answer_matching[$x] = $objAnswerTmp->selectAnswerByAutoId($numAnswer);
166
                            $x++;
167
                            $letter++;
168
                        }
169
                    }
170
171
                    $i = 1;
172
                    $select_items[0]['id'] = 0;
173
                    $select_items[0]['letter'] = '--';
174
                    $select_items[0]['answer'] = '';
175
                    foreach ($answer_matching as $id => $value) {
176
                        $select_items[$i]['id'] = $value['id_auto'];
177
                        $select_items[$i]['letter'] = $cpt1[$id];
178
                        $select_items[$i]['answer'] = $value['answer'];
179
                        $i++;
180
                    }
181
182
                    $user_choice_array_position = [];
183
                    if (!empty($user_choice)) {
184
                        foreach ($user_choice as $item) {
185
                            $user_choice_array_position[$item['position']] = $item['answer'];
186
                        }
187
                    }
188
                    $num_suggestions = ($nbrAnswers - $x) + 1;
189
                    break;
190
                case FREE_ANSWER:
191
                    $fck_content = isset($user_choice[0]) && !empty($user_choice[0]['answer']) ? $user_choice[0]['answer'] : null;
192
                    $form = new FormValidator('free_choice_'.$questionId);
193
                    $config = [
194
                        'ToolbarSet' => 'TestFreeAnswer',
195
                        'id' => 'choice['.$questionId.']',
196
                    ];
197
                    $form->addHtmlEditor(
198
                        'choice['.$questionId.']',
199
                        null,
200
                        false,
201
                        false,
202
                        $config
203
                    );
204
                    $form->setDefaults(["choice[".$questionId."]" => $fck_content]);
205
                    $s .= $form->returnForm();
206
                    break;
207
                case ORAL_EXPRESSION:
208
                    // Add nanog
209
                    if (api_get_setting('enable_record_audio') === 'true') {
210
                        //@todo pass this as a parameter
211
                        global $exercise_stat_info;
212
                        if (!empty($exercise_stat_info)) {
213
                            $objQuestionTmp->initFile(
214
                                api_get_session_id(),
215
                                api_get_user_id(),
216
                                $exercise_stat_info['exe_exo_id'],
217
                                $exercise_stat_info['exe_id']
218
                            );
219
                        } else {
220
                            $objQuestionTmp->initFile(
221
                                api_get_session_id(),
222
                                api_get_user_id(),
223
                                $exerciseId,
224
                                'temp_exe'
225
                            );
226
                        }
227
228
                        echo $objQuestionTmp->returnRecorder();
229
                    }
230
231
                    $form = new FormValidator('free_choice_'.$questionId);
232
                    $config = ['ToolbarSet' => 'TestFreeAnswer'];
233
234
                    $form->addHtml('<div id="'.'hide_description_'.$questionId.'_options" style="display: none;">');
235
                    $form->addHtmlEditor(
236
                        "choice[$questionId]",
237
                        null,
238
                        false,
239
                        false,
240
                        $config
241
                    );
242
                    $form->addHtml('</div>');
243
                    $s .= $form->returnForm();
244
                    break;
245
            }
246
247
            // Now navigate through the possible answers, using the max number of
248
            // answers for the question as a limiter
249
            $lines_count = 1; // a counter for matching-type answers
250
            if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE ||
251
                $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE
252
            ) {
253
                $header = Display::tag('th', get_lang('Options'));
254
                foreach ($objQuestionTmp->options as $item) {
255
                    if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE) {
256
                        if (in_array($item, $objQuestionTmp->options)) {
257
                            $header .= Display::tag('th', get_lang($item));
258
                        } else {
259
                            $header .= Display::tag('th', $item);
260
                        }
261
                    } else {
262
                        $header .= Display::tag('th', $item);
263
                    }
264
                }
265
                if ($show_comment) {
266
                    $header .= Display::tag('th', get_lang('Feedback'));
267
                }
268
                $s .= '<table class="table table-hover table-striped">';
269
                $s .= Display::tag(
270
                    'tr',
271
                    $header,
272
                    ['style' => 'text-align:left;']
273
                );
274
            } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
275
                $header = Display::tag('th', get_lang('Options'), ['width' => '50%']);
276
                echo "
277
                <script>
278
                    function RadioValidator(question_id, answer_id)
279
                    {
280
                        var ShowAlert = '';
281
                        var typeRadioB = '';
282
                        var AllFormElements = window.document.getElementById('exercise_form').elements;
283
284
                        for (i = 0; i < AllFormElements.length; i++) {
285
                            if (AllFormElements[i].type == 'radio') {
286
                                var ThisRadio = AllFormElements[i].name;
287
                                var ThisChecked = 'No';
288
                                var AllRadioOptions = document.getElementsByName(ThisRadio);
289
290
                                for (x = 0; x < AllRadioOptions.length; x++) {
291
                                     if (AllRadioOptions[x].checked && ThisChecked == 'No') {
292
                                         ThisChecked = 'Yes';
293
                                         break;
294
                                     }
295
                                }
296
297
                                var AlreadySearched = ShowAlert.indexOf(ThisRadio);
298
                                if (ThisChecked == 'No' && AlreadySearched == -1) {
299
                                    ShowAlert = ShowAlert + ThisRadio;
300
                                }
301
                            }
302
                        }
303
                        if (ShowAlert != '') {
304
305
                        } else {
306
                            $('.question-validate-btn').removeAttr('disabled');
307
                        }
308
                    }
309
310
                    function handleRadioRow(event, question_id, answer_id) {
311
                        var t = event.target;
312
                        if (t && t.tagName == 'INPUT')
313
                            return;
314
                        while (t && t.tagName != 'TD') {
315
                            t = t.parentElement;
316
                        }
317
                        var r = t.getElementsByTagName('INPUT')[0];
318
                        r.click();
319
                        RadioValidator(question_id, answer_id);
320
                    }
321
322
                    $(function() {
323
                        var ShowAlert = '';
324
                        var typeRadioB = '';
325
                        var question_id = $('input[name=question_id]').val();
326
                        var AllFormElements = window.document.getElementById('exercise_form').elements;
327
328
                        for (i = 0; i < AllFormElements.length; i++) {
329
                            if (AllFormElements[i].type == 'radio') {
330
                                var ThisRadio = AllFormElements[i].name;
331
                                var ThisChecked = 'No';
332
                                var AllRadioOptions = document.getElementsByName(ThisRadio);
333
334
                                for (x = 0; x < AllRadioOptions.length; x++) {
335
                                    if (AllRadioOptions[x].checked && ThisChecked == 'No') {
336
                                        ThisChecked = \"Yes\";
337
                                        break;
338
                                    }
339
                                }
340
341
                                var AlreadySearched = ShowAlert.indexOf(ThisRadio);
342
                                if (ThisChecked == 'No' && AlreadySearched == -1) {
343
                                    ShowAlert = ShowAlert + ThisRadio;
344
                                }
345
                            }
346
                        }
347
348
                        if (ShowAlert != '') {
349
                             $('.question-validate-btn').attr('disabled', 'disabled');
350
                        } else {
351
                            $('.question-validate-btn').removeAttr('disabled');
352
                        }
353
                    });
354
                </script>";
355
356
                foreach ($objQuestionTmp->optionsTitle as $item) {
357
                    if (in_array($item, $objQuestionTmp->optionsTitle)) {
358
                        $properties = [];
359
                        if ($item === 'Answers') {
360
                            $properties['colspan'] = 2;
361
                            $properties['style'] = 'background-color: #F56B2A; color: #ffffff;';
362
                        } elseif ($item == 'DegreeOfCertaintyThatMyAnswerIsCorrect') {
363
                            $properties['colspan'] = 6;
364
                            $properties['style'] = 'background-color: #330066; color: #ffffff;';
365
                        }
366
                        $header .= Display::tag('th', get_lang($item), $properties);
367
                    } else {
368
                        $header .= Display::tag('th', $item);
369
                    }
370
                }
371
372
                if ($show_comment) {
373
                    $header .= Display::tag('th', get_lang('Feedback'));
374
                }
375
376
                $s .= '<table class="table table-hover table-striped data_table">';
377
                $s .= Display::tag('tr', $header, ['style' => 'text-align:left;']);
378
379
                // ajout de la 2eme ligne d'entête pour true/falss et les pourcentages de certitude
380
                $header1 = Display::tag('th', '&nbsp;');
381
                $cpt1 = 0;
382
                foreach ($objQuestionTmp->options as $item) {
383
                    $colorBorder1 = $cpt1 == (count($objQuestionTmp->options) - 1)
384
                        ? '' : 'border-right: solid #FFFFFF 1px;';
385
                    if ($item === 'True' || $item === 'False') {
386
                        $header1 .= Display::tag(
387
                            'th',
388
                            get_lang($item),
389
                            ['style' => 'background-color: #F7C9B4; color: black;'.$colorBorder1]
390
                        );
391
                    } else {
392
                        $header1 .= Display::tag(
393
                            'th',
394
                            $item,
395
                            ['style' => 'background-color: #e6e6ff; color: black;padding:5px; '.$colorBorder1]
396
                        );
397
                    }
398
                    $cpt1++;
399
                }
400
                if ($show_comment) {
401
                    $header1 .= Display::tag('th', '&nbsp;');
402
                }
403
404
                $s .= Display::tag('tr', $header1);
405
406
                // add explanation
407
                $header2 = Display::tag('th', '&nbsp;');
408
                $descriptionList = [
409
                    get_lang('DegreeOfCertaintyIDeclareMyIgnorance'),
410
                    get_lang('DegreeOfCertaintyIAmVeryUnsure'),
411
                    get_lang('DegreeOfCertaintyIAmUnsure'),
412
                    get_lang('DegreeOfCertaintyIAmPrettySure'),
413
                    get_lang('DegreeOfCertaintyIAmSure'),
414
                    get_lang('DegreeOfCertaintyIAmVerySure'),
415
                ];
416
                $counter2 = 0;
417
                foreach ($objQuestionTmp->options as $item) {
418
                    if ($item === 'True' || $item === 'False') {
419
                        $header2 .= Display::tag('td',
420
                            '&nbsp;',
421
                            ['style' => 'background-color: #F7E1D7; color: black;border-right: solid #FFFFFF 1px;']);
422
                    } else {
423
                        $color_border2 = ($counter2 == (count($objQuestionTmp->options) - 1)) ?
424
                            '' : 'border-right: solid #FFFFFF 1px;font-size:11px;';
425
                        $header2 .= Display::tag(
426
                            'td',
427
                            nl2br($descriptionList[$counter2]),
428
                            ['style' => 'background-color: #EFEFFC; color: black; width: 110px; text-align:center;
429
                                vertical-align: top; padding:5px; '.$color_border2]);
430
                        $counter2++;
431
                    }
432
                }
433
                if ($show_comment) {
434
                    $header2 .= Display::tag('th', '&nbsp;');
435
                }
436
                $s .= Display::tag('tr', $header2);
437
            }
438
439
            if ($show_comment) {
440
                if (in_array(
441
                    $answerType,
442
                    [
443
                        MULTIPLE_ANSWER,
444
                        MULTIPLE_ANSWER_COMBINATION,
445
                        UNIQUE_ANSWER,
446
                        UNIQUE_ANSWER_IMAGE,
447
                        UNIQUE_ANSWER_NO_OPTION,
448
                        GLOBAL_MULTIPLE_ANSWER,
449
                    ]
450
                )) {
451
                    $header = Display::tag('th', get_lang('Options'));
452
                    if ($exercise->getFeedbackType() == EXERCISE_FEEDBACK_TYPE_END) {
453
                        $header .= Display::tag('th', get_lang('Feedback'));
454
                    }
455
                    $s .= '<table class="table table-hover table-striped">';
456
                    $s .= Display::tag(
457
                        'tr',
458
                        $header,
459
                        ['style' => 'text-align:left;']
460
                    );
461
                }
462
            }
463
464
            $matching_correct_answer = 0;
465
            $userChoiceList = [];
466
            if (!empty($user_choice)) {
467
                foreach ($user_choice as $item) {
468
                    $userChoiceList[] = $item['answer'];
469
                }
470
            }
471
472
            $hidingClass = '';
473
            if ($answerType == READING_COMPREHENSION) {
474
                $objQuestionTmp->setExerciseType($exercise->selectType());
475
                $objQuestionTmp->processText($objQuestionTmp->selectDescription());
476
                $hidingClass = 'hide-reading-answers';
477
                $s .= Display::div(
478
                    $objQuestionTmp->selectTitle(),
479
                    ['class' => 'question_title '.$hidingClass]
480
                );
481
            }
482
483
            for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
484
                $answer = $objAnswerTmp->selectAnswer($answerId);
485
                $answerCorrect = $objAnswerTmp->isCorrect($answerId);
486
                $numAnswer = $objAnswerTmp->selectAutoId($answerId);
487
                $comment = $objAnswerTmp->selectComment($answerId);
488
                $attributes = [];
489
490
                switch ($answerType) {
491
                    case UNIQUE_ANSWER:
492
                    case UNIQUE_ANSWER_NO_OPTION:
493
                    case UNIQUE_ANSWER_IMAGE:
494
                    case READING_COMPREHENSION:
495
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
496
                        if (isset($user_choice[0]['answer']) && $user_choice[0]['answer'] == $numAnswer) {
497
                            $attributes = [
498
                                'id' => $input_id,
499
                                'checked' => 1,
500
                                'selected' => 1,
501
                            ];
502
                        } else {
503
                            $attributes = ['id' => $input_id];
504
                        }
505
506
                        if ($debug_mark_answer) {
507
                            if ($answerCorrect) {
508
                                $attributes['checked'] = 1;
509
                                $attributes['selected'] = 1;
510
                            }
511
                        }
512
513
                        if ($show_comment) {
514
                            $s .= '<tr><td>';
515
                        }
516
517
                        if ($answerType == UNIQUE_ANSWER_IMAGE) {
518
                            if ($show_comment) {
519
                                if (empty($comment)) {
520
                                    $s .= '<div id="answer'.$questionId.$numAnswer.'"
521
                                            class="exercise-unique-answer-image" style="text-align: center">';
522
                                } else {
523
                                    $s .= '<div id="answer'.$questionId.$numAnswer.'"
524
                                            class="exercise-unique-answer-image col-xs-6 col-sm-12"
525
                                            style="text-align: center">';
526
                                }
527
                            } else {
528
                                $s .= '<div id="answer'.$questionId.$numAnswer.'"
529
                                        class="exercise-unique-answer-image col-xs-6 col-md-3"
530
                                        style="text-align: center">';
531
                            }
532
                        }
533
534
                        if ($answerType != UNIQUE_ANSWER_IMAGE) {
535
                            $answer = Security::remove_XSS($answer, STUDENT);
536
                        }
537
                        $s .= Display::input(
538
                            'hidden',
539
                            'choice2['.$questionId.']',
540
                            '0'
541
                        );
542
543
                        $answer_input = null;
544
                        $attributes['class'] = 'checkradios';
545
                        if ($answerType == UNIQUE_ANSWER_IMAGE) {
546
                            $attributes['class'] = '';
547
                            $attributes['style'] = 'display: none;';
548
                            $answer = '<div class="thumbnail">'.$answer.'</div>';
549
                        }
550
551
                        $answer_input .= '<label class="radio '.$hidingClass.'">';
552
                        $answer_input .= Display::input(
553
                            'radio',
554
                            'choice['.$questionId.']',
555
                            $numAnswer,
556
                            $attributes
557
                        );
558
                        $answer_input .= $answer;
559
                        $answer_input .= '</label>';
560
561
                        if ($answerType == UNIQUE_ANSWER_IMAGE) {
562
                            $answer_input .= "</div>";
563
                        }
564
565
                        if ($show_comment) {
566
                            $s .= $answer_input;
567
                            $s .= '</td>';
568
                            $s .= '<td>';
569
                            $s .= $comment;
570
                            $s .= '</td>';
571
                            $s .= '</tr>';
572
                        } else {
573
                            $s .= $answer_input;
574
                        }
575
                        break;
576
                    case MULTIPLE_ANSWER:
577
                    case MULTIPLE_ANSWER_TRUE_FALSE:
578
                    case GLOBAL_MULTIPLE_ANSWER:
579
                    case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
580
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
581
                        $answer = Security::remove_XSS($answer, STUDENT);
582
583
                        if (in_array($numAnswer, $userChoiceList)) {
584
                            $attributes = [
585
                                'id' => $input_id,
586
                                'checked' => 1,
587
                                'selected' => 1,
588
                            ];
589
                        } else {
590
                            $attributes = ['id' => $input_id];
591
                        }
592
593
                        if ($debug_mark_answer) {
594
                            if ($answerCorrect) {
595
                                $attributes['checked'] = 1;
596
                                $attributes['selected'] = 1;
597
                            }
598
                        }
599
600
                        if ($answerType == MULTIPLE_ANSWER || $answerType == GLOBAL_MULTIPLE_ANSWER) {
601
                            $s .= '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
602
                            $attributes['class'] = 'checkradios';
603
                            $answer_input = '<label class="checkbox">';
604
                            $answer_input .= Display::input(
605
                                'checkbox',
606
                                'choice['.$questionId.']['.$numAnswer.']',
607
                                $numAnswer,
608
                                $attributes
609
                            );
610
                            $answer_input .= $answer;
611
                            $answer_input .= '</label>';
612
613
                            if ($show_comment) {
614
                                $s .= '<tr><td>';
615
                                $s .= $answer_input;
616
                                $s .= '</td>';
617
                                $s .= '<td>';
618
                                $s .= $comment;
619
                                $s .= '</td>';
620
                                $s .= '</tr>';
621
                            } else {
622
                                $s .= $answer_input;
623
                            }
624
                        } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE) {
625
                            $myChoice = [];
626
                            if (!empty($userChoiceList)) {
627
                                foreach ($userChoiceList as $item) {
628
                                    $item = explode(':', $item);
629
                                    if (!empty($item)) {
630
                                        $myChoice[$item[0]] = isset($item[1]) ? $item[1] : '';
631
                                    }
632
                                }
633
                            }
634
635
                            $s .= '<tr>';
636
                            $s .= Display::tag('td', $answer);
637
638
                            if (!empty($quizQuestionOptions)) {
639
                                foreach ($quizQuestionOptions as $id => $item) {
640
                                    if (isset($myChoice[$numAnswer]) && $id == $myChoice[$numAnswer]) {
641
                                        $attributes = [
642
                                            'checked' => 1,
643
                                            'selected' => 1,
644
                                        ];
645
                                    } else {
646
                                        $attributes = [];
647
                                    }
648
649
                                    if ($debug_mark_answer) {
650
                                        if ($id == $answerCorrect) {
651
                                            $attributes['checked'] = 1;
652
                                            $attributes['selected'] = 1;
653
                                        }
654
                                    }
655
                                    $s .= Display::tag(
656
                                        'td',
657
                                        Display::input(
658
                                            'radio',
659
                                            'choice['.$questionId.']['.$numAnswer.']',
660
                                            $id,
661
                                            $attributes
662
                                        ),
663
                                        ['style' => '']
664
                                    );
665
                                }
666
                            }
667
668
                            if ($show_comment) {
669
                                $s .= '<td>';
670
                                $s .= $comment;
671
                                $s .= '</td>';
672
                            }
673
                            $s .= '</tr>';
674
                        } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
675
                            $myChoice = [];
676
                            if (!empty($userChoiceList)) {
677
                                foreach ($userChoiceList as $item) {
678
                                    $item = explode(':', $item);
679
                                    $myChoice[$item[0]] = $item[1];
680
                                }
681
                            }
682
                            $myChoiceDegreeCertainty = [];
683
                            if (!empty($userChoiceList)) {
684
                                foreach ($userChoiceList as $item) {
685
                                    $item = explode(':', $item);
686
                                    $myChoiceDegreeCertainty[$item[0]] = $item[2];
687
                                }
688
                            }
689
                            $s .= '<tr>';
690
                            $s .= Display::tag('td', $answer);
691
692
                            if (!empty($quizQuestionOptions)) {
693
                                foreach ($quizQuestionOptions as $id => $item) {
694
                                    if (isset($myChoice[$numAnswer]) && $id == $myChoice[$numAnswer]) {
695
                                        $attributes = ['checked' => 1, 'selected' => 1];
696
                                    } else {
697
                                        $attributes = [];
698
                                    }
699
                                    $attributes['onChange'] = 'RadioValidator('.$questionId.', '.$numAnswer.')';
700
701
                                    // radio button selection
702
                                    if (isset($myChoiceDegreeCertainty[$numAnswer]) &&
703
                                        $id == $myChoiceDegreeCertainty[$numAnswer]
704
                                    ) {
705
                                        $attributes1 = ['checked' => 1, 'selected' => 1];
706
                                    } else {
707
                                        $attributes1 = [];
708
                                    }
709
710
                                    $attributes1['onChange'] = 'RadioValidator('.$questionId.', '.$numAnswer.')';
711
712
                                    if ($debug_mark_answer) {
713
                                        if ($id == $answerCorrect) {
714
                                            $attributes['checked'] = 1;
715
                                            $attributes['selected'] = 1;
716
                                        }
717
                                    }
718
719
                                    if ($item['name'] == 'True' || $item['name'] == 'False') {
720
                                        $s .= Display::tag('td',
721
                                            Display::input('radio',
722
                                                'choice['.$questionId.']['.$numAnswer.']',
723
                                                $id,
724
                                                $attributes
725
                                            ),
726
                                            ['style' => 'text-align:center; background-color:#F7E1D7;',
727
                                                'onclick' => 'handleRadioRow(event, '.
728
                                                    $questionId.', '.
729
                                                    $numAnswer.')',
730
                                            ]
731
                                        );
732
                                    } else {
733
                                        $s .= Display::tag('td',
734
                                            Display::input('radio',
735
                                                'choiceDegreeCertainty['.$questionId.']['.$numAnswer.']',
736
                                                $id,
737
                                                $attributes1
738
                                            ),
739
                                            ['style' => 'text-align:center; background-color:#EFEFFC;',
740
                                                'onclick' => 'handleRadioRow(event, '.
741
                                                    $questionId.', '.
742
                                                    $numAnswer.')',
743
                                            ]
744
                                        );
745
                                    }
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
                        $answer = Security::remove_XSS($answer, STUDENT);
779
                        $answer_input = '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
780
                        $answer_input .= '<label class="checkbox">';
781
                        $answer_input .= Display::input(
782
                            'checkbox',
783
                            'choice['.$questionId.']['.$numAnswer.']',
784
                            1,
785
                            $attributes
786
                        );
787
                        $answer_input .= $answer;
788
                        $answer_input .= '</label>';
789
790
                        if ($show_comment) {
791
                            $s .= '<tr>';
792
                            $s .= '<td>';
793
                            $s .= $answer_input;
794
                            $s .= '</td>';
795
                            $s .= '<td>';
796
                            $s .= $comment;
797
                            $s .= '</td>';
798
                            $s .= '</tr>';
799
                        } else {
800
                            $s .= $answer_input;
801
                        }
802
                        break;
803
                    case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
804
                        $s .= '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
805
                        $myChoice = [];
806
                        if (!empty($userChoiceList)) {
807
                            foreach ($userChoiceList as $item) {
808
                                $item = explode(':', $item);
809
                                if (isset($item[1]) && isset($item[0])) {
810
                                    $myChoice[$item[0]] = $item[1];
811
                                }
812
                            }
813
                        }
814
                        $answer = Security::remove_XSS($answer, STUDENT);
815
                        $s .= '<tr>';
816
                        $s .= Display::tag('td', $answer);
817
                        foreach ($objQuestionTmp->options as $key => $item) {
818
                            if (isset($myChoice[$numAnswer]) && $key == $myChoice[$numAnswer]) {
819
                                $attributes = [
820
                                    'checked' => 1,
821
                                    'selected' => 1,
822
                                ];
823
                            } else {
824
                                $attributes = [];
825
                            }
826
827
                            if ($debug_mark_answer) {
828
                                if ($key == $answerCorrect) {
829
                                    $attributes['checked'] = 1;
830
                                    $attributes['selected'] = 1;
831
                                }
832
                            }
833
                            $s .= Display::tag(
834
                                'td',
835
                                Display::input(
836
                                    'radio',
837
                                    'choice['.$questionId.']['.$numAnswer.']',
838
                                    $key,
839
                                    $attributes
840
                                )
841
                            );
842
                        }
843
844
                        if ($show_comment) {
845
                            $s .= '<td>';
846
                            $s .= $comment;
847
                            $s .= '</td>';
848
                        }
849
                        $s .= '</tr>';
850
                        break;
851
                    case FILL_IN_BLANKS:
852
                        // display the question, with field empty, for student to fill it,
853
                        // or filled to display the answer in the Question preview of the exercise/admin.php page
854
                        $displayForStudent = true;
855
                        $listAnswerInfo = FillBlanks::getAnswerInfo($answer);
856
                        // Correct answers
857
                        $correctAnswerList = $listAnswerInfo['words'];
858
                        // Student's answer
859
                        $studentAnswerList = [];
860
                        if (isset($user_choice[0]['answer'])) {
861
                            $arrayStudentAnswer = FillBlanks::getAnswerInfo(
862
                                $user_choice[0]['answer'],
863
                                true
864
                            );
865
                            $studentAnswerList = $arrayStudentAnswer['student_answer'];
866
                        }
867
868
                        // If the question must be shown with the answer (in page exercise/admin.php)
869
                        // for teacher preview set the student-answer to the correct answer
870
                        if ($debug_mark_answer) {
871
                            $studentAnswerList = $correctAnswerList;
872
                            $displayForStudent = false;
873
                        }
874
875
                        if (!empty($correctAnswerList) && !empty($studentAnswerList)) {
876
                            $answer = '';
877
                            for ($i = 0; $i < count($listAnswerInfo['common_words']) - 1; $i++) {
878
                                // display the common word
879
                                $answer .= $listAnswerInfo['common_words'][$i];
880
                                // display the blank word
881
                                $correctItem = $listAnswerInfo['words'][$i];
882
                                if (isset($studentAnswerList[$i])) {
883
                                    // If student already started this test and answered this question,
884
                                    // fill the blank with his previous answers
885
                                    // may be "" if student viewed the question, but did not fill the blanks
886
                                    $correctItem = $studentAnswerList[$i];
887
                                }
888
                                $attributes['style'] = 'width:'.$listAnswerInfo['input_size'][$i].'px';
889
                                $answer .= FillBlanks::getFillTheBlankHtml(
890
                                    $current_item,
891
                                    $questionId,
892
                                    $correctItem,
893
                                    $attributes,
894
                                    $answer,
895
                                    $listAnswerInfo,
896
                                    $displayForStudent,
897
                                    $i
898
                                );
899
                            }
900
                            // display the last common word
901
                            $answer .= $listAnswerInfo['common_words'][$i];
902
                        } else {
903
                            // display empty [input] with the right width for student to fill it
904
                            $answer = '';
905
                            for ($i = 0; $i < count($listAnswerInfo['common_words']) - 1; $i++) {
906
                                // display the common words
907
                                $answer .= $listAnswerInfo['common_words'][$i];
908
                                // display the blank word
909
                                $attributes['style'] = 'width:'.$listAnswerInfo['input_size'][$i].'px';
910
                                $answer .= FillBlanks::getFillTheBlankHtml(
911
                                    $current_item,
912
                                    $questionId,
913
                                    '',
914
                                    $attributes,
915
                                    $answer,
916
                                    $listAnswerInfo,
917
                                    $displayForStudent,
918
                                    $i
919
                                );
920
                            }
921
                            // display the last common word
922
                            $answer .= $listAnswerInfo['common_words'][$i];
923
                        }
924
                        $s .= $answer;
925
                        break;
926
                    case CALCULATED_ANSWER:
927
                        /*
928
                         * In the CALCULATED_ANSWER test
929
                         * you mustn't have [ and ] in the textarea
930
                         * you mustn't have @@ in the textarea
931
                         * the text to find mustn't be empty or contains only spaces
932
                         * the text to find mustn't contains HTML tags
933
                         * the text to find mustn't contains char "
934
                         */
935
                        if (null !== $origin) {
936
                            global $exe_id;
937
                            $exe_id = (int) $exe_id;
938
                            $trackAttempts = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
939
                            $sql = "SELECT answer FROM $trackAttempts
940
                                    WHERE exe_id = $exe_id AND question_id= $questionId";
941
                            $rsLastAttempt = Database::query($sql);
942
                            $rowLastAttempt = Database::fetch_array($rsLastAttempt);
943
944
                            $answer = null;
945
                            if (isset($rowLastAttempt['answer'])) {
946
                                $answer = $rowLastAttempt['answer'];
947
                            }
948
949
                            if (empty($answer)) {
950
                                $_SESSION['calculatedAnswerId'][$questionId] = mt_rand(
951
                                    1,
952
                                    $nbrAnswers
953
                                );
954
                                $answer = $objAnswerTmp->selectAnswer(
955
                                    $_SESSION['calculatedAnswerId'][$questionId]
956
                                );
957
                            }
958
                        }
959
960
                        list($answer) = explode('@@', $answer);
961
                        // $correctAnswerList array of array with correct anwsers 0=> [0=>[\p] 1=>[plop]]
962
                        api_preg_match_all(
963
                            '/\[[^]]+\]/',
964
                            $answer,
965
                            $correctAnswerList
966
                        );
967
968
                        // get student answer to display it if student go back
969
                        // to previous calculated answer question in a test
970
                        if (isset($user_choice[0]['answer'])) {
971
                            api_preg_match_all(
972
                                '/\[[^]]+\]/',
973
                                $answer,
974
                                $studentAnswerList
975
                            );
976
                            $studentAnswerListToClean = $studentAnswerList[0];
977
                            $studentAnswerList = [];
978
979
                            $maxStudents = count($studentAnswerListToClean);
980
                            for ($i = 0; $i < $maxStudents; $i++) {
981
                                $answerCorrected = $studentAnswerListToClean[$i];
982
                                $answerCorrected = api_preg_replace(
983
                                    '| / <font color="green"><b>.*$|',
984
                                    '',
985
                                    $answerCorrected
986
                                );
987
                                $answerCorrected = api_preg_replace(
988
                                    '/^\[/',
989
                                    '',
990
                                    $answerCorrected
991
                                );
992
                                $answerCorrected = api_preg_replace(
993
                                    '|^<font color="red"><s>|',
994
                                    '',
995
                                    $answerCorrected
996
                                );
997
                                $answerCorrected = api_preg_replace(
998
                                    '|</s></font>$|',
999
                                    '',
1000
                                    $answerCorrected
1001
                                );
1002
                                $answerCorrected = '['.$answerCorrected.']';
1003
                                $studentAnswerList[] = $answerCorrected;
1004
                            }
1005
                        }
1006
1007
                        // If display preview of answer in test view for exemple,
1008
                        // set the student answer to the correct answers
1009
                        if ($debug_mark_answer) {
1010
                            // contain the rights answers surronded with brackets
1011
                            $studentAnswerList = $correctAnswerList[0];
1012
                        }
1013
1014
                        /*
1015
                        Split the response by bracket
1016
                        tabComments is an array with text surrounding the text to find
1017
                        we add a space before and after the answerQuestion to be sure to
1018
                        have a block of text before and after [xxx] patterns
1019
                        so we have n text to find ([xxx]) and n+1 block of texts before,
1020
                        between and after the text to find
1021
                        */
1022
                        $tabComments = api_preg_split(
1023
                            '/\[[^]]+\]/',
1024
                            ' '.$answer.' '
1025
                        );
1026
                        if (!empty($correctAnswerList) && !empty($studentAnswerList)) {
1027
                            $answer = '';
1028
                            $i = 0;
1029
                            foreach ($studentAnswerList as $studentItem) {
1030
                                // Remove surronding brackets
1031
                                $studentResponse = api_substr(
1032
                                    $studentItem,
1033
                                    1,
1034
                                    api_strlen($studentItem) - 2
1035
                                );
1036
                                $size = strlen($studentItem);
1037
                                $attributes['class'] = self::detectInputAppropriateClass($size);
1038
                                $answer .= $tabComments[$i].
1039
                                    Display::input(
1040
                                        'text',
1041
                                        "choice[$questionId][]",
1042
                                        $studentResponse,
1043
                                        $attributes
1044
                                    );
1045
                                $i++;
1046
                            }
1047
                            $answer .= $tabComments[$i];
1048
                        } else {
1049
                            // display exercise with empty input fields
1050
                            // every [xxx] are replaced with an empty input field
1051
                            foreach ($correctAnswerList[0] as $item) {
1052
                                $size = strlen($item);
1053
                                $attributes['class'] = self::detectInputAppropriateClass($size);
1054
                                if (EXERCISE_FEEDBACK_TYPE_POPUP == $exercise->getFeedbackType()) {
1055
                                    $attributes['id'] = "question_$questionId";
1056
                                    $attributes['class'] .= ' checkCalculatedQuestionOnEnter ';
1057
                                }
1058
1059
                                $answer = str_replace(
1060
                                    $item,
1061
                                    Display::input(
1062
                                        'text',
1063
                                        "choice[$questionId][]",
1064
                                        '',
1065
                                        $attributes
1066
                                    ),
1067
                                    $answer
1068
                                );
1069
                            }
1070
                        }
1071
                        if (null !== $origin) {
1072
                            $s = $answer;
1073
                            break;
1074
                        } else {
1075
                            $s .= $answer;
1076
                        }
1077
                        break;
1078
                    case MATCHING:
1079
                        // matching type, showing suggestions and answers
1080
                        // TODO: replace $answerId by $numAnswer
1081
                        if ($answerCorrect != 0) {
1082
                            // only show elements to be answered (not the contents of
1083
                            // the select boxes, who are correct = 0)
1084
                            $s .= '<tr><td width="45%" valign="top">';
1085
                            $parsed_answer = $answer;
1086
                            // Left part questions
1087
                            $s .= '<p class="indent">'.$lines_count.'.&nbsp;'.$parsed_answer.'</p></td>';
1088
                            // Middle part (matches selects)
1089
                            // Id of select is # question + # of option
1090
                            $s .= '<td width="10%" valign="top" align="center">
1091
                                <div class="select-matching">
1092
                                <select
1093
                                    id="choice_id_'.$current_item.'_'.$lines_count.'"
1094
                                    name="choice['.$questionId.']['.$numAnswer.']">';
1095
1096
                            // fills the list-box
1097
                            foreach ($select_items as $key => $val) {
1098
                                // set $debug_mark_answer to true at function start to
1099
                                // show the correct answer with a suffix '-x'
1100
                                $selected = '';
1101
                                if ($debug_mark_answer) {
1102
                                    if ($val['id'] == $answerCorrect) {
1103
                                        $selected = 'selected="selected"';
1104
                                    }
1105
                                }
1106
                                //$user_choice_array_position
1107
                                if (isset($user_choice_array_position[$numAnswer]) &&
1108
                                    $val['id'] == $user_choice_array_position[$numAnswer]
1109
                                ) {
1110
                                    $selected = 'selected="selected"';
1111
                                }
1112
                                $s .= '<option value="'.$val['id'].'" '.$selected.'>'.$val['letter'].'</option>';
1113
                            }
1114
1115
                            $s .= '</select></div></td><td width="5%" class="separate">&nbsp;</td>';
1116
                            $s .= '<td width="40%" valign="top" >';
1117
                            if (isset($select_items[$lines_count])) {
1118
                                $s .= '<div class="text-right">
1119
                                        <p class="indent">'.
1120
                                    $select_items[$lines_count]['letter'].'.&nbsp; '.
1121
                                    $select_items[$lines_count]['answer'].'
1122
                                        </p>
1123
                                        </div>';
1124
                            } else {
1125
                                $s .= '&nbsp;';
1126
                            }
1127
                            $s .= '</td>';
1128
                            $s .= '</tr>';
1129
                            $lines_count++;
1130
                            // If the left side of the "matching" has been completely
1131
                            // shown but the right side still has values to show...
1132
                            if (($lines_count - 1) == $num_suggestions) {
1133
                                // if it remains answers to shown at the right side
1134
                                while (isset($select_items[$lines_count])) {
1135
                                    $s .= '<tr>
1136
                                      <td colspan="2"></td>
1137
                                      <td valign="top">';
1138
                                    $s .= '<b>'.$select_items[$lines_count]['letter'].'.</b> '.
1139
                                        $select_items[$lines_count]['answer'];
1140
                                    $s .= "</td>
1141
                                </tr>";
1142
                                    $lines_count++;
1143
                                }
1144
                            }
1145
                            $matching_correct_answer++;
1146
                        }
1147
                        break;
1148
                    case DRAGGABLE:
1149
                        if ($answerCorrect) {
1150
                            $windowId = $questionId.'_'.$lines_count;
1151
                            $s .= '<li class="touch-items" id="'.$windowId.'">';
1152
                            $s .= Display::div(
1153
                                $answer,
1154
                                [
1155
                                    'id' => "window_$windowId",
1156
                                    'class' => "window{$questionId}_question_draggable exercise-draggable-answer-option",
1157
                                ]
1158
                            );
1159
1160
                            $draggableSelectOptions = [];
1161
                            $selectedValue = 0;
1162
                            $selectedIndex = 0;
1163
                            if ($user_choice) {
1164
                                foreach ($user_choice as $userChoiceKey => $chosen) {
1165
                                    $userChoiceKey++;
1166
                                    if ($lines_count != $userChoiceKey) {
1167
                                        continue;
1168
                                    }
1169
                                    /*if ($answerCorrect != $chosen['answer']) {
1170
                                        continue;
1171
                                    }*/
1172
                                    $selectedValue = $chosen['answer'];
1173
                                }
1174
                            }
1175
                            foreach ($select_items as $key => $select_item) {
1176
                                $draggableSelectOptions[$select_item['id']] = $select_item['letter'];
1177
                            }
1178
1179
                            foreach ($draggableSelectOptions as $value => $text) {
1180
                                if ($value == $selectedValue) {
1181
                                    break;
1182
                                }
1183
                                $selectedIndex++;
1184
                            }
1185
1186
                            $s .= Display::select(
1187
                                "choice[$questionId][$numAnswer]",
1188
                                $draggableSelectOptions,
1189
                                $selectedValue,
1190
                                [
1191
                                    'id' => "window_{$windowId}_select",
1192
                                    'class' => 'select_option hidden',
1193
                                ],
1194
                                false
1195
                            );
1196
1197
                            if ($selectedValue && $selectedIndex) {
1198
                                $s .= "
1199
                                    <script>
1200
                                        $(function() {
1201
                                            DraggableAnswer.deleteItem(
1202
                                                $('#{$questionId}_$lines_count'),
1203
                                                $('#drop_{$questionId}_{$selectedIndex}')
1204
                                            );
1205
                                        });
1206
                                    </script>
1207
                                ";
1208
                            }
1209
1210
                            if (isset($select_items[$lines_count])) {
1211
                                $s .= Display::div(
1212
                                    Display::tag(
1213
                                        'b',
1214
                                        $select_items[$lines_count]['letter']
1215
                                    ).$select_items[$lines_count]['answer'],
1216
                                    [
1217
                                        'id' => "window_{$windowId}_answer",
1218
                                        'class' => 'hidden',
1219
                                    ]
1220
                                );
1221
                            } else {
1222
                                $s .= '&nbsp;';
1223
                            }
1224
1225
                            $lines_count++;
1226
                            if (($lines_count - 1) == $num_suggestions) {
1227
                                while (isset($select_items[$lines_count])) {
1228
                                    $s .= Display::tag('b', $select_items[$lines_count]['letter']);
1229
                                    $s .= $select_items[$lines_count]['answer'];
1230
                                    $lines_count++;
1231
                                }
1232
                            }
1233
1234
                            $matching_correct_answer++;
1235
                            $s .= '</li>';
1236
                        }
1237
                        break;
1238
                    case MATCHING_DRAGGABLE:
1239
                        if ($answerId == 1) {
1240
                            echo $objAnswerTmp->getJs();
1241
                        }
1242
                        if ($answerCorrect != 0) {
1243
                            $windowId = "{$questionId}_{$lines_count}";
1244
                            $s .= <<<HTML
1245
                            <tr>
1246
                                <td width="45%">
1247
                                    <div id="window_{$windowId}"
1248
                                        class="window window_left_question window{$questionId}_question">
1249
                                        <strong>$lines_count.</strong>
1250
                                        $answer
1251
                                    </div>
1252
                                </td>
1253
                                <td width="10%">
1254
HTML;
1255
1256
                            $draggableSelectOptions = [];
1257
                            $selectedValue = 0;
1258
                            $selectedIndex = 0;
1259
1260
                            if ($user_choice) {
1261
                                foreach ($user_choice as $chosen) {
1262
                                    if ($numAnswer == $chosen['position']) {
1263
                                        $selectedValue = $chosen['answer'];
1264
                                        break;
1265
                                    }
1266
                                }
1267
                            }
1268
1269
                            foreach ($select_items as $key => $selectItem) {
1270
                                $draggableSelectOptions[$selectItem['id']] = $selectItem['letter'];
1271
                            }
1272
1273
                            foreach ($draggableSelectOptions as $value => $text) {
1274
                                if ($value == $selectedValue) {
1275
                                    break;
1276
                                }
1277
                                $selectedIndex++;
1278
                            }
1279
1280
                            $s .= Display::select(
1281
                                "choice[$questionId][$numAnswer]",
1282
                                $draggableSelectOptions,
1283
                                $selectedValue,
1284
                                [
1285
                                    'id' => "window_{$windowId}_select",
1286
                                    'class' => 'hidden',
1287
                                ],
1288
                                false
1289
                            );
1290
1291
                            if (!empty($answerCorrect) && !empty($selectedValue)) {
1292
                                // Show connect if is not freeze (question preview)
1293
                                if (!$freeze) {
1294
                                    $s .= "
1295
                                        <script>
1296
                                            $(function() {
1297
                                                $(window).on('load', function() {
1298
                                                    jsPlumb.connect({
1299
                                                        source: 'window_$windowId',
1300
                                                        target: 'window_{$questionId}_{$selectedIndex}_answer',
1301
                                                        endpoint: ['Blank', {radius: 15}],
1302
                                                        anchors: ['RightMiddle', 'LeftMiddle'],
1303
                                                        paintStyle: {strokeStyle: '#8A8888', lineWidth: 8},
1304
                                                        connector: [
1305
                                                            MatchingDraggable.connectorType,
1306
                                                            {curvines: MatchingDraggable.curviness}
1307
                                                        ]
1308
                                                    });
1309
                                                });
1310
                                            });
1311
                                        </script>
1312
                                    ";
1313
                                }
1314
                            }
1315
1316
                            $s .= '</td><td width="45%">';
1317
                            if (isset($select_items[$lines_count])) {
1318
                                $s .= <<<HTML
1319
                                <div id="window_{$windowId}_answer" class="window window_right_question">
1320
                                    <strong>{$select_items[$lines_count]['letter']}.</strong>
1321
                                    {$select_items[$lines_count]['answer']}
1322
                                </div>
1323
HTML;
1324
                            } else {
1325
                                $s .= '&nbsp;';
1326
                            }
1327
1328
                            $s .= '</td></tr>';
1329
                            $lines_count++;
1330
                            if (($lines_count - 1) == $num_suggestions) {
1331
                                while (isset($select_items[$lines_count])) {
1332
                                    $s .= <<<HTML
1333
                                    <tr>
1334
                                        <td colspan="2"></td>
1335
                                        <td>
1336
                                            <strong>{$select_items[$lines_count]['letter']}</strong>
1337
                                            {$select_items[$lines_count]['answer']}
1338
                                        </td>
1339
                                    </tr>
1340
HTML;
1341
                                    $lines_count++;
1342
                                }
1343
                            }
1344
                            $matching_correct_answer++;
1345
                        }
1346
                        break;
1347
                }
1348
            }
1349
1350
            if ($show_comment) {
1351
                $s .= '</table>';
1352
            } elseif (in_array(
1353
                $answerType,
1354
                [
1355
                    MATCHING,
1356
                    MATCHING_DRAGGABLE,
1357
                    UNIQUE_ANSWER_NO_OPTION,
1358
                    MULTIPLE_ANSWER_TRUE_FALSE,
1359
                    MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE,
1360
                    MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY,
1361
                ]
1362
            )) {
1363
                $s .= '</table>';
1364
            }
1365
1366
            if ($answerType == DRAGGABLE) {
1367
                $isVertical = $objQuestionTmp->extra == 'v';
1368
                $s .= "
1369
                           </ul>
1370
                        </div><!-- .col-md-12 -->
1371
                    </div><!-- .row -->
1372
                ";
1373
                $counterAnswer = 1;
1374
                $s .= $isVertical ? '' : '<div class="row">';
1375
                for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
1376
                    $answerCorrect = $objAnswerTmp->isCorrect($answerId);
1377
                    $windowId = $questionId.'_'.$counterAnswer;
1378
                    if ($answerCorrect) {
1379
                        $s .= $isVertical ? '<div class="row">' : '';
1380
                        $s .= '
1381
                            <div class="'.($isVertical ? 'col-md-12' : 'col-xs-12 col-sm-4 col-md-3 col-lg-2').'">
1382
                                <div class="droppable-item">
1383
                                    <span class="number">'.$counterAnswer.'.</span>
1384
                                    <div id="drop_'.$windowId.'" class="droppable">
1385
                                    </div>
1386
                                 </div>
1387
                            </div>
1388
                        ';
1389
                        $s .= $isVertical ? '</div>' : '';
1390
                        $counterAnswer++;
1391
                    }
1392
                }
1393
1394
                $s .= $isVertical ? '' : '</div>'; // row
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 ($answerType == HOT_SPOT || $answerType == HOT_SPOT_DELINEATION) {
1413
            global $exe_id;
1414
            // Question is a HOT_SPOT
1415
            // Checking document/images visibility
1416
            if (api_is_platform_admin() || api_is_course_admin()) {
1417
                $doc_id = $objQuestionTmp->getPictureId();
1418
                if (is_numeric($doc_id)) {
1419
                    $images_folder_visibility = api_get_item_visibility(
1420
                        $course,
1421
                        'document',
1422
                        $doc_id,
1423
                        api_get_session_id()
1424
                    );
1425
                    if (!$images_folder_visibility) {
1426
                        // Show only to the course/platform admin if the image is set to visibility = false
1427
                        echo Display::return_message(
1428
                            get_lang('ChangeTheVisibilityOfTheCurrentImage'),
1429
                            'warning'
1430
                        );
1431
                    }
1432
                }
1433
            }
1434
            $questionDescription = $objQuestionTmp->selectDescription();
1435
1436
            // Get the answers, make a list
1437
            $objAnswerTmp = new Answer($questionId, $course_id);
1438
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
1439
1440
            // get answers of hotpost
1441
            $answers_hotspot = [];
1442
            for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
1443
                $answers = $objAnswerTmp->selectAnswerByAutoId(
1444
                    $objAnswerTmp->selectAutoId($answerId)
1445
                );
1446
                $answers_hotspot[$answers['id']] = $objAnswerTmp->selectAnswer(
1447
                    $answerId
1448
                );
1449
            }
1450
1451
            $answerList = '';
1452
            $hotspotColor = 0;
1453
            if ($answerType != HOT_SPOT_DELINEATION) {
1454
                $answerList = '
1455
                    <div class="well well-sm">
1456
                        <h5 class="page-header">'.get_lang('HotspotZones').'</h5>
1457
                        <ol>
1458
                ';
1459
1460
                if (!empty($answers_hotspot)) {
1461
                    Session::write("hotspot_ordered$questionId", array_keys($answers_hotspot));
1462
                    foreach ($answers_hotspot as $value) {
1463
                        $answerList .= '<li>';
1464
                        if ($freeze) {
1465
                            $answerList .= '<span class="hotspot-color-'.$hotspotColor
1466
                                .' fa fa-square" aria-hidden="true"></span>'.PHP_EOL;
1467
                        }
1468
                        $answerList .= $value;
1469
                        $answerList .= '</li>';
1470
                        $hotspotColor++;
1471
                    }
1472
                }
1473
1474
                $answerList .= '
1475
                        </ol>
1476
                    </div>
1477
                ';
1478
            }
1479
1480
            if ($freeze) {
1481
                $relPath = api_get_path(WEB_CODE_PATH);
1482
                echo "
1483
                    <div class=\"row\">
1484
                        <div class=\"col-sm-9\">
1485
                            <div id=\"hotspot-preview-$questionId\"></div>
1486
                        </div>
1487
                        <div class=\"col-sm-3\">
1488
                            $answerList
1489
                        </div>
1490
                    </div>
1491
                    <script>
1492
                        new ".($answerType == HOT_SPOT ? "HotspotQuestion" : "DelineationQuestion")."({
1493
                            questionId: $questionId,
1494
                            exerciseId: {$exercise->id},
1495
                            exeId: 0,
1496
                            selector: '#hotspot-preview-$questionId',
1497
                            'for': 'preview',
1498
                            relPath: '$relPath'
1499
                        });
1500
                    </script>
1501
                ";
1502
1503
                return;
1504
            }
1505
1506
            if (!$only_questions) {
1507
                if ($show_title) {
1508
                    if ($exercise->display_category_name) {
1509
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->id);
1510
                    }
1511
                    echo $objQuestionTmp->getTitleToDisplay($current_item);
1512
                }
1513
1514
                if ($questionRequireAuth) {
1515
                    WhispeakAuthPlugin::quizQuestionAuthentify($questionId, $exercise);
1516
1517
                    return false;
1518
                }
1519
1520
                //@todo I need to the get the feedback type
1521
                echo <<<HOTSPOT
1522
                    <input type="hidden" name="hidden_hotspot_id" value="$questionId" />
1523
                    <div class="exercise_questions">
1524
                        $questionDescription
1525
                        <div class="row">
1526
HOTSPOT;
1527
            }
1528
1529
            $relPath = api_get_path(WEB_CODE_PATH);
1530
            $s .= "<div class=\"col-sm-8 col-md-9\">
1531
                   <div class=\"hotspot-image\"></div>
1532
                    <script>
1533
                        $(function() {
1534
                            new ".($answerType == HOT_SPOT_DELINEATION ? 'DelineationQuestion' : 'HotspotQuestion')."({
1535
                                questionId: $questionId,
1536
                                exerciseId: {$exercise->id},
1537
                                exeId: 0,
1538
                                selector: '#question_div_' + $questionId + ' .hotspot-image',
1539
                                'for': 'user',
1540
                                relPath: '$relPath'
1541
                            });
1542
                        });
1543
                    </script>
1544
                </div>
1545
                <div class=\"col-sm-4 col-md-3\">
1546
                    $answerList
1547
                </div>
1548
            ";
1549
1550
            echo <<<HOTSPOT
1551
                            $s
1552
                        </div>
1553
                    </div>
1554
HOTSPOT;
1555
        } elseif ($answerType == ANNOTATION) {
1556
            global $exe_id;
1557
            $relPath = api_get_path(WEB_CODE_PATH);
1558
            if (api_is_platform_admin() || api_is_course_admin()) {
1559
                $docId = DocumentManager::get_document_id($course, '/images/'.$pictureName);
1560
                if ($docId) {
1561
                    $images_folder_visibility = api_get_item_visibility(
1562
                        $course,
1563
                        'document',
1564
                        $docId,
1565
                        api_get_session_id()
1566
                    );
1567
1568
                    if (!$images_folder_visibility) {
1569
                        echo Display::return_message(get_lang('ChangeTheVisibilityOfTheCurrentImage'), 'warning');
1570
                    }
1571
                }
1572
1573
                if ($freeze) {
1574
                    echo Display::img(
1575
                        api_get_path(WEB_COURSE_PATH).$course['path'].'/document/images/'.$pictureName,
1576
                        $objQuestionTmp->selectTitle(),
1577
                        ['width' => '600px']
1578
                    );
1579
1580
                    return 0;
1581
                }
1582
            }
1583
1584
            if (!$only_questions) {
1585
                if ($show_title) {
1586
                    if ($exercise->display_category_name) {
1587
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->id);
1588
                    }
1589
                    echo $objQuestionTmp->getTitleToDisplay($current_item);
1590
                }
1591
1592
                if ($questionRequireAuth) {
1593
                    WhispeakAuthPlugin::quizQuestionAuthentify($questionId, $exercise);
1594
1595
                    return false;
1596
                }
1597
1598
                echo '
1599
                    <input type="hidden" name="hidden_hotspot_id" value="'.$questionId.'" />
1600
                    <div class="exercise_questions">
1601
                        '.$objQuestionTmp->selectDescription().'
1602
                        <div class="row">
1603
                            <div class="col-sm-8 col-md-9">
1604
                                <div id="annotation-canvas-'.$questionId.'" class="annotation-canvas center-block">
1605
                                </div>
1606
                                <script>
1607
                                    AnnotationQuestion({
1608
                                        questionId: '.$questionId.',
1609
                                        exerciseId: '.$exerciseId.',
1610
                                        relPath: \''.$relPath.'\',
1611
                                        courseId: '.$course_id.',
1612
                                    });
1613
                                </script>
1614
                            </div>
1615
                            <div class="col-sm-4 col-md-3">
1616
                                <div class="well well-sm" id="annotation-toolbar-'.$questionId.'">
1617
                                    <div class="btn-toolbar">
1618
                                        <div class="btn-group" data-toggle="buttons">
1619
                                            <label class="btn btn-default active"
1620
                                                aria-label="'.get_lang('AddAnnotationPath').'">
1621
                                                <input
1622
                                                    type="radio" value="0"
1623
                                                    name="'.$questionId.'-options" autocomplete="off" checked>
1624
                                                <span class="fa fa-pencil" aria-hidden="true"></span>
1625
                                            </label>
1626
                                            <label class="btn btn-default"
1627
                                                aria-label="'.get_lang('AddAnnotationText').'">
1628
                                                <input
1629
                                                    type="radio" value="1"
1630
                                                    name="'.$questionId.'-options" autocomplete="off">
1631
                                                <span class="fa fa-font fa-fw" aria-hidden="true"></span>
1632
                                            </label>
1633
                                        </div>
1634
                                    </div>
1635
                                    <ul class="list-unstyled"></ul>
1636
                                </div>
1637
                            </div>
1638
                        </div>
1639
                    </div>
1640
                ';
1641
            }
1642
            $objAnswerTmp = new Answer($questionId);
1643
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
1644
            unset($objAnswerTmp, $objQuestionTmp);
1645
        }
1646
1647
        return $nbrAnswers;
1648
    }
1649
1650
    /**
1651
     * @param int $exeId
1652
     *
1653
     * @return array
1654
     */
1655
    public static function get_exercise_track_exercise_info($exeId)
1656
    {
1657
        $quizTable = Database::get_course_table(TABLE_QUIZ_TEST);
1658
        $trackExerciseTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1659
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
1660
        $exeId = (int) $exeId;
1661
        $result = [];
1662
        if (!empty($exeId)) {
1663
            $sql = " SELECT q.*, tee.*
1664
                FROM $quizTable as q
1665
                INNER JOIN $trackExerciseTable as tee
1666
                ON q.id = tee.exe_exo_id
1667
                INNER JOIN $courseTable c
1668
                ON c.id = tee.c_id
1669
                WHERE tee.exe_id = $exeId
1670
                AND q.c_id = c.id";
1671
1672
            $sqlResult = Database::query($sql);
1673
            if (Database::num_rows($sqlResult)) {
1674
                $result = Database::fetch_array($sqlResult, 'ASSOC');
1675
                $result['duration_formatted'] = '';
1676
                if (!empty($result['exe_duration'])) {
1677
                    $time = api_format_time($result['exe_duration'], 'js');
1678
                    $result['duration_formatted'] = $time;
1679
                }
1680
            }
1681
        }
1682
1683
        return $result;
1684
    }
1685
1686
    /**
1687
     * Validates the time control key.
1688
     *
1689
     * @param int $exercise_id
1690
     * @param int $lp_id
1691
     * @param int $lp_item_id
1692
     *
1693
     * @return bool
1694
     */
1695
    public static function exercise_time_control_is_valid(
1696
        $exercise_id,
1697
        $lp_id = 0,
1698
        $lp_item_id = 0
1699
    ) {
1700
        $course_id = api_get_course_int_id();
1701
        $exercise_id = (int) $exercise_id;
1702
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
1703
        $sql = "SELECT expired_time FROM $table
1704
                WHERE c_id = $course_id AND id = $exercise_id";
1705
        $result = Database::query($sql);
1706
        $row = Database::fetch_array($result, 'ASSOC');
1707
        if (!empty($row['expired_time'])) {
1708
            $current_expired_time_key = self::get_time_control_key(
1709
                $exercise_id,
1710
                $lp_id,
1711
                $lp_item_id
1712
            );
1713
            if (isset($_SESSION['expired_time'][$current_expired_time_key])) {
1714
                $current_time = time();
1715
                $expired_time = api_strtotime(
1716
                    $_SESSION['expired_time'][$current_expired_time_key],
1717
                    'UTC'
1718
                );
1719
                $total_time_allowed = $expired_time + 30;
1720
                if ($total_time_allowed < $current_time) {
1721
                    return false;
1722
                }
1723
1724
                return true;
1725
            }
1726
1727
            return false;
1728
        }
1729
1730
        return true;
1731
    }
1732
1733
    /**
1734
     * Deletes the time control token.
1735
     *
1736
     * @param int $exercise_id
1737
     * @param int $lp_id
1738
     * @param int $lp_item_id
1739
     */
1740
    public static function exercise_time_control_delete(
1741
        $exercise_id,
1742
        $lp_id = 0,
1743
        $lp_item_id = 0
1744
    ) {
1745
        $current_expired_time_key = self::get_time_control_key(
1746
            $exercise_id,
1747
            $lp_id,
1748
            $lp_item_id
1749
        );
1750
        unset($_SESSION['expired_time'][$current_expired_time_key]);
1751
    }
1752
1753
    /**
1754
     * Generates the time control key.
1755
     *
1756
     * @param int $exercise_id
1757
     * @param int $lp_id
1758
     * @param int $lp_item_id
1759
     *
1760
     * @return string
1761
     */
1762
    public static function get_time_control_key(
1763
        $exercise_id,
1764
        $lp_id = 0,
1765
        $lp_item_id = 0
1766
    ) {
1767
        $exercise_id = (int) $exercise_id;
1768
        $lp_id = (int) $lp_id;
1769
        $lp_item_id = (int) $lp_item_id;
1770
1771
        return
1772
            api_get_course_int_id().'_'.
1773
            api_get_session_id().'_'.
1774
            $exercise_id.'_'.
1775
            api_get_user_id().'_'.
1776
            $lp_id.'_'.
1777
            $lp_item_id;
1778
    }
1779
1780
    /**
1781
     * Get session time control.
1782
     *
1783
     * @param int $exercise_id
1784
     * @param int $lp_id
1785
     * @param int $lp_item_id
1786
     *
1787
     * @return int
1788
     */
1789
    public static function get_session_time_control_key(
1790
        $exercise_id,
1791
        $lp_id = 0,
1792
        $lp_item_id = 0
1793
    ) {
1794
        $return_value = 0;
1795
        $time_control_key = self::get_time_control_key(
1796
            $exercise_id,
1797
            $lp_id,
1798
            $lp_item_id
1799
        );
1800
        if (isset($_SESSION['expired_time']) && isset($_SESSION['expired_time'][$time_control_key])) {
1801
            $return_value = $_SESSION['expired_time'][$time_control_key];
1802
        }
1803
1804
        return $return_value;
1805
    }
1806
1807
    /**
1808
     * Gets count of exam results.
1809
     *
1810
     * @param int    $exerciseId
1811
     * @param array  $conditions
1812
     * @param string $courseCode
1813
     * @param bool   $showSession
1814
     *
1815
     * @return array
1816
     */
1817
    public static function get_count_exam_results($exerciseId, $conditions, $courseCode = '', $showSession = false)
1818
    {
1819
        $count = self::get_exam_results_data(
1820
            null,
1821
            null,
1822
            null,
1823
            null,
1824
            $exerciseId,
1825
            $conditions,
1826
            true,
1827
            $courseCode,
1828
            $showSession
1829
        );
1830
1831
        return $count;
1832
    }
1833
1834
    /**
1835
     * @param string $path
1836
     *
1837
     * @return int
1838
     */
1839
    public static function get_count_exam_hotpotatoes_results($path)
1840
    {
1841
        return self::get_exam_results_hotpotatoes_data(
1842
            0,
1843
            0,
1844
            '',
1845
            '',
1846
            $path,
1847
            true,
1848
            ''
1849
        );
1850
    }
1851
1852
    /**
1853
     * @param int    $in_from
1854
     * @param int    $in_number_of_items
1855
     * @param int    $in_column
1856
     * @param int    $in_direction
1857
     * @param string $in_hotpot_path
1858
     * @param bool   $in_get_count
1859
     * @param null   $where_condition
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $where_condition is correct as it would always require null to be passed?
Loading history...
1860
     *
1861
     * @return array|int
1862
     */
1863
    public static function get_exam_results_hotpotatoes_data(
1864
        $in_from,
1865
        $in_number_of_items,
1866
        $in_column,
1867
        $in_direction,
1868
        $in_hotpot_path,
1869
        $in_get_count = false,
1870
        $where_condition = null
1871
    ) {
1872
        $courseId = api_get_course_int_id();
1873
        // by default in_column = 1 If parameters given, it is the name of the column witch is the bdd field name
1874
        if ($in_column == 1) {
1875
            $in_column = 'firstname';
1876
        }
1877
        $in_hotpot_path = Database::escape_string($in_hotpot_path);
1878
        $in_direction = Database::escape_string($in_direction);
1879
        $in_column = Database::escape_string($in_column);
1880
        $in_number_of_items = intval($in_number_of_items);
1881
        $in_from = (int) $in_from;
1882
1883
        $TBL_TRACK_HOTPOTATOES = Database::get_main_table(
1884
            TABLE_STATISTIC_TRACK_E_HOTPOTATOES
1885
        );
1886
        $TBL_USER = Database::get_main_table(TABLE_MAIN_USER);
1887
1888
        $sql = "SELECT *, thp.id AS thp_id
1889
                FROM $TBL_TRACK_HOTPOTATOES thp
1890
                JOIN $TBL_USER u
1891
                ON thp.exe_user_id = u.user_id
1892
                WHERE
1893
                    thp.c_id = $courseId AND
1894
                    exe_name LIKE '$in_hotpot_path%'";
1895
1896
        // just count how many answers
1897
        if ($in_get_count) {
1898
            $res = Database::query($sql);
1899
1900
            return Database::num_rows($res);
1901
        }
1902
        // get a number of sorted results
1903
        $sql .= " $where_condition
1904
            ORDER BY $in_column $in_direction
1905
            LIMIT $in_from, $in_number_of_items";
1906
1907
        $res = Database::query($sql);
1908
        $result = [];
1909
        $apiIsAllowedToEdit = api_is_allowed_to_edit();
1910
        $urlBase = api_get_path(WEB_CODE_PATH).
1911
            'exercise/hotpotatoes_exercise_report.php?action=delete&'.
1912
            api_get_cidreq().'&id=';
1913
        while ($data = Database::fetch_array($res)) {
1914
            $actions = null;
1915
1916
            if ($apiIsAllowedToEdit) {
1917
                $url = $urlBase.$data['thp_id'].'&path='.$data['exe_name'];
1918
                $actions = Display::url(
1919
                    Display::return_icon('delete.png', get_lang('Delete')),
1920
                    $url
1921
                );
1922
            }
1923
1924
            $result[] = [
1925
                'firstname' => $data['firstname'],
1926
                'lastname' => $data['lastname'],
1927
                'username' => $data['username'],
1928
                'group_name' => implode(
1929
                    '<br/>',
1930
                    GroupManager::get_user_group_name($data['user_id'])
1931
                ),
1932
                'exe_date' => $data['exe_date'],
1933
                'score' => $data['exe_result'].' / '.$data['exe_weighting'],
1934
                'actions' => $actions,
1935
            ];
1936
        }
1937
1938
        return $result;
1939
    }
1940
1941
    /**
1942
     * @param string $exercisePath
1943
     * @param int    $userId
1944
     * @param int    $courseId
1945
     * @param int    $sessionId
1946
     *
1947
     * @return array
1948
     */
1949
    public static function getLatestHotPotatoResult(
1950
        $exercisePath,
1951
        $userId,
1952
        $courseId,
1953
        $sessionId
1954
    ) {
1955
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES);
1956
        $exercisePath = Database::escape_string($exercisePath);
1957
        $userId = (int) $userId;
1958
        $courseId = (int) $courseId;
1959
1960
        $sql = "SELECT * FROM $table
1961
                WHERE
1962
                    c_id = $courseId AND
1963
                    exe_name LIKE '$exercisePath%' AND
1964
                    exe_user_id = $userId
1965
                ORDER BY id
1966
                LIMIT 1";
1967
        $result = Database::query($sql);
1968
        $attempt = [];
1969
        if (Database::num_rows($result)) {
1970
            $attempt = Database::fetch_array($result, 'ASSOC');
1971
        }
1972
1973
        return $attempt;
1974
    }
1975
1976
    /**
1977
     * Gets the exam'data results.
1978
     *
1979
     * @todo this function should be moved in a library  + no global calls
1980
     *
1981
     * @param int    $from
1982
     * @param int    $number_of_items
1983
     * @param int    $column
1984
     * @param string $direction
1985
     * @param int    $exercise_id
1986
     * @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...
1987
     * @param bool   $get_count
1988
     * @param string $courseCode
1989
     * @param bool   $showSessionField
1990
     * @param bool   $showExerciseCategories
1991
     * @param array  $userExtraFieldsToAdd
1992
     * @param bool   $useCommaAsDecimalPoint
1993
     * @param bool   $roundValues
1994
     * @param bool   $getOnyIds
1995
     *
1996
     * @return array
1997
     */
1998
    public static function get_exam_results_data(
1999
        $from,
2000
        $number_of_items,
2001
        $column,
2002
        $direction,
2003
        $exercise_id,
2004
        $extra_where_conditions = null,
2005
        $get_count = false,
2006
        $courseCode = null,
2007
        $showSessionField = false,
2008
        $showExerciseCategories = false,
2009
        $userExtraFieldsToAdd = [],
2010
        $useCommaAsDecimalPoint = false,
2011
        $roundValues = false,
2012
        $getOnyIds = false
2013
    ) {
2014
        //@todo replace all this globals
2015
        global $filter;
2016
        $courseCode = empty($courseCode) ? api_get_course_id() : $courseCode;
2017
        $courseInfo = api_get_course_info($courseCode);
2018
2019
        if (empty($courseInfo)) {
2020
            return [];
2021
        }
2022
2023
        $documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
2024
2025
        $course_id = $courseInfo['real_id'];
2026
        $sessionId = api_get_session_id();
2027
        $exercise_id = (int) $exercise_id;
2028
2029
        $is_allowedToEdit =
2030
            api_is_allowed_to_edit(null, true) ||
2031
            api_is_allowed_to_edit(true) ||
2032
            api_is_drh() ||
2033
            api_is_student_boss() ||
2034
            api_is_session_admin();
2035
        $TBL_USER = Database::get_main_table(TABLE_MAIN_USER);
2036
        $TBL_EXERCICES = Database::get_course_table(TABLE_QUIZ_TEST);
2037
        $TBL_GROUP_REL_USER = Database::get_course_table(TABLE_GROUP_USER);
2038
        $TBL_GROUP = Database::get_course_table(TABLE_GROUP);
2039
        $TBL_TRACK_EXERCICES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2040
        $TBL_TRACK_HOTPOTATOES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES);
2041
        $TBL_TRACK_ATTEMPT_RECORDING = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING);
2042
2043
        $session_id_and = '';
2044
        $sessionCondition = '';
2045
        if (!$showSessionField) {
2046
            $session_id_and = " AND te.session_id = $sessionId ";
2047
            $sessionCondition = " AND ttte.session_id = $sessionId";
2048
        }
2049
2050
        $exercise_where = '';
2051
        if (!empty($exercise_id)) {
2052
            $exercise_where .= ' AND te.exe_exo_id = '.$exercise_id.'  ';
2053
        }
2054
2055
        $hotpotatoe_where = '';
2056
        if (!empty($_GET['path'])) {
2057
            $hotpotatoe_path = Database::escape_string($_GET['path']);
2058
            $hotpotatoe_where .= ' AND exe_name = "'.$hotpotatoe_path.'"  ';
2059
        }
2060
2061
        // sql for chamilo-type tests for teacher / tutor view
2062
        $sql_inner_join_tbl_track_exercices = "
2063
        (
2064
            SELECT DISTINCT ttte.*, if(tr.exe_id,1, 0) as revised
2065
            FROM $TBL_TRACK_EXERCICES ttte
2066
            LEFT JOIN $TBL_TRACK_ATTEMPT_RECORDING tr
2067
            ON (ttte.exe_id = tr.exe_id)
2068
            WHERE
2069
                c_id = $course_id AND
2070
                exe_exo_id = $exercise_id
2071
                $sessionCondition
2072
        )";
2073
2074
        if ($is_allowedToEdit) {
2075
            //@todo fix to work with COURSE_RELATION_TYPE_RRHH in both queries
2076
            // Hack in order to filter groups
2077
            $sql_inner_join_tbl_user = '';
2078
            if (strpos($extra_where_conditions, 'group_id')) {
2079
                $sql_inner_join_tbl_user = "
2080
                (
2081
                    SELECT
2082
                        u.user_id,
2083
                        firstname,
2084
                        lastname,
2085
                        official_code,
2086
                        email,
2087
                        username,
2088
                        g.name as group_name,
2089
                        g.id as group_id
2090
                    FROM $TBL_USER u
2091
                    INNER JOIN $TBL_GROUP_REL_USER gru
2092
                    ON (gru.user_id = u.user_id AND gru.c_id= $course_id )
2093
                    INNER JOIN $TBL_GROUP g
2094
                    ON (gru.group_id = g.id AND g.c_id= $course_id )
2095
                )";
2096
            }
2097
2098
            if (strpos($extra_where_conditions, 'group_all')) {
2099
                $extra_where_conditions = str_replace(
2100
                    "AND (  group_id = 'group_all'  )",
2101
                    '',
2102
                    $extra_where_conditions
2103
                );
2104
                $extra_where_conditions = str_replace(
2105
                    "AND group_id = 'group_all'",
2106
                    '',
2107
                    $extra_where_conditions
2108
                );
2109
                $extra_where_conditions = str_replace(
2110
                    "group_id = 'group_all' AND",
2111
                    '',
2112
                    $extra_where_conditions
2113
                );
2114
2115
                $sql_inner_join_tbl_user = "
2116
                (
2117
                    SELECT
2118
                        u.user_id,
2119
                        firstname,
2120
                        lastname,
2121
                        official_code,
2122
                        email,
2123
                        username,
2124
                        '' as group_name,
2125
                        '' as group_id
2126
                    FROM $TBL_USER u
2127
                )";
2128
                $sql_inner_join_tbl_user = null;
2129
            }
2130
2131
            if (strpos($extra_where_conditions, 'group_none')) {
2132
                $extra_where_conditions = str_replace(
2133
                    "AND (  group_id = 'group_none'  )",
2134
                    "AND (  group_id is null  )",
2135
                    $extra_where_conditions
2136
                );
2137
                $extra_where_conditions = str_replace(
2138
                    "AND group_id = 'group_none'",
2139
                    "AND (  group_id is null  )",
2140
                    $extra_where_conditions
2141
                );
2142
                $sql_inner_join_tbl_user = "
2143
            (
2144
                SELECT
2145
                    u.user_id,
2146
                    firstname,
2147
                    lastname,
2148
                    official_code,
2149
                    email,
2150
                    username,
2151
                    g.name as group_name,
2152
                    g.id as group_id
2153
                FROM $TBL_USER u
2154
                LEFT OUTER JOIN $TBL_GROUP_REL_USER gru
2155
                ON ( gru.user_id = u.user_id AND gru.c_id= $course_id )
2156
                LEFT OUTER JOIN $TBL_GROUP g
2157
                ON (gru.group_id = g.id AND g.c_id = $course_id )
2158
            )";
2159
            }
2160
2161
            // All
2162
            $is_empty_sql_inner_join_tbl_user = false;
2163
            if (empty($sql_inner_join_tbl_user)) {
2164
                $is_empty_sql_inner_join_tbl_user = true;
2165
                $sql_inner_join_tbl_user = "
2166
            (
2167
                SELECT u.user_id, firstname, lastname, email, username, ' ' as group_name, '' as group_id, official_code
2168
                FROM $TBL_USER u
2169
                WHERE u.status NOT IN(".api_get_users_status_ignored_in_reports('string').")
2170
            )";
2171
            }
2172
2173
            $sqlFromOption = " , $TBL_GROUP_REL_USER AS gru ";
2174
            $sqlWhereOption = "  AND gru.c_id = $course_id AND gru.user_id = user.user_id ";
2175
            $first_and_last_name = api_is_western_name_order() ? "firstname, lastname" : "lastname, firstname";
2176
2177
            if ($get_count) {
2178
                $sql_select = 'SELECT count(te.exe_id) ';
2179
            } else {
2180
                $sql_select = "SELECT DISTINCT
2181
                    user_id,
2182
                    $first_and_last_name,
2183
                    official_code,
2184
                    ce.title,
2185
                    username,
2186
                    te.exe_result,
2187
                    te.exe_weighting,
2188
                    te.exe_date,
2189
                    te.exe_id,
2190
                    te.session_id,
2191
                    email as exemail,
2192
                    te.start_date,
2193
                    ce.expired_time,
2194
                    steps_counter,
2195
                    exe_user_id,
2196
                    te.exe_duration,
2197
                    te.status as completion_status,
2198
                    propagate_neg,
2199
                    revised,
2200
                    group_name,
2201
                    group_id,
2202
                    orig_lp_id,
2203
                    te.user_ip";
2204
            }
2205
2206
            $sql = " $sql_select
2207
                FROM $TBL_EXERCICES AS ce
2208
                INNER JOIN $sql_inner_join_tbl_track_exercices AS te
2209
                ON (te.exe_exo_id = ce.id)
2210
                INNER JOIN $sql_inner_join_tbl_user AS user
2211
                ON (user.user_id = exe_user_id)
2212
                WHERE
2213
                    te.c_id = $course_id $session_id_and AND
2214
                    ce.active <> -1 AND
2215
                    ce.c_id = $course_id
2216
                    $exercise_where
2217
                    $extra_where_conditions
2218
                ";
2219
2220
            // sql for hotpotatoes tests for teacher / tutor view
2221
            if ($get_count) {
2222
                $hpsql_select = ' SELECT count(username) ';
2223
            } else {
2224
                $hpsql_select = " SELECT
2225
                    $first_and_last_name ,
2226
                    username,
2227
                    official_code,
2228
                    tth.exe_name,
2229
                    tth.exe_result ,
2230
                    tth.exe_weighting,
2231
                    tth.exe_date";
2232
            }
2233
2234
            $hpsql = " $hpsql_select
2235
                FROM
2236
                    $TBL_TRACK_HOTPOTATOES tth,
2237
                    $TBL_USER user
2238
                    $sqlFromOption
2239
                WHERE
2240
                    user.user_id=tth.exe_user_id
2241
                    AND tth.c_id = $course_id
2242
                    $hotpotatoe_where
2243
                    $sqlWhereOption
2244
                    AND user.status NOT IN (".api_get_users_status_ignored_in_reports('string').")
2245
                ORDER BY tth.c_id ASC, tth.exe_date DESC ";
2246
        }
2247
2248
        if (empty($sql)) {
2249
            return false;
2250
        }
2251
2252
        if ($get_count) {
2253
            $resx = Database::query($sql);
2254
            $rowx = Database::fetch_row($resx, 'ASSOC');
2255
2256
            return $rowx[0];
2257
        }
2258
2259
        $teacher_list = CourseManager::get_teacher_list_from_course_code($courseCode);
2260
        $teacher_id_list = [];
2261
        if (!empty($teacher_list)) {
2262
            foreach ($teacher_list as $teacher) {
2263
                $teacher_id_list[] = $teacher['user_id'];
2264
            }
2265
        }
2266
2267
        $scoreDisplay = new ScoreDisplay();
2268
        $decimalSeparator = '.';
2269
        $thousandSeparator = ',';
2270
2271
        if ($useCommaAsDecimalPoint) {
2272
            $decimalSeparator = ',';
2273
            $thousandSeparator = '';
2274
        }
2275
2276
        $listInfo = [];
2277
        // Simple exercises
2278
        if (empty($hotpotatoe_where)) {
2279
            $column = !empty($column) ? Database::escape_string($column) : null;
2280
            $from = (int) $from;
2281
            $number_of_items = (int) $number_of_items;
2282
2283
            if (!empty($column)) {
2284
                $sql .= " ORDER BY $column $direction ";
2285
            }
2286
2287
            if (!$getOnyIds) {
2288
                $sql .= " LIMIT $from, $number_of_items";
2289
            }
2290
2291
            $results = [];
2292
            $resx = Database::query($sql);
2293
            while ($rowx = Database::fetch_array($resx, 'ASSOC')) {
2294
                $results[] = $rowx;
2295
            }
2296
2297
            $group_list = GroupManager::get_group_list(null, $courseInfo);
2298
            $clean_group_list = [];
2299
            if (!empty($group_list)) {
2300
                foreach ($group_list as $group) {
2301
                    $clean_group_list[$group['id']] = $group['name'];
2302
                }
2303
            }
2304
2305
            $lp_list_obj = new LearnpathList(api_get_user_id());
2306
            $lp_list = $lp_list_obj->get_flat_list();
2307
            $oldIds = array_column($lp_list, 'lp_old_id', 'iid');
2308
2309
            if (is_array($results)) {
2310
                $users_array_id = [];
2311
                $from_gradebook = false;
2312
                if (isset($_GET['gradebook']) && $_GET['gradebook'] == 'view') {
2313
                    $from_gradebook = true;
2314
                }
2315
                $sizeof = count($results);
2316
                $locked = api_resource_is_locked_by_gradebook(
2317
                    $exercise_id,
2318
                    LINK_EXERCISE
2319
                );
2320
2321
                $timeNow = strtotime(api_get_utc_datetime());
2322
                // Looping results
2323
                for ($i = 0; $i < $sizeof; $i++) {
2324
                    $revised = $results[$i]['revised'];
2325
                    if ($results[$i]['completion_status'] == 'incomplete') {
2326
                        // If the exercise was incomplete, we need to determine
2327
                        // if it is still into the time allowed, or if its
2328
                        // allowed time has expired and it can be closed
2329
                        // (it's "unclosed")
2330
                        $minutes = $results[$i]['expired_time'];
2331
                        if ($minutes == 0) {
2332
                            // There's no time limit, so obviously the attempt
2333
                            // can still be "ongoing", but the teacher should
2334
                            // be able to choose to close it, so mark it as
2335
                            // "unclosed" instead of "ongoing"
2336
                            $revised = 2;
2337
                        } else {
2338
                            $allowedSeconds = $minutes * 60;
2339
                            $timeAttemptStarted = strtotime($results[$i]['start_date']);
2340
                            $secondsSinceStart = $timeNow - $timeAttemptStarted;
2341
                            if ($secondsSinceStart > $allowedSeconds) {
2342
                                $revised = 2; // mark as "unclosed"
2343
                            } else {
2344
                                $revised = 3; // mark as "ongoing"
2345
                            }
2346
                        }
2347
                    }
2348
2349
                    if ($from_gradebook && ($is_allowedToEdit)) {
2350
                        if (in_array(
2351
                            $results[$i]['username'].$results[$i]['firstname'].$results[$i]['lastname'],
2352
                            $users_array_id
2353
                        )) {
2354
                            continue;
2355
                        }
2356
                        $users_array_id[] = $results[$i]['username'].$results[$i]['firstname'].$results[$i]['lastname'];
2357
                    }
2358
2359
                    $lp_obj = isset($results[$i]['orig_lp_id']) && isset($lp_list[$results[$i]['orig_lp_id']]) ? $lp_list[$results[$i]['orig_lp_id']] : null;
2360
                    if (empty($lp_obj)) {
2361
                        // Try to get the old id (id instead of iid)
2362
                        $lpNewId = isset($results[$i]['orig_lp_id']) && isset($oldIds[$results[$i]['orig_lp_id']]) ? $oldIds[$results[$i]['orig_lp_id']] : null;
2363
                        if ($lpNewId) {
2364
                            $lp_obj = isset($lp_list[$lpNewId]) ? $lp_list[$lpNewId] : null;
2365
                        }
2366
                    }
2367
                    $lp_name = null;
2368
                    if ($lp_obj) {
2369
                        $url = api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?'.api_get_cidreq().'&action=view&lp_id='.$results[$i]['orig_lp_id'];
2370
                        $lp_name = Display::url(
2371
                            $lp_obj['lp_name'],
2372
                            $url,
2373
                            ['target' => '_blank']
2374
                        );
2375
                    }
2376
2377
                    // Add all groups by user
2378
                    $group_name_list = '';
2379
                    if ($is_empty_sql_inner_join_tbl_user) {
2380
                        $group_list = GroupManager::get_group_ids(
2381
                            api_get_course_int_id(),
2382
                            $results[$i]['user_id']
2383
                        );
2384
2385
                        foreach ($group_list as $id) {
2386
                            if (isset($clean_group_list[$id])) {
2387
                                $group_name_list .= $clean_group_list[$id].'<br/>';
2388
                            }
2389
                        }
2390
                        $results[$i]['group_name'] = $group_name_list;
2391
                    }
2392
2393
                    $results[$i]['exe_duration'] = !empty($results[$i]['exe_duration']) ? round($results[$i]['exe_duration'] / 60) : 0;
2394
                    $id = $results[$i]['exe_id'];
2395
                    $dt = api_convert_and_format_date($results[$i]['exe_weighting']);
2396
2397
                    // we filter the results if we have the permission to
2398
                    $result_disabled = 0;
2399
                    if (isset($results[$i]['results_disabled'])) {
2400
                        $result_disabled = (int) $results[$i]['results_disabled'];
2401
                    }
2402
                    if ($result_disabled == 0) {
2403
                        $my_res = $results[$i]['exe_result'];
2404
                        $my_total = $results[$i]['exe_weighting'];
2405
                        $results[$i]['start_date'] = api_get_local_time($results[$i]['start_date']);
2406
                        $results[$i]['exe_date'] = api_get_local_time($results[$i]['exe_date']);
2407
2408
                        if (!$results[$i]['propagate_neg'] && $my_res < 0) {
2409
                            $my_res = 0;
2410
                        }
2411
2412
                        $score = self::show_score(
2413
                            $my_res,
2414
                            $my_total,
2415
                            true,
2416
                            true,
2417
                            false,
2418
                            false,
2419
                            $decimalSeparator,
2420
                            $thousandSeparator,
2421
                            $roundValues
2422
                        );
2423
2424
                        $actions = '<div class="pull-right">';
2425
                        if ($is_allowedToEdit) {
2426
                            if (isset($teacher_id_list)) {
2427
                                if (in_array(
2428
                                    $results[$i]['exe_user_id'],
2429
                                    $teacher_id_list
2430
                                )) {
2431
                                    $actions .= Display::return_icon('teacher.png', get_lang('Teacher'));
2432
                                }
2433
                            }
2434
                            $revisedLabel = '';
2435
                            switch ($revised) {
2436
                                case 0:
2437
                                    $actions .= "<a href='exercise_show.php?".api_get_cidreq()."&action=qualify&id=$id'>".
2438
                                        Display:: return_icon(
2439
                                            'quiz.png',
2440
                                            get_lang('Qualify')
2441
                                        );
2442
                                    $actions .= '</a>';
2443
                                    $revisedLabel = Display::label(
2444
                                        get_lang('NotValidated'),
2445
                                        'info'
2446
                                    );
2447
                                    break;
2448
                                case 1:
2449
                                    $actions .= "<a href='exercise_show.php?".api_get_cidreq()."&action=edit&id=$id'>".
2450
                                        Display:: return_icon(
2451
                                            'edit.png',
2452
                                            get_lang('Edit'),
2453
                                            [],
2454
                                            ICON_SIZE_SMALL
2455
                                        );
2456
                                    $actions .= '</a>';
2457
                                    $revisedLabel = Display::label(
2458
                                        get_lang('Validated'),
2459
                                        'success'
2460
                                    );
2461
                                    break;
2462
                                case 2: //finished but not marked as such
2463
                                    $actions .= '<a href="exercise_report.php?'
2464
                                        .api_get_cidreq()
2465
                                        .'&exerciseId='
2466
                                        .$exercise_id
2467
                                        .'&a=close&id='
2468
                                        .$id
2469
                                        .'">'.
2470
                                        Display:: return_icon(
2471
                                            'lock.png',
2472
                                            get_lang('MarkAttemptAsClosed'),
2473
                                            [],
2474
                                            ICON_SIZE_SMALL
2475
                                        );
2476
                                    $actions .= '</a>';
2477
                                    $revisedLabel = Display::label(
2478
                                        get_lang('Unclosed'),
2479
                                        'warning'
2480
                                    );
2481
                                    break;
2482
                                case 3: //still ongoing
2483
                                    $actions .= Display:: return_icon(
2484
                                        'clock.png',
2485
                                        get_lang('AttemptStillOngoingPleaseWait'),
2486
                                        [],
2487
                                        ICON_SIZE_SMALL
2488
                                    );
2489
                                    $actions .= '';
2490
                                    $revisedLabel = Display::label(
2491
                                        get_lang('Ongoing'),
2492
                                        'danger'
2493
                                    );
2494
                                    break;
2495
                            }
2496
2497
                            if ($filter == 2) {
2498
                                $actions .= ' <a href="exercise_history.php?'.api_get_cidreq().'&exe_id='.$id.'">'.
2499
                                    Display:: return_icon(
2500
                                        'history.png',
2501
                                        get_lang('ViewHistoryChange')
2502
                                    ).'</a>';
2503
                            }
2504
2505
                            // Admin can always delete the attempt
2506
                            if (($locked == false || api_is_platform_admin()) && !api_is_student_boss()) {
2507
                                $ip = Tracking::get_ip_from_user_event(
2508
                                    $results[$i]['exe_user_id'],
2509
                                    api_get_utc_datetime(),
2510
                                    false
2511
                                );
2512
                                $actions .= '<a href="http://www.whatsmyip.org/ip-geo-location/?ip='.$ip.'" target="_blank">'
2513
                                    .Display::return_icon('info.png', $ip)
2514
                                    .'</a>';
2515
2516
                                $recalculateUrl = api_get_path(WEB_CODE_PATH).'exercise/recalculate.php?'.
2517
                                    api_get_cidreq().'&'.
2518
                                    http_build_query([
2519
                                        'id' => $id,
2520
                                        'exercise' => $exercise_id,
2521
                                        'user' => $results[$i]['exe_user_id'],
2522
                                    ]);
2523
                                $actions .= Display::url(
2524
                                    Display::return_icon('reload.png', get_lang('RecalculateResults')),
2525
                                    $recalculateUrl,
2526
                                    [
2527
                                        'data-exercise' => $exercise_id,
2528
                                        'data-user' => $results[$i]['exe_user_id'],
2529
                                        'data-id' => $id,
2530
                                        'class' => 'exercise-recalculate',
2531
                                    ]
2532
                                );
2533
2534
                                $filterByUser = isset($_GET['filter_by_user']) ? (int) $_GET['filter_by_user'] : 0;
2535
                                $delete_link = '<a href="exercise_report.php?'.api_get_cidreq().'&filter_by_user='.$filterByUser.'&filter='.$filter.'&exerciseId='.$exercise_id.'&delete=delete&did='.$id.'"
2536
                                onclick="javascript:if(!confirm(\''.sprintf(
2537
                                        addslashes(get_lang('DeleteAttempt')),
2538
                                        $results[$i]['username'],
2539
                                        $dt
2540
                                    ).'\')) return false;">';
2541
                                $delete_link .= Display::return_icon(
2542
                                        'delete.png',
2543
                                        addslashes(get_lang('Delete'))
2544
                                    ).'</a>';
2545
2546
                                if (api_is_drh() && !api_is_platform_admin()) {
2547
                                    $delete_link = null;
2548
                                }
2549
                                if (api_is_session_admin()) {
2550
                                    $delete_link = '';
2551
                                }
2552
                                if ($revised == 3) {
2553
                                    $delete_link = null;
2554
                                }
2555
                                $actions .= $delete_link;
2556
                            }
2557
                        } else {
2558
                            $attempt_url = api_get_path(WEB_CODE_PATH).'exercise/result.php?'.api_get_cidreq().'&id='.$results[$i]['exe_id'].'&id_session='.$sessionId;
2559
                            $attempt_link = Display::url(
2560
                                get_lang('Show'),
2561
                                $attempt_url,
2562
                                [
2563
                                    'class' => 'ajax btn btn-default',
2564
                                    'data-title' => get_lang('Show'),
2565
                                ]
2566
                            );
2567
                            $actions .= $attempt_link;
2568
                        }
2569
                        $actions .= '</div>';
2570
2571
                        if (!empty($userExtraFieldsToAdd)) {
2572
                            foreach ($userExtraFieldsToAdd as $variable) {
2573
                                $extraFieldValue = new ExtraFieldValue('user');
2574
                                $values = $extraFieldValue->get_values_by_handler_and_field_variable(
2575
                                    $results[$i]['user_id'],
2576
                                    $variable
2577
                                );
2578
                                if (isset($values['value'])) {
2579
                                    $results[$i][$variable] = $values['value'];
2580
                                }
2581
                            }
2582
                        }
2583
2584
                        $exeId = $results[$i]['exe_id'];
2585
                        $results[$i]['id'] = $exeId;
2586
                        $category_list = [];
2587
                        if ($is_allowedToEdit) {
2588
                            $sessionName = '';
2589
                            $sessionStartAccessDate = '';
2590
                            if (!empty($results[$i]['session_id'])) {
2591
                                $sessionInfo = api_get_session_info($results[$i]['session_id']);
2592
                                if (!empty($sessionInfo)) {
2593
                                    $sessionName = $sessionInfo['name'];
2594
                                    $sessionStartAccessDate = api_get_local_time($sessionInfo['access_start_date']);
2595
                                }
2596
                            }
2597
2598
                            $objExercise = new Exercise($course_id);
2599
                            if ($showExerciseCategories) {
2600
                                // Getting attempt info
2601
                                $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
2602
                                if (!empty($exercise_stat_info['data_tracking'])) {
2603
                                    $question_list = explode(',', $exercise_stat_info['data_tracking']);
2604
                                    if (!empty($question_list)) {
2605
                                        foreach ($question_list as $questionId) {
2606
                                            $objQuestionTmp = Question::read($questionId, $objExercise->course);
2607
                                            // We're inside *one* question. Go through each possible answer for this question
2608
                                            $result = $objExercise->manage_answer(
2609
                                                $exeId,
2610
                                                $questionId,
2611
                                                null,
2612
                                                'exercise_result',
2613
                                                false,
2614
                                                false,
2615
                                                true,
2616
                                                false,
2617
                                                $objExercise->selectPropagateNeg(),
2618
                                                null,
2619
                                                true
2620
                                            );
2621
2622
                                            $my_total_score = $result['score'];
2623
                                            $my_total_weight = $result['weight'];
2624
2625
                                            // Category report
2626
                                            $category_was_added_for_this_test = false;
2627
                                            if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
2628
                                                if (!isset($category_list[$objQuestionTmp->category]['score'])) {
2629
                                                    $category_list[$objQuestionTmp->category]['score'] = 0;
2630
                                                }
2631
                                                if (!isset($category_list[$objQuestionTmp->category]['total'])) {
2632
                                                    $category_list[$objQuestionTmp->category]['total'] = 0;
2633
                                                }
2634
                                                $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
2635
                                                $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
2636
                                                $category_was_added_for_this_test = true;
2637
                                            }
2638
2639
                                            if (isset($objQuestionTmp->category_list) &&
2640
                                                !empty($objQuestionTmp->category_list)
2641
                                            ) {
2642
                                                foreach ($objQuestionTmp->category_list as $category_id) {
2643
                                                    $category_list[$category_id]['score'] += $my_total_score;
2644
                                                    $category_list[$category_id]['total'] += $my_total_weight;
2645
                                                    $category_was_added_for_this_test = true;
2646
                                                }
2647
                                            }
2648
2649
                                            // No category for this question!
2650
                                            if ($category_was_added_for_this_test == false) {
2651
                                                if (!isset($category_list['none']['score'])) {
2652
                                                    $category_list['none']['score'] = 0;
2653
                                                }
2654
                                                if (!isset($category_list['none']['total'])) {
2655
                                                    $category_list['none']['total'] = 0;
2656
                                                }
2657
2658
                                                $category_list['none']['score'] += $my_total_score;
2659
                                                $category_list['none']['total'] += $my_total_weight;
2660
                                            }
2661
                                        }
2662
                                    }
2663
                                }
2664
                            }
2665
2666
                            foreach ($category_list as $categoryId => $result) {
2667
                                $scoreToDisplay = self::show_score(
2668
                                    $result['score'],
2669
                                    $result['total'],
2670
                                    true,
2671
                                    true,
2672
                                    false,
2673
                                    false,
2674
                                    $decimalSeparator,
2675
                                    $thousandSeparator,
2676
                                    $roundValues
2677
                                );
2678
                                $results[$i]['category_'.$categoryId] = $scoreToDisplay;
2679
                                $results[$i]['category_'.$categoryId.'_score_percentage'] = self::show_score(
2680
                                    $result['score'],
2681
                                    $result['total'],
2682
                                    true,
2683
                                    true,
2684
                                    true, // $show_only_percentage = false
2685
                                    true, // hide % sign
2686
                                    $decimalSeparator,
2687
                                    $thousandSeparator,
2688
                                    $roundValues
2689
                                );
2690
                                $results[$i]['category_'.$categoryId.'_only_score'] = $result['score'];
2691
                                $results[$i]['category_'.$categoryId.'_total'] = $result['total'];
2692
                            }
2693
                            $results[$i]['session'] = $sessionName;
2694
                            $results[$i]['session_access_start_date'] = $sessionStartAccessDate;
2695
                            $results[$i]['status'] = $revisedLabel;
2696
                            $results[$i]['score'] = $score;
2697
                            $results[$i]['score_percentage'] = self::show_score(
2698
                                $my_res,
2699
                                $my_total,
2700
                                true,
2701
                                true,
2702
                                true,
2703
                                true,
2704
                                $decimalSeparator,
2705
                                $thousandSeparator,
2706
                                $roundValues
2707
                            );
2708
2709
                            if ($roundValues) {
2710
                                $whole = floor($my_res); // 1
2711
                                $fraction = $my_res - $whole; // .25
2712
                                if ($fraction >= 0.5) {
2713
                                    $onlyScore = ceil($my_res);
2714
                                } else {
2715
                                    $onlyScore = round($my_res);
2716
                                }
2717
                            } else {
2718
                                $onlyScore = $scoreDisplay->format_score(
2719
                                    $my_res,
2720
                                    false,
2721
                                    $decimalSeparator,
2722
                                    $thousandSeparator
2723
                                );
2724
                            }
2725
2726
                            $results[$i]['only_score'] = $onlyScore;
2727
2728
                            if ($roundValues) {
2729
                                $whole = floor($my_total); // 1
2730
                                $fraction = $my_total - $whole; // .25
2731
                                if ($fraction >= 0.5) {
2732
                                    $onlyTotal = ceil($my_total);
2733
                                } else {
2734
                                    $onlyTotal = round($my_total);
2735
                                }
2736
                            } else {
2737
                                $onlyTotal = $scoreDisplay->format_score(
2738
                                    $my_total,
2739
                                    false,
2740
                                    $decimalSeparator,
2741
                                    $thousandSeparator
2742
                                );
2743
                            }
2744
                            $results[$i]['total'] = $onlyTotal;
2745
                            $results[$i]['lp'] = $lp_name;
2746
                            $results[$i]['actions'] = $actions;
2747
                            $listInfo[] = $results[$i];
2748
                        } else {
2749
                            $results[$i]['status'] = $revisedLabel;
2750
                            $results[$i]['score'] = $score;
2751
                            $results[$i]['actions'] = $actions;
2752
                            $listInfo[] = $results[$i];
2753
                        }
2754
                    }
2755
                }
2756
            }
2757
        } else {
2758
            $hpresults = [];
2759
            $res = Database::query($hpsql);
2760
            if ($res !== false) {
2761
                $i = 0;
2762
                while ($resA = Database::fetch_array($res, 'NUM')) {
2763
                    for ($j = 0; $j < 6; $j++) {
2764
                        $hpresults[$i][$j] = $resA[$j];
2765
                    }
2766
                    $i++;
2767
                }
2768
            }
2769
2770
            // Print HotPotatoes test results.
2771
            if (is_array($hpresults)) {
2772
                for ($i = 0; $i < count($hpresults); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
2773
                    $hp_title = GetQuizName($hpresults[$i][3], $documentPath);
2774
                    if ($hp_title == '') {
2775
                        $hp_title = basename($hpresults[$i][3]);
2776
                    }
2777
2778
                    $hp_date = api_get_local_time(
2779
                        $hpresults[$i][6],
2780
                        null,
2781
                        date_default_timezone_get()
2782
                    );
2783
                    $hp_result = round(($hpresults[$i][4] / ($hpresults[$i][5] != 0 ? $hpresults[$i][5] : 1)) * 100, 2);
2784
                    $hp_result .= '% ('.$hpresults[$i][4].' / '.$hpresults[$i][5].')';
2785
2786
                    if ($is_allowedToEdit) {
2787
                        $listInfo[] = [
2788
                            $hpresults[$i][0],
2789
                            $hpresults[$i][1],
2790
                            $hpresults[$i][2],
2791
                            '',
2792
                            $hp_title,
2793
                            '-',
2794
                            $hp_date,
2795
                            $hp_result,
2796
                            '-',
2797
                        ];
2798
                    } else {
2799
                        $listInfo[] = [
2800
                            $hp_title,
2801
                            '-',
2802
                            $hp_date,
2803
                            $hp_result,
2804
                            '-',
2805
                        ];
2806
                    }
2807
                }
2808
            }
2809
        }
2810
2811
        return $listInfo;
2812
    }
2813
2814
    /**
2815
     * @param $score
2816
     * @param $weight
2817
     *
2818
     * @return array
2819
     */
2820
    public static function convertScoreToPlatformSetting($score, $weight)
2821
    {
2822
        $maxNote = api_get_setting('exercise_max_score');
2823
        $minNote = api_get_setting('exercise_min_score');
2824
2825
        if ($maxNote != '' && $minNote != '') {
2826
            if (!empty($weight) && (float) $weight !== (float) 0) {
2827
                $score = $minNote + ($maxNote - $minNote) * $score / $weight;
2828
            } else {
2829
                $score = $minNote;
2830
            }
2831
            $weight = $maxNote;
2832
        }
2833
2834
        return ['score' => $score, 'weight' => $weight];
2835
    }
2836
2837
    /**
2838
     * Converts the score with the exercise_max_note and exercise_min_score
2839
     * the platform settings + formats the results using the float_format function.
2840
     *
2841
     * @param float  $score
2842
     * @param float  $weight
2843
     * @param bool   $show_percentage       show percentage or not
2844
     * @param bool   $use_platform_settings use or not the platform settings
2845
     * @param bool   $show_only_percentage
2846
     * @param bool   $hidePercentageSign    hide "%" sign
2847
     * @param string $decimalSeparator
2848
     * @param string $thousandSeparator
2849
     * @param bool   $roundValues           This option rounds the float values into a int using ceil()
2850
     * @param bool   $removeEmptyDecimals
2851
     *
2852
     * @return string an html with the score modified
2853
     */
2854
    public static function show_score(
2855
        $score,
2856
        $weight,
2857
        $show_percentage = true,
2858
        $use_platform_settings = true,
2859
        $show_only_percentage = false,
2860
        $hidePercentageSign = false,
2861
        $decimalSeparator = '.',
2862
        $thousandSeparator = ',',
2863
        $roundValues = false,
2864
        $removeEmptyDecimals = false
2865
    ) {
2866
        if (is_null($score) && is_null($weight)) {
2867
            return '-';
2868
        }
2869
2870
        $decimalSeparator = empty($decimalSeparator) ? '.' : $decimalSeparator;
2871
        $thousandSeparator = empty($thousandSeparator) ? ',' : $thousandSeparator;
2872
2873
        if ($use_platform_settings) {
2874
            $result = self::convertScoreToPlatformSetting($score, $weight);
2875
            $score = $result['score'];
2876
            $weight = $result['weight'];
2877
        }
2878
2879
        $percentage = (100 * $score) / ($weight != 0 ? $weight : 1);
2880
2881
        // Formats values
2882
        $percentage = float_format($percentage, 1);
2883
        $score = float_format($score, 1);
2884
        $weight = float_format($weight, 1);
2885
2886
        if ($roundValues) {
2887
            $whole = floor($percentage); // 1
2888
            $fraction = $percentage - $whole; // .25
2889
2890
            // Formats values
2891
            if ($fraction >= 0.5) {
2892
                $percentage = ceil($percentage);
2893
            } else {
2894
                $percentage = round($percentage);
2895
            }
2896
2897
            $whole = floor($score); // 1
2898
            $fraction = $score - $whole; // .25
2899
            if ($fraction >= 0.5) {
2900
                $score = ceil($score);
2901
            } else {
2902
                $score = round($score);
2903
            }
2904
2905
            $whole = floor($weight); // 1
2906
            $fraction = $weight - $whole; // .25
2907
            if ($fraction >= 0.5) {
2908
                $weight = ceil($weight);
2909
            } else {
2910
                $weight = round($weight);
2911
            }
2912
        } else {
2913
            // Formats values
2914
            $percentage = float_format($percentage, 1, $decimalSeparator, $thousandSeparator);
2915
            $score = float_format($score, 1, $decimalSeparator, $thousandSeparator);
2916
            $weight = float_format($weight, 1, $decimalSeparator, $thousandSeparator);
2917
        }
2918
2919
        if ($show_percentage) {
2920
            $percentageSign = ' %';
2921
            if ($hidePercentageSign) {
2922
                $percentageSign = '';
2923
            }
2924
            $html = $percentage."$percentageSign ($score / $weight)";
2925
            if ($show_only_percentage) {
2926
                $html = $percentage.$percentageSign;
2927
            }
2928
        } else {
2929
            if ($removeEmptyDecimals) {
2930
                if (ScoreDisplay::hasEmptyDecimals($weight)) {
2931
                    $weight = round($weight);
2932
                }
2933
            }
2934
            $html = $score.' / '.$weight;
2935
        }
2936
2937
        // Over write score
2938
        $scoreBasedInModel = self::convertScoreToModel($percentage);
2939
        if (!empty($scoreBasedInModel)) {
2940
            $html = $scoreBasedInModel;
2941
        }
2942
2943
        // Ignore other formats and use the configuration['exercise_score_format'] value
2944
        // But also keep the round values settings.
2945
        $format = api_get_configuration_value('exercise_score_format');
2946
        if (!empty($format)) {
2947
            $html = ScoreDisplay::instance()->display_score([$score, $weight], $format);
2948
        }
2949
2950
        return Display::span($html, ['class' => 'score_exercise']);
2951
    }
2952
2953
    /**
2954
     * @param array $model
2955
     * @param float $percentage
2956
     *
2957
     * @return string
2958
     */
2959
    public static function getModelStyle($model, $percentage)
2960
    {
2961
        return '<span class="'.$model['css_class'].'">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>';
2962
    }
2963
2964
    /**
2965
     * @param float $percentage value between 0 and 100
2966
     *
2967
     * @return string
2968
     */
2969
    public static function convertScoreToModel($percentage)
2970
    {
2971
        $model = self::getCourseScoreModel();
2972
        if (!empty($model)) {
2973
            $scoreWithGrade = [];
2974
            foreach ($model['score_list'] as $item) {
2975
                if ($percentage >= $item['min'] && $percentage <= $item['max']) {
2976
                    $scoreWithGrade = $item;
2977
                    break;
2978
                }
2979
            }
2980
2981
            if (!empty($scoreWithGrade)) {
2982
                return self::getModelStyle($scoreWithGrade, $percentage);
2983
            }
2984
        }
2985
2986
        return '';
2987
    }
2988
2989
    /**
2990
     * @return array
2991
     */
2992
    public static function getCourseScoreModel()
2993
    {
2994
        $modelList = self::getScoreModels();
2995
        if (empty($modelList)) {
2996
            return [];
2997
        }
2998
2999
        $courseInfo = api_get_course_info();
3000
        if (!empty($courseInfo)) {
3001
            $scoreModelId = api_get_course_setting('score_model_id');
3002
            if (-1 != $scoreModelId) {
3003
                $modelIdList = array_column($modelList['models'], 'id');
3004
                if (in_array($scoreModelId, $modelIdList)) {
3005
                    foreach ($modelList['models'] as $item) {
3006
                        if ($item['id'] == $scoreModelId) {
3007
                            return $item;
3008
                        }
3009
                    }
3010
                }
3011
            }
3012
        }
3013
3014
        return [];
3015
    }
3016
3017
    /**
3018
     * @return array
3019
     */
3020
    public static function getScoreModels()
3021
    {
3022
        return api_get_configuration_value('score_grade_model');
3023
    }
3024
3025
    /**
3026
     * @param float  $score
3027
     * @param float  $weight
3028
     * @param string $passPercentage
3029
     *
3030
     * @return bool
3031
     */
3032
    public static function isSuccessExerciseResult($score, $weight, $passPercentage)
3033
    {
3034
        $percentage = float_format(
3035
            ($score / (0 != $weight ? $weight : 1)) * 100,
3036
            1
3037
        );
3038
        if (isset($passPercentage) && !empty($passPercentage)) {
3039
            if ($percentage >= $passPercentage) {
3040
                return true;
3041
            }
3042
        }
3043
3044
        return false;
3045
    }
3046
3047
    /**
3048
     * @param string $name
3049
     * @param $weight
3050
     * @param $selected
3051
     *
3052
     * @return bool
3053
     */
3054
    public static function addScoreModelInput(
3055
        FormValidator $form,
3056
        $name,
3057
        $weight,
3058
        $selected
3059
    ) {
3060
        $model = self::getCourseScoreModel();
3061
        if (empty($model)) {
3062
            return false;
3063
        }
3064
3065
        /** @var HTML_QuickForm_select $element */
3066
        $element = $form->createElement(
3067
            'select',
3068
            $name,
3069
            get_lang('Qualification'),
3070
            [],
3071
            ['class' => 'exercise_mark_select']
3072
        );
3073
3074
        foreach ($model['score_list'] as $item) {
3075
            $i = api_number_format($item['score_to_qualify'] / 100 * $weight, 2);
3076
            $label = self::getModelStyle($item, $i);
3077
            $attributes = [
3078
                'class' => $item['css_class'],
3079
            ];
3080
            if ($selected == $i) {
3081
                $attributes['selected'] = 'selected';
3082
            }
3083
            $element->addOption($label, $i, $attributes);
3084
        }
3085
        $form->addElement($element);
3086
    }
3087
3088
    /**
3089
     * @return string
3090
     */
3091
    public static function getJsCode()
3092
    {
3093
        // Filling the scores with the right colors.
3094
        $models = self::getCourseScoreModel();
3095
        $cssListToString = '';
3096
        if (!empty($models)) {
3097
            $cssList = array_column($models['score_list'], 'css_class');
3098
            $cssListToString = implode(' ', $cssList);
3099
        }
3100
3101
        if (empty($cssListToString)) {
3102
            return '';
3103
        }
3104
        $js = <<<EOT
3105
3106
        function updateSelect(element) {
3107
            var spanTag = element.parent().find('span.filter-option');
3108
            var value = element.val();
3109
            var selectId = element.attr('id');
3110
            var optionClass = $('#' + selectId + ' option[value="'+value+'"]').attr('class');
3111
            spanTag.removeClass('$cssListToString');
3112
            spanTag.addClass(optionClass);
3113
        }
3114
3115
        $(function() {
3116
            // Loading values
3117
            $('.exercise_mark_select').on('loaded.bs.select', function() {
3118
                updateSelect($(this));
3119
            });
3120
            // On change
3121
            $('.exercise_mark_select').on('changed.bs.select', function() {
3122
                updateSelect($(this));
3123
            });
3124
        });
3125
EOT;
3126
3127
        return $js;
3128
    }
3129
3130
    /**
3131
     * @param float  $score
3132
     * @param float  $weight
3133
     * @param string $pass_percentage
3134
     *
3135
     * @return string
3136
     */
3137
    public static function showSuccessMessage($score, $weight, $pass_percentage)
3138
    {
3139
        $res = '';
3140
        if (self::isPassPercentageEnabled($pass_percentage)) {
3141
            $isSuccess = self::isSuccessExerciseResult(
3142
                $score,
3143
                $weight,
3144
                $pass_percentage
3145
            );
3146
3147
            if ($isSuccess) {
3148
                $html = get_lang('CongratulationsYouPassedTheTest');
3149
                $icon = Display::return_icon(
3150
                    'completed.png',
3151
                    get_lang('Correct'),
3152
                    [],
3153
                    ICON_SIZE_MEDIUM
3154
                );
3155
            } else {
3156
                $html = get_lang('YouDidNotReachTheMinimumScore');
3157
                $icon = Display::return_icon(
3158
                    'warning.png',
3159
                    get_lang('Wrong'),
3160
                    [],
3161
                    ICON_SIZE_MEDIUM
3162
                );
3163
            }
3164
            $html = Display::tag('h4', $html);
3165
            $html .= Display::tag(
3166
                'h5',
3167
                $icon,
3168
                ['style' => 'width:40px; padding:2px 10px 0px 0px']
3169
            );
3170
            $res = $html;
3171
        }
3172
3173
        return $res;
3174
    }
3175
3176
    /**
3177
     * Return true if pass_pourcentage activated (we use the pass pourcentage feature
3178
     * return false if pass_percentage = 0 (we don't use the pass pourcentage feature.
3179
     *
3180
     * @param $value
3181
     *
3182
     * @return bool
3183
     *              In this version, pass_percentage and show_success_message are disabled if
3184
     *              pass_percentage is set to 0
3185
     */
3186
    public static function isPassPercentageEnabled($value)
3187
    {
3188
        return $value > 0;
3189
    }
3190
3191
    /**
3192
     * Converts a numeric value in a percentage example 0.66666 to 66.67 %.
3193
     *
3194
     * @param $value
3195
     *
3196
     * @return float Converted number
3197
     */
3198
    public static function convert_to_percentage($value)
3199
    {
3200
        $return = '-';
3201
        if ($value != '') {
3202
            $return = float_format($value * 100, 1).' %';
3203
        }
3204
3205
        return $return;
3206
    }
3207
3208
    /**
3209
     * Getting all active exercises from a course from a session
3210
     * (if a session_id is provided we will show all the exercises in the course +
3211
     * all exercises in the session).
3212
     *
3213
     * @param array  $course_info
3214
     * @param int    $session_id
3215
     * @param bool   $check_publication_dates
3216
     * @param string $search                  Search exercise name
3217
     * @param bool   $search_all_sessions     Search exercises in all sessions
3218
     * @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...
3219
     *                  1 = only active exercises,
3220
     *                  2 = all exercises
3221
     *                  3 = active <> -1
3222
     *
3223
     * @return array array with exercise data
3224
     */
3225
    public static function get_all_exercises(
3226
        $course_info = null,
3227
        $session_id = 0,
3228
        $check_publication_dates = false,
3229
        $search = '',
3230
        $search_all_sessions = false,
3231
        $active = 2
3232
    ) {
3233
        $course_id = api_get_course_int_id();
3234
3235
        if (!empty($course_info) && !empty($course_info['real_id'])) {
3236
            $course_id = $course_info['real_id'];
3237
        }
3238
3239
        if ($session_id == -1) {
3240
            $session_id = 0;
3241
        }
3242
3243
        $now = api_get_utc_datetime();
3244
        $timeConditions = '';
3245
        if ($check_publication_dates) {
3246
            // Start and end are set
3247
            $timeConditions = " AND ((start_time <> '' AND start_time < '$now' AND end_time <> '' AND end_time > '$now' )  OR ";
3248
            // only start is set
3249
            $timeConditions .= " (start_time <> '' AND start_time < '$now' AND end_time is NULL) OR ";
3250
            // only end is set
3251
            $timeConditions .= " (start_time IS NULL AND end_time <> '' AND end_time > '$now') OR ";
3252
            // nothing is set
3253
            $timeConditions .= ' (start_time IS NULL AND end_time IS NULL)) ';
3254
        }
3255
3256
        $needle_where = !empty($search) ? " AND title LIKE '?' " : '';
3257
        $needle = !empty($search) ? "%".$search."%" : '';
3258
3259
        // Show courses by active status
3260
        $active_sql = '';
3261
        if ($active == 3) {
3262
            $active_sql = ' active <> -1 AND';
3263
        } else {
3264
            if ($active != 2) {
3265
                $active_sql = sprintf(' active = %d AND', $active);
3266
            }
3267
        }
3268
3269
        if ($search_all_sessions == true) {
3270
            $conditions = [
3271
                'where' => [
3272
                    $active_sql.' c_id = ? '.$needle_where.$timeConditions => [
3273
                        $course_id,
3274
                        $needle,
3275
                    ],
3276
                ],
3277
                'order' => 'title',
3278
            ];
3279
        } else {
3280
            if (empty($session_id)) {
3281
                $conditions = [
3282
                    'where' => [
3283
                        $active_sql.' (session_id = 0 OR session_id IS NULL) AND c_id = ? '.$needle_where.$timeConditions => [
3284
                            $course_id,
3285
                            $needle,
3286
                        ],
3287
                    ],
3288
                    'order' => 'title',
3289
                ];
3290
            } else {
3291
                $conditions = [
3292
                    'where' => [
3293
                        $active_sql.' (session_id = 0 OR session_id IS NULL OR session_id = ? ) AND c_id = ? '.$needle_where.$timeConditions => [
3294
                            $session_id,
3295
                            $course_id,
3296
                            $needle,
3297
                        ],
3298
                    ],
3299
                    'order' => 'title',
3300
                ];
3301
            }
3302
        }
3303
3304
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
3305
3306
        return Database::select('*', $table, $conditions);
3307
    }
3308
3309
    /**
3310
     * Getting all exercises (active only or all)
3311
     * from a course from a session
3312
     * (if a session_id is provided we will show all the exercises in the
3313
     * course + all exercises in the session).
3314
     *
3315
     * @param   array   course data
3316
     * @param   int     session id
3317
     * @param    int        course c_id
3318
     * @param bool $only_active_exercises
3319
     *
3320
     * @return array array with exercise data
3321
     *               modified by Hubert Borderiou
3322
     */
3323
    public static function get_all_exercises_for_course_id(
3324
        $course_info = null,
3325
        $session_id = 0,
3326
        $course_id = 0,
3327
        $only_active_exercises = true
3328
    ) {
3329
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
3330
3331
        if ($only_active_exercises) {
3332
            // Only active exercises.
3333
            $sql_active_exercises = "active = 1 AND ";
3334
        } else {
3335
            // Not only active means visible and invisible NOT deleted (-2)
3336
            $sql_active_exercises = "active IN (1, 0) AND ";
3337
        }
3338
3339
        if ($session_id == -1) {
3340
            $session_id = 0;
3341
        }
3342
3343
        $params = [
3344
            $session_id,
3345
            $course_id,
3346
        ];
3347
3348
        if (empty($session_id)) {
3349
            $conditions = [
3350
                'where' => ["$sql_active_exercises (session_id = 0 OR session_id IS NULL) AND c_id = ?" => [$course_id]],
3351
                'order' => 'title',
3352
            ];
3353
        } else {
3354
            // All exercises
3355
            $conditions = [
3356
                'where' => ["$sql_active_exercises (session_id = 0 OR session_id IS NULL OR session_id = ? ) AND c_id=?" => $params],
3357
                'order' => 'title',
3358
            ];
3359
        }
3360
3361
        return Database::select('*', $table, $conditions);
3362
    }
3363
3364
    /**
3365
     * Gets the position of the score based in a given score (result/weight)
3366
     * and the exe_id based in the user list
3367
     * (NO Exercises in LPs ).
3368
     *
3369
     * @param float  $my_score      user score to be compared *attention*
3370
     *                              $my_score = score/weight and not just the score
3371
     * @param int    $my_exe_id     exe id of the exercise
3372
     *                              (this is necessary because if 2 students have the same score the one
3373
     *                              with the minor exe_id will have a best position, just to be fair and FIFO)
3374
     * @param int    $exercise_id
3375
     * @param string $course_code
3376
     * @param int    $session_id
3377
     * @param array  $user_list
3378
     * @param bool   $return_string
3379
     *
3380
     * @return int the position of the user between his friends in a course
3381
     *             (or course within a session)
3382
     */
3383
    public static function get_exercise_result_ranking(
3384
        $my_score,
3385
        $my_exe_id,
3386
        $exercise_id,
3387
        $course_code,
3388
        $session_id = 0,
3389
        $user_list = [],
3390
        $return_string = true
3391
    ) {
3392
        //No score given we return
3393
        if (is_null($my_score)) {
3394
            return '-';
3395
        }
3396
        if (empty($user_list)) {
3397
            return '-';
3398
        }
3399
3400
        $best_attempts = [];
3401
        foreach ($user_list as $user_data) {
3402
            $user_id = $user_data['user_id'];
3403
            $best_attempts[$user_id] = self::get_best_attempt_by_user(
3404
                $user_id,
3405
                $exercise_id,
3406
                $course_code,
3407
                $session_id
3408
            );
3409
        }
3410
3411
        if (empty($best_attempts)) {
3412
            return 1;
3413
        } else {
3414
            $position = 1;
3415
            $my_ranking = [];
3416
            foreach ($best_attempts as $user_id => $result) {
3417
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
3418
                    $my_ranking[$user_id] = $result['exe_result'] / $result['exe_weighting'];
3419
                } else {
3420
                    $my_ranking[$user_id] = 0;
3421
                }
3422
            }
3423
            //if (!empty($my_ranking)) {
3424
            asort($my_ranking);
3425
            $position = count($my_ranking);
3426
            if (!empty($my_ranking)) {
3427
                foreach ($my_ranking as $user_id => $ranking) {
3428
                    if ($my_score >= $ranking) {
3429
                        if ($my_score == $ranking && isset($best_attempts[$user_id]['exe_id'])) {
3430
                            $exe_id = $best_attempts[$user_id]['exe_id'];
3431
                            if ($my_exe_id < $exe_id) {
3432
                                $position--;
3433
                            }
3434
                        } else {
3435
                            $position--;
3436
                        }
3437
                    }
3438
                }
3439
            }
3440
            //}
3441
            $return_value = [
3442
                'position' => $position,
3443
                'count' => count($my_ranking),
3444
            ];
3445
3446
            if ($return_string) {
3447
                if (!empty($position) && !empty($my_ranking)) {
3448
                    $return_value = $position.'/'.count($my_ranking);
3449
                } else {
3450
                    $return_value = '-';
3451
                }
3452
            }
3453
3454
            return $return_value;
3455
        }
3456
    }
3457
3458
    /**
3459
     * Gets the position of the score based in a given score (result/weight) and the exe_id based in all attempts
3460
     * (NO Exercises in LPs ) old functionality by attempt.
3461
     *
3462
     * @param   float   user score to be compared attention => score/weight
3463
     * @param   int     exe id of the exercise
3464
     * (this is necessary because if 2 students have the same score the one
3465
     * with the minor exe_id will have a best position, just to be fair and FIFO)
3466
     * @param   int     exercise id
3467
     * @param   string  course code
3468
     * @param   int     session id
3469
     * @param bool $return_string
3470
     *
3471
     * @return int the position of the user between his friends in a course (or course within a session)
3472
     */
3473
    public static function get_exercise_result_ranking_by_attempt(
3474
        $my_score,
3475
        $my_exe_id,
3476
        $exercise_id,
3477
        $courseId,
3478
        $session_id = 0,
3479
        $return_string = true
3480
    ) {
3481
        if (empty($session_id)) {
3482
            $session_id = 0;
3483
        }
3484
        if (is_null($my_score)) {
3485
            return '-';
3486
        }
3487
        $user_results = Event::get_all_exercise_results(
3488
            $exercise_id,
3489
            $courseId,
3490
            $session_id,
3491
            false
3492
        );
3493
        $position_data = [];
3494
        if (empty($user_results)) {
3495
            return 1;
3496
        } else {
3497
            $position = 1;
3498
            $my_ranking = [];
3499
            foreach ($user_results as $result) {
3500
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
3501
                    $my_ranking[$result['exe_id']] = $result['exe_result'] / $result['exe_weighting'];
3502
                } else {
3503
                    $my_ranking[$result['exe_id']] = 0;
3504
                }
3505
            }
3506
            asort($my_ranking);
3507
            $position = count($my_ranking);
3508
            if (!empty($my_ranking)) {
3509
                foreach ($my_ranking as $exe_id => $ranking) {
3510
                    if ($my_score >= $ranking) {
3511
                        if ($my_score == $ranking) {
3512
                            if ($my_exe_id < $exe_id) {
3513
                                $position--;
3514
                            }
3515
                        } else {
3516
                            $position--;
3517
                        }
3518
                    }
3519
                }
3520
            }
3521
            $return_value = [
3522
                'position' => $position,
3523
                'count' => count($my_ranking),
3524
            ];
3525
3526
            if ($return_string) {
3527
                if (!empty($position) && !empty($my_ranking)) {
3528
                    return $position.'/'.count($my_ranking);
3529
                }
3530
            }
3531
3532
            return $return_value;
3533
        }
3534
    }
3535
3536
    /**
3537
     * Get the best attempt in a exercise (NO Exercises in LPs ).
3538
     *
3539
     * @param int $exercise_id
3540
     * @param int $courseId
3541
     * @param int $session_id
3542
     *
3543
     * @return array
3544
     */
3545
    public static function get_best_attempt_in_course($exercise_id, $courseId, $session_id)
3546
    {
3547
        $user_results = Event::get_all_exercise_results(
3548
            $exercise_id,
3549
            $courseId,
3550
            $session_id,
3551
            false
3552
        );
3553
3554
        $best_score_data = [];
3555
        $best_score = 0;
3556
        if (!empty($user_results)) {
3557
            foreach ($user_results as $result) {
3558
                if (!empty($result['exe_weighting']) &&
3559
                    intval($result['exe_weighting']) != 0
3560
                ) {
3561
                    $score = $result['exe_result'] / $result['exe_weighting'];
3562
                    if ($score >= $best_score) {
3563
                        $best_score = $score;
3564
                        $best_score_data = $result;
3565
                    }
3566
                }
3567
            }
3568
        }
3569
3570
        return $best_score_data;
3571
    }
3572
3573
    /**
3574
     * Get the best score in a exercise (NO Exercises in LPs ).
3575
     *
3576
     * @param int $user_id
3577
     * @param int $exercise_id
3578
     * @param int $courseId
3579
     * @param int $session_id
3580
     *
3581
     * @return array
3582
     */
3583
    public static function get_best_attempt_by_user(
3584
        $user_id,
3585
        $exercise_id,
3586
        $courseId,
3587
        $session_id
3588
    ) {
3589
        $user_results = Event::get_all_exercise_results(
3590
            $exercise_id,
3591
            $courseId,
3592
            $session_id,
3593
            false,
3594
            $user_id
3595
        );
3596
        $best_score_data = [];
3597
        $best_score = 0;
3598
        if (!empty($user_results)) {
3599
            foreach ($user_results as $result) {
3600
                if (!empty($result['exe_weighting']) && (float) $result['exe_weighting'] != 0) {
3601
                    $score = $result['exe_result'] / $result['exe_weighting'];
3602
                    if ($score >= $best_score) {
3603
                        $best_score = $score;
3604
                        $best_score_data = $result;
3605
                    }
3606
                }
3607
            }
3608
        }
3609
3610
        return $best_score_data;
3611
    }
3612
3613
    /**
3614
     * Get average score (NO Exercises in LPs ).
3615
     *
3616
     * @param    int    exercise id
3617
     * @param int $courseId
3618
     * @param    int    session id
3619
     *
3620
     * @return float Average score
3621
     */
3622
    public static function get_average_score($exercise_id, $courseId, $session_id)
3623
    {
3624
        $user_results = Event::get_all_exercise_results(
3625
            $exercise_id,
3626
            $courseId,
3627
            $session_id
3628
        );
3629
        $avg_score = 0;
3630
        if (!empty($user_results)) {
3631
            foreach ($user_results as $result) {
3632
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
3633
                    $score = $result['exe_result'] / $result['exe_weighting'];
3634
                    $avg_score += $score;
3635
                }
3636
            }
3637
            $avg_score = float_format($avg_score / count($user_results), 1);
3638
        }
3639
3640
        return $avg_score;
3641
    }
3642
3643
    /**
3644
     * Get average score by score (NO Exercises in LPs ).
3645
     *
3646
     * @param int $courseId
3647
     * @param    int    session id
3648
     *
3649
     * @return float Average score
3650
     */
3651
    public static function get_average_score_by_course($courseId, $session_id)
3652
    {
3653
        $user_results = Event::get_all_exercise_results_by_course(
3654
            $courseId,
3655
            $session_id,
3656
            false
3657
        );
3658
        $avg_score = 0;
3659
        if (!empty($user_results)) {
3660
            foreach ($user_results as $result) {
3661
                if (!empty($result['exe_weighting']) && intval(
3662
                        $result['exe_weighting']
3663
                    ) != 0
3664
                ) {
3665
                    $score = $result['exe_result'] / $result['exe_weighting'];
3666
                    $avg_score += $score;
3667
                }
3668
            }
3669
            // We assume that all exe_weighting
3670
            $avg_score = $avg_score / count($user_results);
3671
        }
3672
3673
        return $avg_score;
3674
    }
3675
3676
    /**
3677
     * @param int $user_id
3678
     * @param int $courseId
3679
     * @param int $session_id
3680
     *
3681
     * @return float|int
3682
     */
3683
    public static function get_average_score_by_course_by_user(
3684
        $user_id,
3685
        $courseId,
3686
        $session_id
3687
    ) {
3688
        $user_results = Event::get_all_exercise_results_by_user(
3689
            $user_id,
3690
            $courseId,
3691
            $session_id
3692
        );
3693
        $avg_score = 0;
3694
        if (!empty($user_results)) {
3695
            foreach ($user_results as $result) {
3696
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
3697
                    $score = $result['exe_result'] / $result['exe_weighting'];
3698
                    $avg_score += $score;
3699
                }
3700
            }
3701
            // We assume that all exe_weighting
3702
            $avg_score = ($avg_score / count($user_results));
3703
        }
3704
3705
        return $avg_score;
3706
    }
3707
3708
    /**
3709
     * Get average score by score (NO Exercises in LPs ).
3710
     *
3711
     * @param int $exercise_id
3712
     * @param int $courseId
3713
     * @param int $session_id
3714
     * @param int $user_count
3715
     *
3716
     * @return float Best average score
3717
     */
3718
    public static function get_best_average_score_by_exercise(
3719
        $exercise_id,
3720
        $courseId,
3721
        $session_id,
3722
        $user_count
3723
    ) {
3724
        $user_results = Event::get_best_exercise_results_by_user(
3725
            $exercise_id,
3726
            $courseId,
3727
            $session_id
3728
        );
3729
        $avg_score = 0;
3730
        if (!empty($user_results)) {
3731
            foreach ($user_results as $result) {
3732
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
3733
                    $score = $result['exe_result'] / $result['exe_weighting'];
3734
                    $avg_score += $score;
3735
                }
3736
            }
3737
            // We asumme that all exe_weighting
3738
            if (!empty($user_count)) {
3739
                $avg_score = float_format($avg_score / $user_count, 1) * 100;
3740
            } else {
3741
                $avg_score = 0;
3742
            }
3743
        }
3744
3745
        return $avg_score;
3746
    }
3747
3748
    /**
3749
     * Get average score by score (NO Exercises in LPs ).
3750
     *
3751
     * @param int $exercise_id
3752
     * @param int $courseId
3753
     * @param int $session_id
3754
     *
3755
     * @return float Best average score
3756
     */
3757
    public static function getBestScoreByExercise(
3758
        $exercise_id,
3759
        $courseId,
3760
        $session_id
3761
    ) {
3762
        $user_results = Event::get_best_exercise_results_by_user(
3763
            $exercise_id,
3764
            $courseId,
3765
            $session_id
3766
        );
3767
        $avg_score = 0;
3768
        if (!empty($user_results)) {
3769
            foreach ($user_results as $result) {
3770
                if (!empty($result['exe_weighting']) && intval($result['exe_weighting']) != 0) {
3771
                    $score = $result['exe_result'] / $result['exe_weighting'];
3772
                    $avg_score += $score;
3773
                }
3774
            }
3775
        }
3776
3777
        return $avg_score;
3778
    }
3779
3780
    /**
3781
     * @param string $course_code
3782
     * @param int    $session_id
3783
     *
3784
     * @return array
3785
     */
3786
    public static function get_exercises_to_be_taken($course_code, $session_id)
3787
    {
3788
        $course_info = api_get_course_info($course_code);
3789
        $exercises = self::get_all_exercises($course_info, $session_id);
3790
        $result = [];
3791
        $now = time() + 15 * 24 * 60 * 60;
3792
        foreach ($exercises as $exercise_item) {
3793
            if (isset($exercise_item['end_time']) &&
3794
                !empty($exercise_item['end_time']) &&
3795
                api_strtotime($exercise_item['end_time'], 'UTC') < $now
3796
            ) {
3797
                $result[] = $exercise_item;
3798
            }
3799
        }
3800
3801
        return $result;
3802
    }
3803
3804
    /**
3805
     * Get student results (only in completed exercises) stats by question.
3806
     *
3807
     * @param int    $question_id
3808
     * @param int    $exercise_id
3809
     * @param string $course_code
3810
     * @param int    $session_id
3811
     * @param bool   $onlyStudent Filter only enrolled students
3812
     *
3813
     * @return array
3814
     */
3815
    public static function get_student_stats_by_question(
3816
        $question_id,
3817
        $exercise_id,
3818
        $course_code,
3819
        $session_id,
3820
        $onlyStudent = false
3821
    ) {
3822
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3823
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3824
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3825
3826
        $question_id = (int) $question_id;
3827
        $exercise_id = (int) $exercise_id;
3828
        $course_code = Database::escape_string($course_code);
3829
        $session_id = (int) $session_id;
3830
        $courseId = api_get_course_int_id($course_code);
3831
3832
        $sql = "SELECT MAX(marks) as max, MIN(marks) as min, AVG(marks) as average
3833
    		FROM $track_exercises e
3834
    		";
3835
        if (true == $onlyStudent) {
3836
            $courseCondition = '';
3837
            if (empty($session_id)) {
3838
                $courseCondition = "
3839
            INNER JOIN $courseUser c
3840
            ON (
3841
                        e.exe_user_id = c.user_id AND
3842
                        e.c_id = c.c_id AND
3843
                        c.status = ".STUDENT."
3844
                        AND relation_type <> 2
3845
                )";
3846
            } else {
3847
                $courseCondition = "
3848
            INNER JOIN $courseUser c
3849
            ON (
3850
                        e.exe_user_id = c.user_id AND
3851
                        e.c_id = c.c_id AND
3852
                        c.status = 0
3853
                )";
3854
            }
3855
            $sql .= $courseCondition;
3856
        }
3857
        $sql .= "
3858
            INNER JOIN $track_attempt a
3859
    		ON (
3860
    		    a.exe_id = e.exe_id AND
3861
    		    e.c_id = a.c_id AND
3862
    		    e.session_id  = a.session_id
3863
            )
3864
    		WHERE
3865
    		    exe_exo_id 	= $exercise_id AND
3866
                a.c_id = $courseId AND
3867
                e.session_id = $session_id AND
3868
                question_id = $question_id AND
3869
                e.status = ''
3870
            LIMIT 1";
3871
        $result = Database::query($sql);
3872
        $return = [];
3873
        if ($result) {
3874
            $return = Database::fetch_array($result, 'ASSOC');
3875
        }
3876
3877
        return $return;
3878
    }
3879
3880
    /**
3881
     * Get the correct answer count for a fill blanks question.
3882
     *
3883
     * @param int $question_id
3884
     * @param int $exercise_id
3885
     *
3886
     * @return array
3887
     */
3888
    public static function getNumberStudentsFillBlanksAnswerCount(
3889
        $question_id,
3890
        $exercise_id
3891
    ) {
3892
        $listStudentsId = [];
3893
        $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
3894
            api_get_course_id(),
3895
            true
3896
        );
3897
        foreach ($listAllStudentInfo as $i => $listStudentInfo) {
3898
            $listStudentsId[] = $listStudentInfo['user_id'];
3899
        }
3900
3901
        $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3902
            $exercise_id,
3903
            $question_id,
3904
            $listStudentsId,
3905
            '1970-01-01',
3906
            '3000-01-01'
3907
        );
3908
3909
        $arrayCount = [];
3910
3911
        foreach ($listFillTheBlankResult as $resultCount) {
3912
            foreach ($resultCount as $index => $count) {
3913
                //this is only for declare the array index per answer
3914
                $arrayCount[$index] = 0;
3915
            }
3916
        }
3917
3918
        foreach ($listFillTheBlankResult as $resultCount) {
3919
            foreach ($resultCount as $index => $count) {
3920
                $count = ($count === 0) ? 1 : 0;
3921
                $arrayCount[$index] += $count;
3922
            }
3923
        }
3924
3925
        return $arrayCount;
3926
    }
3927
3928
    /**
3929
     * Get the number of questions with answers.
3930
     *
3931
     * @param int    $question_id
3932
     * @param int    $exercise_id
3933
     * @param string $course_code
3934
     * @param int    $session_id
3935
     * @param string $questionType
3936
     *
3937
     * @return int
3938
     */
3939
    public static function get_number_students_question_with_answer_count(
3940
        $question_id,
3941
        $exercise_id,
3942
        $course_code,
3943
        $session_id,
3944
        $questionType = ''
3945
    ) {
3946
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3947
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3948
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3949
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
3950
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3951
3952
        $question_id = intval($question_id);
3953
        $exercise_id = intval($exercise_id);
3954
        $courseId = api_get_course_int_id($course_code);
3955
        $session_id = intval($session_id);
3956
3957
        if ($questionType == FILL_IN_BLANKS) {
3958
            $listStudentsId = [];
3959
            $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
3960
                api_get_course_id(),
3961
                true
3962
            );
3963
            foreach ($listAllStudentInfo as $i => $listStudentInfo) {
3964
                $listStudentsId[] = $listStudentInfo['user_id'];
3965
            }
3966
3967
            $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3968
                $exercise_id,
3969
                $question_id,
3970
                $listStudentsId,
3971
                '1970-01-01',
3972
                '3000-01-01'
3973
            );
3974
3975
            return FillBlanks::getNbResultFillBlankAll($listFillTheBlankResult);
3976
        }
3977
3978
        if (empty($session_id)) {
3979
            $courseCondition = "
3980
            INNER JOIN $courseUser cu
3981
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
3982
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3983
        } else {
3984
            $courseCondition = "
3985
            INNER JOIN $courseUserSession cu
3986
            ON cu.c_id = c.id AND cu.user_id = exe_user_id";
3987
            $courseConditionWhere = " AND cu.status = 0 ";
3988
        }
3989
3990
        $sql = "SELECT DISTINCT exe_user_id
3991
    		FROM $track_exercises e
3992
    		INNER JOIN $track_attempt a
3993
    		ON (
3994
    		    a.exe_id = e.exe_id AND
3995
    		    e.c_id = a.c_id AND
3996
    		    e.session_id  = a.session_id
3997
            )
3998
            INNER JOIN $courseTable c
3999
            ON (c.id = a.c_id)
4000
    		$courseCondition
4001
    		WHERE
4002
    		    exe_exo_id = $exercise_id AND
4003
                a.c_id = $courseId AND
4004
                e.session_id = $session_id AND
4005
                question_id = $question_id AND
4006
                answer <> '0' AND
4007
                e.status = ''
4008
                $courseConditionWhere
4009
            ";
4010
        $result = Database::query($sql);
4011
        $return = 0;
4012
        if ($result) {
4013
            $return = Database::num_rows($result);
4014
        }
4015
4016
        return $return;
4017
    }
4018
4019
    /**
4020
     * Get number of answers to hotspot questions.
4021
     *
4022
     * @param int    $answer_id
4023
     * @param int    $question_id
4024
     * @param int    $exercise_id
4025
     * @param string $course_code
4026
     * @param int    $session_id
4027
     *
4028
     * @return int
4029
     */
4030
    public static function get_number_students_answer_hotspot_count(
4031
        $answer_id,
4032
        $question_id,
4033
        $exercise_id,
4034
        $course_code,
4035
        $session_id
4036
    ) {
4037
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4038
        $track_hotspot = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4039
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4040
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4041
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4042
4043
        $question_id = (int) $question_id;
4044
        $answer_id = (int) $answer_id;
4045
        $exercise_id = (int) $exercise_id;
4046
        $course_code = Database::escape_string($course_code);
4047
        $session_id = (int) $session_id;
4048
4049
        if (empty($session_id)) {
4050
            $courseCondition = "
4051
            INNER JOIN $courseUser cu
4052
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
4053
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4054
        } else {
4055
            $courseCondition = "
4056
            INNER JOIN $courseUserSession cu
4057
            ON cu.c_id = c.id AND cu.user_id = exe_user_id";
4058
            $courseConditionWhere = ' AND cu.status = 0 ';
4059
        }
4060
4061
        $sql = "SELECT DISTINCT exe_user_id
4062
    		FROM $track_exercises e
4063
    		INNER JOIN $track_hotspot a
4064
    		ON (a.hotspot_exe_id = e.exe_id)
4065
    		INNER JOIN $courseTable c
4066
    		ON (hotspot_course_code = c.code)
4067
    		$courseCondition
4068
    		WHERE
4069
    		    exe_exo_id              = $exercise_id AND
4070
                a.hotspot_course_code 	= '$course_code' AND
4071
                e.session_id            = $session_id AND
4072
                hotspot_answer_id       = $answer_id AND
4073
                hotspot_question_id     = $question_id AND
4074
                hotspot_correct         =  1 AND
4075
                e.status                = ''
4076
                $courseConditionWhere
4077
            ";
4078
4079
        $result = Database::query($sql);
4080
        $return = 0;
4081
        if ($result) {
4082
            $return = Database::num_rows($result);
4083
        }
4084
4085
        return $return;
4086
    }
4087
4088
    /**
4089
     * @param int    $answer_id
4090
     * @param int    $question_id
4091
     * @param int    $exercise_id
4092
     * @param string $course_code
4093
     * @param int    $session_id
4094
     * @param string $question_type
4095
     * @param string $correct_answer
4096
     * @param string $current_answer
4097
     *
4098
     * @return int
4099
     */
4100
    public static function get_number_students_answer_count(
4101
        $answer_id,
4102
        $question_id,
4103
        $exercise_id,
4104
        $course_code,
4105
        $session_id,
4106
        $question_type = null,
4107
        $correct_answer = null,
4108
        $current_answer = null
4109
    ) {
4110
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4111
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4112
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4113
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4114
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4115
4116
        $question_id = (int) $question_id;
4117
        $answer_id = (int) $answer_id;
4118
        $exercise_id = (int) $exercise_id;
4119
        $courseId = api_get_course_int_id($course_code);
4120
        $session_id = (int) $session_id;
4121
4122
        switch ($question_type) {
4123
            case FILL_IN_BLANKS:
4124
                $answer_condition = '';
4125
                $select_condition = ' e.exe_id, answer ';
4126
                break;
4127
            case MATCHING:
4128
            case MATCHING_DRAGGABLE:
4129
            default:
4130
                $answer_condition = " answer = $answer_id AND ";
4131
                $select_condition = ' DISTINCT exe_user_id ';
4132
        }
4133
4134
        if (empty($session_id)) {
4135
            $courseCondition = "
4136
            INNER JOIN $courseUser cu
4137
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
4138
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4139
        } else {
4140
            $courseCondition = "
4141
            INNER JOIN $courseUserSession cu
4142
            ON cu.c_id = a.c_id AND cu.user_id = exe_user_id";
4143
            $courseConditionWhere = ' AND cu.status = 0 ';
4144
        }
4145
4146
        $sql = "SELECT $select_condition
4147
    		FROM $track_exercises e
4148
    		INNER JOIN $track_attempt a
4149
    		ON (
4150
    		    a.exe_id = e.exe_id AND
4151
    		    e.c_id = a.c_id AND
4152
    		    e.session_id  = a.session_id
4153
            )
4154
            INNER JOIN $courseTable c
4155
            ON c.id = a.c_id
4156
    		$courseCondition
4157
    		WHERE
4158
    		    exe_exo_id = $exercise_id AND
4159
                a.c_id = $courseId AND
4160
                e.session_id = $session_id AND
4161
                $answer_condition
4162
                question_id = $question_id AND
4163
                e.status = ''
4164
                $courseConditionWhere
4165
            ";
4166
        $result = Database::query($sql);
4167
        $return = 0;
4168
        if ($result) {
4169
            $good_answers = 0;
4170
            switch ($question_type) {
4171
                case FILL_IN_BLANKS:
4172
                    while ($row = Database::fetch_array($result, 'ASSOC')) {
4173
                        $fill_blank = self::check_fill_in_blanks(
4174
                            $correct_answer,
4175
                            $row['answer'],
4176
                            $current_answer
4177
                        );
4178
                        if (isset($fill_blank[$current_answer]) && $fill_blank[$current_answer] == 1) {
4179
                            $good_answers++;
4180
                        }
4181
                    }
4182
4183
                    return $good_answers;
4184
                    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...
4185
                case MATCHING:
4186
                case MATCHING_DRAGGABLE:
4187
                default:
4188
                    $return = Database::num_rows($result);
4189
            }
4190
        }
4191
4192
        return $return;
4193
    }
4194
4195
    /**
4196
     * @param array  $answer
4197
     * @param string $user_answer
4198
     *
4199
     * @return array
4200
     */
4201
    public static function check_fill_in_blanks($answer, $user_answer, $current_answer)
4202
    {
4203
        // the question is encoded like this
4204
        // [A] B [C] D [E] F::10,10,10@1
4205
        // number 1 before the "@" means that is a switchable fill in blank question
4206
        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4207
        // means that is a normal fill blank question
4208
        // first we explode the "::"
4209
        $pre_array = explode('::', $answer);
4210
        // is switchable fill blank or not
4211
        $last = count($pre_array) - 1;
4212
        $is_set_switchable = explode('@', $pre_array[$last]);
4213
        $switchable_answer_set = false;
4214
        if (isset($is_set_switchable[1]) && $is_set_switchable[1] == 1) {
4215
            $switchable_answer_set = true;
4216
        }
4217
        $answer = '';
4218
        for ($k = 0; $k < $last; $k++) {
4219
            $answer .= $pre_array[$k];
4220
        }
4221
        // splits weightings that are joined with a comma
4222
        $answerWeighting = explode(',', $is_set_switchable[0]);
4223
4224
        // we save the answer because it will be modified
4225
        //$temp = $answer;
4226
        $temp = $answer;
4227
4228
        $answer = '';
4229
        $j = 0;
4230
        //initialise answer tags
4231
        $user_tags = $correct_tags = $real_text = [];
4232
        // the loop will stop at the end of the text
4233
        while (1) {
4234
            // quits the loop if there are no more blanks (detect '[')
4235
            if (($pos = api_strpos($temp, '[')) === false) {
4236
                // adds the end of the text
4237
                $answer = $temp;
4238
                $real_text[] = $answer;
4239
                break; //no more "blanks", quit the loop
4240
            }
4241
            // adds the piece of text that is before the blank
4242
            //and ends with '[' into a general storage array
4243
            $real_text[] = api_substr($temp, 0, $pos + 1);
4244
            $answer .= api_substr($temp, 0, $pos + 1);
4245
            //take the string remaining (after the last "[" we found)
4246
            $temp = api_substr($temp, $pos + 1);
4247
            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4248
            if (($pos = api_strpos($temp, ']')) === false) {
4249
                // adds the end of the text
4250
                $answer .= $temp;
4251
                break;
4252
            }
4253
4254
            $str = $user_answer;
4255
4256
            preg_match_all('#\[([^[]*)\]#', $str, $arr);
4257
            $str = str_replace('\r\n', '', $str);
4258
            $choices = $arr[1];
4259
            $choice = [];
4260
            $check = false;
4261
            $i = 0;
4262
            foreach ($choices as $item) {
4263
                if ($current_answer === $item) {
4264
                    $check = true;
4265
                }
4266
                if ($check) {
4267
                    $choice[] = $item;
4268
                    $i++;
4269
                }
4270
                if ($i == 3) {
4271
                    break;
4272
                }
4273
            }
4274
            $tmp = api_strrpos($choice[$j], ' / ');
4275
4276
            if ($tmp !== false) {
4277
                $choice[$j] = api_substr($choice[$j], 0, $tmp);
4278
            }
4279
4280
            $choice[$j] = trim($choice[$j]);
4281
4282
            //Needed to let characters ' and " to work as part of an answer
4283
            $choice[$j] = stripslashes($choice[$j]);
4284
4285
            $user_tags[] = api_strtolower($choice[$j]);
4286
            //put the contents of the [] answer tag into correct_tags[]
4287
            $correct_tags[] = api_strtolower(api_substr($temp, 0, $pos));
4288
            $j++;
4289
            $temp = api_substr($temp, $pos + 1);
4290
        }
4291
4292
        $answer = '';
4293
        $real_correct_tags = $correct_tags;
4294
        $chosen_list = [];
4295
        $good_answer = [];
4296
4297
        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...
4298
            if (!$switchable_answer_set) {
4299
                //needed to parse ' and " characters
4300
                $user_tags[$i] = stripslashes($user_tags[$i]);
4301
                if ($correct_tags[$i] == $user_tags[$i]) {
4302
                    $good_answer[$correct_tags[$i]] = 1;
4303
                } elseif (!empty($user_tags[$i])) {
4304
                    $good_answer[$correct_tags[$i]] = 0;
4305
                } else {
4306
                    $good_answer[$correct_tags[$i]] = 0;
4307
                }
4308
            } else {
4309
                // switchable fill in the blanks
4310
                if (in_array($user_tags[$i], $correct_tags)) {
4311
                    $correct_tags = array_diff($correct_tags, $chosen_list);
4312
                    $good_answer[$correct_tags[$i]] = 1;
4313
                } elseif (!empty($user_tags[$i])) {
4314
                    $good_answer[$correct_tags[$i]] = 0;
4315
                } else {
4316
                    $good_answer[$correct_tags[$i]] = 0;
4317
                }
4318
            }
4319
            // adds the correct word, followed by ] to close the blank
4320
            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4321
            if (isset($real_text[$i + 1])) {
4322
                $answer .= $real_text[$i + 1];
4323
            }
4324
        }
4325
4326
        return $good_answer;
4327
    }
4328
4329
    /**
4330
     * @param int    $exercise_id
4331
     * @param string $course_code
4332
     * @param int    $session_id
4333
     *
4334
     * @return int
4335
     */
4336
    public static function get_number_students_finish_exercise(
4337
        $exercise_id,
4338
        $course_code,
4339
        $session_id
4340
    ) {
4341
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4342
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4343
4344
        $exercise_id = (int) $exercise_id;
4345
        $course_code = Database::escape_string($course_code);
4346
        $session_id = (int) $session_id;
4347
4348
        $sql = "SELECT DISTINCT exe_user_id
4349
                FROM $track_exercises e
4350
                INNER JOIN $track_attempt a
4351
                ON (a.exe_id = e.exe_id)
4352
                WHERE
4353
                    exe_exo_id 	 = $exercise_id AND
4354
                    course_code  = '$course_code' AND
4355
                    e.session_id = $session_id AND
4356
                    status = ''";
4357
        $result = Database::query($sql);
4358
        $return = 0;
4359
        if ($result) {
4360
            $return = Database::num_rows($result);
4361
        }
4362
4363
        return $return;
4364
    }
4365
4366
    /**
4367
     * Return an HTML select menu with the student groups.
4368
     *
4369
     * @param string $name     is the name and the id of the <select>
4370
     * @param string $default  default value for option
4371
     * @param string $onchange
4372
     *
4373
     * @return string the html code of the <select>
4374
     */
4375
    public static function displayGroupMenu($name, $default, $onchange = "")
4376
    {
4377
        // check the default value of option
4378
        $tabSelected = [$default => " selected='selected' "];
4379
        $res = "";
4380
        $res .= "<select name='$name' id='$name' onchange='".$onchange."' >";
4381
        $res .= "<option value='-1'".$tabSelected["-1"].">-- ".get_lang(
4382
                'AllGroups'
4383
            )." --</option>";
4384
        $res .= "<option value='0'".$tabSelected["0"].">- ".get_lang(
4385
                'NotInAGroup'
4386
            )." -</option>";
4387
        $tabGroups = GroupManager::get_group_list();
4388
        $currentCatId = 0;
4389
        $countGroups = count($tabGroups);
4390
        for ($i = 0; $i < $countGroups; $i++) {
4391
            $tabCategory = GroupManager::get_category_from_group(
4392
                $tabGroups[$i]['iid']
4393
            );
4394
            if ($tabCategory["id"] != $currentCatId) {
4395
                $res .= "<option value='-1' disabled='disabled'>".$tabCategory["title"]."</option>";
4396
                $currentCatId = $tabCategory["id"];
4397
            }
4398
            $res .= "<option ".$tabSelected[$tabGroups[$i]["id"]]."style='margin-left:40px' value='".
4399
                $tabGroups[$i]["id"]."'>".
4400
                $tabGroups[$i]["name"].
4401
                "</option>";
4402
        }
4403
        $res .= "</select>";
4404
4405
        return $res;
4406
    }
4407
4408
    /**
4409
     * @param int $exe_id
4410
     */
4411
    public static function create_chat_exercise_session($exe_id)
4412
    {
4413
        if (!isset($_SESSION['current_exercises'])) {
4414
            $_SESSION['current_exercises'] = [];
4415
        }
4416
        $_SESSION['current_exercises'][$exe_id] = true;
4417
    }
4418
4419
    /**
4420
     * @param int $exe_id
4421
     */
4422
    public static function delete_chat_exercise_session($exe_id)
4423
    {
4424
        if (isset($_SESSION['current_exercises'])) {
4425
            $_SESSION['current_exercises'][$exe_id] = false;
4426
        }
4427
    }
4428
4429
    /**
4430
     * Display the exercise results.
4431
     *
4432
     * @param Exercise $objExercise
4433
     * @param int      $exeId
4434
     * @param bool     $save_user_result save users results (true) or just show the results (false)
4435
     * @param string   $remainingMessage
4436
     * @param bool     $allowSignature
4437
     * @param bool     $allowExportPdf
4438
     * @param bool     $isExport
4439
     */
4440
    public static function displayQuestionListByAttempt(
4441
        $objExercise,
4442
        $exeId,
4443
        $save_user_result = false,
4444
        $remainingMessage = '',
4445
        $allowSignature = false,
4446
        $allowExportPdf = false,
4447
        $isExport = false
4448
    ) {
4449
        $origin = api_get_origin();
4450
        $courseId = api_get_course_int_id();
4451
        $courseCode = api_get_course_id();
4452
        $sessionId = api_get_session_id();
4453
4454
        // Getting attempt info
4455
        $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
4456
4457
        // Getting question list
4458
        $question_list = [];
4459
        $studentInfo = [];
4460
        if (!empty($exercise_stat_info['data_tracking'])) {
4461
            $studentInfo = api_get_user_info($exercise_stat_info['exe_user_id']);
4462
            $question_list = explode(',', $exercise_stat_info['data_tracking']);
4463
        } else {
4464
            // Try getting the question list only if save result is off
4465
            if ($save_user_result == false) {
4466
                $question_list = $objExercise->get_validated_question_list();
4467
            }
4468
            if (in_array(
4469
                $objExercise->getFeedbackType(),
4470
                [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4471
            )) {
4472
                $question_list = $objExercise->get_validated_question_list();
4473
            }
4474
        }
4475
4476
        if ($objExercise->getResultAccess()) {
4477
            if ($objExercise->hasResultsAccess($exercise_stat_info) === false) {
4478
                echo Display::return_message(
4479
                    sprintf(get_lang('YouPassedTheLimitOfXMinutesToSeeTheResults'), $objExercise->getResultsAccess())
4480
                );
4481
4482
                return false;
4483
            }
4484
4485
            if (!empty($objExercise->getResultAccess())) {
4486
                $url = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$objExercise->id;
4487
                echo $objExercise->returnTimeLeftDiv();
4488
                echo $objExercise->showSimpleTimeControl(
4489
                    $objExercise->getResultAccessTimeDiff($exercise_stat_info),
4490
                    $url
4491
                );
4492
            }
4493
        }
4494
4495
        $counter = 1;
4496
        $total_score = $total_weight = 0;
4497
        $exercise_content = null;
4498
        // Hide results
4499
        $show_results = false;
4500
        $show_only_score = false;
4501
        if (in_array($objExercise->results_disabled,
4502
            [
4503
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4504
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
4505
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4506
            ]
4507
        )) {
4508
            $show_results = true;
4509
        }
4510
4511
        if (in_array(
4512
            $objExercise->results_disabled,
4513
            [
4514
                RESULT_DISABLE_SHOW_SCORE_ONLY,
4515
                RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES,
4516
                RESULT_DISABLE_RANKING,
4517
            ]
4518
        )
4519
        ) {
4520
            $show_only_score = true;
4521
        }
4522
4523
        // Not display expected answer, but score, and feedback
4524
        $show_all_but_expected_answer = false;
4525
        if ($objExercise->results_disabled == RESULT_DISABLE_SHOW_SCORE_ONLY &&
4526
            $objExercise->getFeedbackType() == EXERCISE_FEEDBACK_TYPE_END
4527
        ) {
4528
            $show_all_but_expected_answer = true;
4529
            $show_results = true;
4530
            $show_only_score = false;
4531
        }
4532
4533
        $showTotalScoreAndUserChoicesInLastAttempt = true;
4534
        $showTotalScore = true;
4535
        $showQuestionScore = true;
4536
        $attemptResult = [];
4537
4538
        if (in_array(
4539
            $objExercise->results_disabled,
4540
            [
4541
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
4542
                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
4543
            ])
4544
        ) {
4545
            $show_only_score = true;
4546
            $show_results = true;
4547
            $numberAttempts = 0;
4548
            if ($objExercise->attempts > 0) {
4549
                $attempts = Event::getExerciseResultsByUser(
4550
                    api_get_user_id(),
4551
                    $objExercise->id,
4552
                    $courseId,
4553
                    $sessionId,
4554
                    $exercise_stat_info['orig_lp_id'],
4555
                    $exercise_stat_info['orig_lp_item_id'],
4556
                    'desc'
4557
                );
4558
                if ($attempts) {
4559
                    $numberAttempts = count($attempts);
4560
                }
4561
4562
                if ($save_user_result) {
4563
                    $numberAttempts++;
4564
                }
4565
4566
                $showTotalScore = false;
4567
                if ($objExercise->results_disabled == RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT) {
4568
                    $showTotalScore = true;
4569
                }
4570
                $showTotalScoreAndUserChoicesInLastAttempt = false;
4571
                if ($numberAttempts >= $objExercise->attempts) {
4572
                    $showTotalScore = true;
4573
                    $show_results = true;
4574
                    $show_only_score = false;
4575
                    $showTotalScoreAndUserChoicesInLastAttempt = true;
4576
                }
4577
            }
4578
4579
            if ($objExercise->results_disabled ==
4580
                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK
4581
            ) {
4582
                $show_only_score = false;
4583
                $show_results = true;
4584
                $show_all_but_expected_answer = false;
4585
                $showTotalScore = false;
4586
                $showQuestionScore = false;
4587
                if ($numberAttempts >= $objExercise->attempts) {
4588
                    $showTotalScore = true;
4589
                    $showQuestionScore = true;
4590
                }
4591
            }
4592
        }
4593
4594
        // When exporting to PDF hide feedback/comment/score show warning in hotspot.
4595
        if ($allowExportPdf && $isExport) {
4596
            $showTotalScore = false;
4597
            $showQuestionScore = false;
4598
            $objExercise->feedback_type = 2;
4599
            $objExercise->hideComment = true;
4600
            $objExercise->hideNoAnswer = true;
4601
            $objExercise->results_disabled = 0;
4602
            $objExercise->hideExpectedAnswer = true;
4603
            $show_results = true;
4604
        }
4605
4606
        if ('embeddable' !== $origin &&
4607
            !empty($exercise_stat_info['exe_user_id']) &&
4608
            !empty($studentInfo)
4609
        ) {
4610
            // Shows exercise header.
4611
            echo $objExercise->showExerciseResultHeader(
4612
                $studentInfo,
4613
                $exercise_stat_info,
4614
                $save_user_result,
4615
                $allowSignature,
4616
                $allowExportPdf
4617
            );
4618
        }
4619
4620
        // Display text when test is finished #4074 and for LP #4227
4621
        $endOfMessage = $objExercise->getTextWhenFinished();
4622
        if (!empty($endOfMessage)) {
4623
            echo Display::div(
4624
                $endOfMessage,
4625
                ['id' => 'quiz_end_message']
4626
            );
4627
        }
4628
4629
        $question_list_answers = [];
4630
        $media_list = [];
4631
        $category_list = [];
4632
        $loadChoiceFromSession = false;
4633
        $fromDatabase = true;
4634
        $exerciseResult = null;
4635
        $exerciseResultCoordinates = null;
4636
        $delineationResults = null;
4637
        if (true === $save_user_result && in_array(
4638
            $objExercise->getFeedbackType(),
4639
            [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4640
        )) {
4641
            $loadChoiceFromSession = true;
4642
            $fromDatabase = false;
4643
            $exerciseResult = Session::read('exerciseResult');
4644
            $exerciseResultCoordinates = Session::read('exerciseResultCoordinates');
4645
            $delineationResults = Session::read('hotspot_delineation_result');
4646
            $delineationResults = isset($delineationResults[$objExercise->id]) ? $delineationResults[$objExercise->id] : null;
4647
        }
4648
4649
        $countPendingQuestions = 0;
4650
        $result = [];
4651
        // Loop over all question to show results for each of them, one by one
4652
        if (!empty($question_list)) {
4653
            foreach ($question_list as $questionId) {
4654
                // Creates a temporary Question object
4655
                $objQuestionTmp = Question::read($questionId, $objExercise->course);
4656
                // This variable came from exercise_submit_modal.php
4657
                ob_start();
4658
                $choice = null;
4659
                $delineationChoice = null;
4660
                if ($loadChoiceFromSession) {
4661
                    $choice = isset($exerciseResult[$questionId]) ? $exerciseResult[$questionId] : null;
4662
                    $delineationChoice = isset($delineationResults[$questionId]) ? $delineationResults[$questionId] : null;
4663
                }
4664
4665
                // We're inside *one* question. Go through each possible answer for this question
4666
                $result = $objExercise->manage_answer(
4667
                    $exeId,
4668
                    $questionId,
4669
                    $choice,
4670
                    'exercise_result',
4671
                    $exerciseResultCoordinates,
4672
                    $save_user_result,
4673
                    $fromDatabase,
4674
                    $show_results,
4675
                    $objExercise->selectPropagateNeg(),
4676
                    $delineationChoice,
4677
                    $showTotalScoreAndUserChoicesInLastAttempt
4678
                );
4679
4680
                if (empty($result)) {
4681
                    continue;
4682
                }
4683
4684
                $total_score += $result['score'];
4685
                $total_weight += $result['weight'];
4686
4687
                $question_list_answers[] = [
4688
                    'question' => $result['open_question'],
4689
                    'answer' => $result['open_answer'],
4690
                    'answer_type' => $result['answer_type'],
4691
                    'generated_oral_file' => $result['generated_oral_file'],
4692
                ];
4693
4694
                $my_total_score = $result['score'];
4695
                $my_total_weight = $result['weight'];
4696
                $scorePassed = self::scorePassed($my_total_score, $my_total_weight);
4697
4698
                // Category report
4699
                $category_was_added_for_this_test = false;
4700
                if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
4701
                    if (!isset($category_list[$objQuestionTmp->category]['score'])) {
4702
                        $category_list[$objQuestionTmp->category]['score'] = 0;
4703
                    }
4704
                    if (!isset($category_list[$objQuestionTmp->category]['total'])) {
4705
                        $category_list[$objQuestionTmp->category]['total'] = 0;
4706
                    }
4707
                    if (!isset($category_list[$objQuestionTmp->category]['total_questions'])) {
4708
                        $category_list[$objQuestionTmp->category]['total_questions'] = 0;
4709
                    }
4710
                    if (!isset($category_list[$objQuestionTmp->category]['passed'])) {
4711
                        $category_list[$objQuestionTmp->category]['passed'] = 0;
4712
                    }
4713
                    if (!isset($category_list[$objQuestionTmp->category]['wrong'])) {
4714
                        $category_list[$objQuestionTmp->category]['wrong'] = 0;
4715
                    }
4716
                    if (!isset($category_list[$objQuestionTmp->category]['no_answer'])) {
4717
                        $category_list[$objQuestionTmp->category]['no_answer'] = 0;
4718
                    }
4719
4720
                    $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
4721
                    $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
4722
                    if ($scorePassed) {
4723
                        // Only count passed if score is not empty
4724
                        if (!empty($my_total_score)) {
4725
                            $category_list[$objQuestionTmp->category]['passed']++;
4726
                        }
4727
                    } else {
4728
                        if ($result['user_answered']) {
4729
                            $category_list[$objQuestionTmp->category]['wrong']++;
4730
                        } else {
4731
                            $category_list[$objQuestionTmp->category]['no_answer']++;
4732
                        }
4733
                    }
4734
4735
                    $category_list[$objQuestionTmp->category]['total_questions']++;
4736
                    $category_was_added_for_this_test = true;
4737
                }
4738
                if (isset($objQuestionTmp->category_list) && !empty($objQuestionTmp->category_list)) {
4739
                    foreach ($objQuestionTmp->category_list as $category_id) {
4740
                        $category_list[$category_id]['score'] += $my_total_score;
4741
                        $category_list[$category_id]['total'] += $my_total_weight;
4742
                        $category_was_added_for_this_test = true;
4743
                    }
4744
                }
4745
4746
                // No category for this question!
4747
                if ($category_was_added_for_this_test == false) {
4748
                    if (!isset($category_list['none']['score'])) {
4749
                        $category_list['none']['score'] = 0;
4750
                    }
4751
                    if (!isset($category_list['none']['total'])) {
4752
                        $category_list['none']['total'] = 0;
4753
                    }
4754
4755
                    $category_list['none']['score'] += $my_total_score;
4756
                    $category_list['none']['total'] += $my_total_weight;
4757
                }
4758
4759
                if ($objExercise->selectPropagateNeg() == 0 && $my_total_score < 0) {
4760
                    $my_total_score = 0;
4761
                }
4762
4763
                $comnt = null;
4764
                if ($show_results) {
4765
                    $comnt = Event::get_comments($exeId, $questionId);
4766
                    $teacherAudio = self::getOralFeedbackAudio(
4767
                        $exeId,
4768
                        $questionId,
4769
                        api_get_user_id()
4770
                    );
4771
4772
                    if (!empty($comnt) || $teacherAudio) {
4773
                        echo '<b>'.get_lang('Feedback').'</b>';
4774
                    }
4775
4776
                    if (!empty($comnt)) {
4777
                        echo self::getFeedbackText($comnt);
4778
                    }
4779
4780
                    if ($teacherAudio) {
4781
                        echo $teacherAudio;
4782
                    }
4783
                }
4784
4785
                $calculatedScore = [
4786
                    'result' => self::show_score(
4787
                        $my_total_score,
4788
                        $my_total_weight,
4789
                        false
4790
                    ),
4791
                    'pass' => $scorePassed,
4792
                    'score' => $my_total_score,
4793
                    'weight' => $my_total_weight,
4794
                    'comments' => $comnt,
4795
                    'user_answered' => $result['user_answered'],
4796
                ];
4797
4798
                $score = [];
4799
                if ($show_results) {
4800
                    $score = $calculatedScore;
4801
                }
4802
                if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION])) {
4803
                    $reviewScore = [
4804
                        'score' => $my_total_score,
4805
                        'comments' => Event::get_comments($exeId, $questionId),
4806
                    ];
4807
                    $check = $objQuestionTmp->isQuestionWaitingReview($reviewScore);
4808
                    if (false === $check) {
4809
                        $countPendingQuestions++;
4810
                    }
4811
                }
4812
4813
                $contents = ob_get_clean();
4814
                $question_content = '';
4815
                if ($show_results) {
4816
                    $question_content = '<div class="question_row_answer">';
4817
                    if (false === $showQuestionScore) {
4818
                        $score = [];
4819
                    }
4820
4821
                    // Shows question title an description
4822
                    $question_content .= $objQuestionTmp->return_header(
4823
                        $objExercise,
4824
                        $counter,
4825
                        $score
4826
                    );
4827
                }
4828
                $counter++;
4829
                $question_content .= $contents;
4830
                if ($show_results) {
4831
                    $question_content .= '</div>';
4832
                }
4833
4834
                $calculatedScore['question_content'] = $question_content;
4835
                $attemptResult[] = $calculatedScore;
4836
4837
                if ($objExercise->showExpectedChoice()) {
4838
                    $exercise_content .= Display::div(
4839
                        Display::panel($question_content),
4840
                        ['class' => 'question-panel']
4841
                    );
4842
                } else {
4843
                    // $show_all_but_expected_answer should not happen at
4844
                    // the same time as $show_results
4845
                    if ($show_results && !$show_only_score) {
4846
                        $exercise_content .= Display::div(
4847
                            Display::panel($question_content),
4848
                            ['class' => 'question-panel']
4849
                        );
4850
                    }
4851
                }
4852
            }
4853
        }
4854
4855
        $totalScoreText = null;
4856
        $certificateBlock = '';
4857
        if (($show_results || $show_only_score) && $showTotalScore) {
4858
            if ($result['answer_type'] == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
4859
                echo '<h1 style="text-align : center; margin : 20px 0;">'.get_lang('YourResults').'</h1><br />';
4860
            }
4861
            $totalScoreText .= '<div class="question_row_score">';
4862
            if ($result['answer_type'] == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
4863
                $totalScoreText .= self::getQuestionDiagnosisRibbon(
4864
                    $objExercise,
4865
                    $total_score,
4866
                    $total_weight,
4867
                    true
4868
                );
4869
            } else {
4870
                $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
4871
                if ('true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)) {
4872
                    $formula = $pluginEvaluation->getFormulaForExercise($objExercise->selectId());
4873
4874
                    if (!empty($formula)) {
4875
                        $total_score = $pluginEvaluation->getResultWithFormula($exeId, $formula);
4876
                        $total_weight = $pluginEvaluation->getMaxScore();
4877
                    }
4878
                }
4879
4880
                $totalScoreText .= self::getTotalScoreRibbon(
4881
                    $objExercise,
4882
                    $total_score,
4883
                    $total_weight,
4884
                    true,
4885
                    $countPendingQuestions
4886
                );
4887
            }
4888
            $totalScoreText .= '</div>';
4889
4890
            if (!empty($studentInfo)) {
4891
                $certificateBlock = self::generateAndShowCertificateBlock(
4892
                    $total_score,
4893
                    $total_weight,
4894
                    $objExercise,
4895
                    $studentInfo['id'],
4896
                    $courseCode,
4897
                    $sessionId
4898
                );
4899
            }
4900
        }
4901
4902
        if ($result['answer_type'] == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
4903
            $chartMultiAnswer = MultipleAnswerTrueFalseDegreeCertainty::displayStudentsChartResults(
4904
                $exeId,
4905
                $objExercise
4906
            );
4907
            echo $chartMultiAnswer;
4908
        }
4909
4910
        if (!empty($category_list) &&
4911
            ($show_results || $show_only_score || RESULT_DISABLE_RADAR == $objExercise->results_disabled)
4912
        ) {
4913
            // Adding total
4914
            $category_list['total'] = [
4915
                'score' => $total_score,
4916
                'total' => $total_weight,
4917
            ];
4918
            echo TestCategory::get_stats_table_by_attempt($objExercise, $category_list);
4919
        }
4920
4921
        if ($show_all_but_expected_answer) {
4922
            $exercise_content .= Display::return_message(get_lang('ExerciseWithFeedbackWithoutCorrectionComment'));
4923
        }
4924
4925
        // Remove audio auto play from questions on results page - refs BT#7939
4926
        $exercise_content = preg_replace(
4927
            ['/autoplay[\=\".+\"]+/', '/autostart[\=\".+\"]+/'],
4928
            '',
4929
            $exercise_content
4930
        );
4931
4932
        echo $totalScoreText;
4933
        echo $certificateBlock;
4934
4935
        // Ofaj change BT#11784
4936
        if (api_get_configuration_value('quiz_show_description_on_results_page') &&
4937
            !empty($objExercise->description)
4938
        ) {
4939
            echo Display::div($objExercise->description, ['class' => 'exercise_description']);
4940
        }
4941
4942
        echo $exercise_content;
4943
        if (!$show_only_score) {
4944
            echo $totalScoreText;
4945
        }
4946
4947
        if ($save_user_result) {
4948
            // Tracking of results
4949
            if ($exercise_stat_info) {
4950
                $learnpath_id = $exercise_stat_info['orig_lp_id'];
4951
                $learnpath_item_id = $exercise_stat_info['orig_lp_item_id'];
4952
                $learnpath_item_view_id = $exercise_stat_info['orig_lp_item_view_id'];
4953
4954
                if (api_is_allowed_to_session_edit()) {
4955
                    Event::updateEventExercise(
4956
                        $exercise_stat_info['exe_id'],
4957
                        $objExercise->selectId(),
4958
                        $total_score,
4959
                        $total_weight,
4960
                        $sessionId,
4961
                        $learnpath_id,
4962
                        $learnpath_item_id,
4963
                        $learnpath_item_view_id,
4964
                        $exercise_stat_info['exe_duration'],
4965
                        $question_list
4966
                    );
4967
4968
                    $allowStats = api_get_configuration_value('allow_gradebook_stats');
4969
                    if ($allowStats) {
4970
                        $objExercise->generateStats(
4971
                            $objExercise->selectId(),
4972
                            api_get_course_info(),
4973
                            $sessionId
4974
                        );
4975
                    }
4976
                }
4977
            }
4978
4979
            // Send notification at the end
4980
            if (!api_is_allowed_to_edit(null, true) &&
4981
                !api_is_excluded_user_type()
4982
            ) {
4983
                $objExercise->send_mail_notification_for_exam(
4984
                    'end',
4985
                    $question_list_answers,
4986
                    $origin,
4987
                    $exeId,
4988
                    $total_score,
4989
                    $total_weight
4990
                );
4991
            }
4992
        }
4993
4994
        if (in_array(
4995
            $objExercise->selectResultsDisabled(),
4996
            [RESULT_DISABLE_RANKING, RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING]
4997
        )) {
4998
            echo Display::page_header(get_lang('Ranking'), null, 'h4');
4999
            echo self::displayResultsInRanking(
5000
                $objExercise,
5001
                api_get_user_id(),
5002
                $courseId,
5003
                $sessionId
5004
            );
5005
        }
5006
5007
        if (!empty($remainingMessage)) {
5008
            echo Display::return_message($remainingMessage, 'normal', false);
5009
        }
5010
5011
        $failedAnswersCount = 0;
5012
        $wrongQuestionHtml = '';
5013
        $all = '';
5014
        foreach ($attemptResult as $item) {
5015
            if (false === $item['pass']) {
5016
                $failedAnswersCount++;
5017
                $wrongQuestionHtml .= $item['question_content'].'<br />';
5018
            }
5019
            $all .= $item['question_content'].'<br />';
5020
        }
5021
5022
        $passed = self::isPassPercentageAttemptPassed(
5023
            $objExercise,
5024
            $total_score,
5025
            $total_weight
5026
        );
5027
5028
        $percentage = 0;
5029
        if (!empty($total_weight)) {
5030
            $percentage = ($total_score / $total_weight) * 100;
5031
        }
5032
5033
        return [
5034
            'category_list' => $category_list,
5035
            'attempts_result_list' => $attemptResult, // array of results
5036
            'exercise_passed' => $passed, // boolean
5037
            'total_answers_count' => count($attemptResult), // int
5038
            'failed_answers_count' => $failedAnswersCount, // int
5039
            'failed_answers_html' => $wrongQuestionHtml,
5040
            'all_answers_html' => $all,
5041
            'total_score' => $total_score,
5042
            'total_weight' => $total_weight,
5043
            'total_percentage' => $percentage,
5044
            'count_pending_questions' => $countPendingQuestions,
5045
        ];
5046
    }
5047
5048
    /**
5049
     * Display the ranking of results in a exercise.
5050
     *
5051
     * @param Exercise $exercise
5052
     * @param int      $currentUserId
5053
     * @param int      $courseId
5054
     * @param int      $sessionId
5055
     *
5056
     * @return string
5057
     */
5058
    public static function displayResultsInRanking($exercise, $currentUserId, $courseId, $sessionId = 0)
5059
    {
5060
        $exerciseId = $exercise->iId;
5061
        $data = self::exerciseResultsInRanking($exerciseId, $courseId, $sessionId);
5062
5063
        $table = new HTML_Table(['class' => 'table table-hover table-striped table-bordered']);
5064
        $table->setHeaderContents(0, 0, get_lang('Position'), ['class' => 'text-right']);
5065
        $table->setHeaderContents(0, 1, get_lang('Username'));
5066
        $table->setHeaderContents(0, 2, get_lang('Score'), ['class' => 'text-right']);
5067
        $table->setHeaderContents(0, 3, get_lang('Date'), ['class' => 'text-center']);
5068
5069
        foreach ($data as $r => $item) {
5070
            if (!isset($item[1])) {
5071
                continue;
5072
            }
5073
            $selected = $item[1]->getId() == $currentUserId;
5074
5075
            foreach ($item as $c => $value) {
5076
                $table->setCellContents($r + 1, $c, $value);
5077
5078
                $attrClass = '';
5079
5080
                if (in_array($c, [0, 2])) {
5081
                    $attrClass = 'text-right';
5082
                } elseif (3 == $c) {
5083
                    $attrClass = 'text-center';
5084
                }
5085
5086
                if ($selected) {
5087
                    $attrClass .= ' warning';
5088
                }
5089
5090
                $table->setCellAttributes($r + 1, $c, ['class' => $attrClass]);
5091
            }
5092
        }
5093
5094
        return $table->toHtml();
5095
    }
5096
5097
    /**
5098
     * Get the ranking for results in a exercise.
5099
     * Function used internally by ExerciseLib::displayResultsInRanking.
5100
     *
5101
     * @param int $exerciseId
5102
     * @param int $courseId
5103
     * @param int $sessionId
5104
     *
5105
     * @return array
5106
     */
5107
    public static function exerciseResultsInRanking($exerciseId, $courseId, $sessionId = 0)
5108
    {
5109
        $em = Database::getManager();
5110
5111
        $dql = 'SELECT DISTINCT te.exeUserId FROM ChamiloCoreBundle:TrackEExercises te WHERE te.exeExoId = :id AND te.cId = :cId';
5112
        $dql .= api_get_session_condition($sessionId, true, false, 'te.sessionId');
5113
5114
        $result = $em
5115
            ->createQuery($dql)
5116
            ->setParameters(['id' => $exerciseId, 'cId' => $courseId])
5117
            ->getScalarResult();
5118
5119
        $data = [];
5120
        /** @var TrackEExercises $item */
5121
        foreach ($result as $item) {
5122
            $data[] = self::get_best_attempt_by_user($item['exeUserId'], $exerciseId, $courseId, $sessionId);
5123
        }
5124
5125
        usort(
5126
            $data,
5127
            function ($a, $b) {
5128
                if ($a['exe_result'] != $b['exe_result']) {
5129
                    return $a['exe_result'] > $b['exe_result'] ? -1 : 1;
5130
                }
5131
5132
                if ($a['exe_date'] != $b['exe_date']) {
5133
                    return $a['exe_date'] < $b['exe_date'] ? -1 : 1;
5134
                }
5135
5136
                return 0;
5137
            }
5138
        );
5139
5140
        // flags to display the same position in case of tie
5141
        $lastScore = $data[0]['exe_result'];
5142
        $position = 1;
5143
        $data = array_map(
5144
            function ($item) use (&$lastScore, &$position) {
5145
                if ($item['exe_result'] < $lastScore) {
5146
                    $position++;
5147
                }
5148
5149
                $lastScore = $item['exe_result'];
5150
5151
                return [
5152
                    $position,
5153
                    api_get_user_entity($item['exe_user_id']),
5154
                    self::show_score($item['exe_result'], $item['exe_weighting'], true, true, true),
5155
                    api_convert_and_format_date($item['exe_date'], DATE_TIME_FORMAT_SHORT),
5156
                ];
5157
            },
5158
            $data
5159
        );
5160
5161
        return $data;
5162
    }
5163
5164
    /**
5165
     * Get a special ribbon on top of "degree of certainty" questions (
5166
     * variation from getTotalScoreRibbon() for other question types).
5167
     *
5168
     * @param Exercise $objExercise
5169
     * @param float    $score
5170
     * @param float    $weight
5171
     * @param bool     $checkPassPercentage
5172
     *
5173
     * @return string
5174
     */
5175
    public static function getQuestionDiagnosisRibbon($objExercise, $score, $weight, $checkPassPercentage = false)
5176
    {
5177
        $displayChartDegree = true;
5178
        $ribbon = $displayChartDegree ? '<div class="ribbon">' : '';
5179
5180
        if ($checkPassPercentage) {
5181
            $passPercentage = $objExercise->selectPassPercentage();
5182
            $isSuccess = self::isSuccessExerciseResult($score, $weight, $passPercentage);
5183
            // Color the final test score if pass_percentage activated
5184
            $ribbonTotalSuccessOrError = '';
5185
            if (self::isPassPercentageEnabled($passPercentage)) {
5186
                if ($isSuccess) {
5187
                    $ribbonTotalSuccessOrError = ' ribbon-total-success';
5188
                } else {
5189
                    $ribbonTotalSuccessOrError = ' ribbon-total-error';
5190
                }
5191
            }
5192
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total '.$ribbonTotalSuccessOrError.'">' : '';
5193
        } else {
5194
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total">' : '';
5195
        }
5196
5197
        if ($displayChartDegree) {
5198
            $ribbon .= '<h3>'.get_lang('YourTotalScore').':&nbsp;';
5199
            $ribbon .= self::show_score($score, $weight, false, true);
5200
            $ribbon .= '</h3>';
5201
            $ribbon .= '</div>';
5202
        }
5203
5204
        if ($checkPassPercentage) {
5205
            $ribbon .= self::showSuccessMessage(
5206
                $score,
5207
                $weight,
5208
                $objExercise->selectPassPercentage()
5209
            );
5210
        }
5211
5212
        $ribbon .= $displayChartDegree ? '</div>' : '';
5213
5214
        return $ribbon;
5215
    }
5216
5217
    public static function isPassPercentageAttemptPassed($objExercise, $score, $weight)
5218
    {
5219
        $passPercentage = $objExercise->selectPassPercentage();
5220
5221
        return self::isSuccessExerciseResult($score, $weight, $passPercentage);
5222
    }
5223
5224
    /**
5225
     * @param float $score
5226
     * @param float $weight
5227
     * @param bool  $checkPassPercentage
5228
     * @param int   $countPendingQuestions
5229
     *
5230
     * @return string
5231
     */
5232
    public static function getTotalScoreRibbon(
5233
        Exercise $objExercise,
5234
        $score,
5235
        $weight,
5236
        $checkPassPercentage = false,
5237
        $countPendingQuestions = 0
5238
    ) {
5239
        $hide = (int) $objExercise->getPageConfigurationAttribute('hide_total_score');
5240
        if (1 === $hide) {
5241
            return '';
5242
        }
5243
5244
        $passPercentage = $objExercise->selectPassPercentage();
5245
        $ribbon = '<div class="title-score">';
5246
        if ($checkPassPercentage) {
5247
            $isSuccess = self::isSuccessExerciseResult(
5248
                $score,
5249
                $weight,
5250
                $passPercentage
5251
            );
5252
            // Color the final test score if pass_percentage activated
5253
            $class = '';
5254
            if (self::isPassPercentageEnabled($passPercentage)) {
5255
                if ($isSuccess) {
5256
                    $class = ' ribbon-total-success';
5257
                } else {
5258
                    $class = ' ribbon-total-error';
5259
                }
5260
            }
5261
            $ribbon .= '<div class="total '.$class.'">';
5262
        } else {
5263
            $ribbon .= '<div class="total">';
5264
        }
5265
        $ribbon .= '<h3>'.get_lang('YourTotalScore').':&nbsp;';
5266
        $ribbon .= self::show_score($score, $weight, false, true);
5267
        $ribbon .= '</h3>';
5268
        $ribbon .= '</div>';
5269
        if ($checkPassPercentage) {
5270
            $ribbon .= self::showSuccessMessage(
5271
                $score,
5272
                $weight,
5273
                $passPercentage
5274
            );
5275
        }
5276
        $ribbon .= '</div>';
5277
5278
        if (!empty($countPendingQuestions)) {
5279
            $ribbon .= '<br />';
5280
            $ribbon .= Display::return_message(
5281
                sprintf(
5282
                    get_lang('TempScoreXQuestionsNotCorrectedYet'),
5283
                    $countPendingQuestions
5284
                ),
5285
                'warning'
5286
            );
5287
        }
5288
5289
        return $ribbon;
5290
    }
5291
5292
    /**
5293
     * @param int $countLetter
5294
     *
5295
     * @return mixed
5296
     */
5297
    public static function detectInputAppropriateClass($countLetter)
5298
    {
5299
        $limits = [
5300
            0 => 'input-mini',
5301
            10 => 'input-mini',
5302
            15 => 'input-medium',
5303
            20 => 'input-xlarge',
5304
            40 => 'input-xlarge',
5305
            60 => 'input-xxlarge',
5306
            100 => 'input-xxlarge',
5307
            200 => 'input-xxlarge',
5308
        ];
5309
5310
        foreach ($limits as $size => $item) {
5311
            if ($countLetter <= $size) {
5312
                return $item;
5313
            }
5314
        }
5315
5316
        return $limits[0];
5317
    }
5318
5319
    /**
5320
     * @param int    $senderId
5321
     * @param array  $course_info
5322
     * @param string $test
5323
     * @param string $url
5324
     *
5325
     * @return string
5326
     */
5327
    public static function getEmailNotification($senderId, $course_info, $test, $url)
5328
    {
5329
        $teacher_info = api_get_user_info($senderId);
5330
        $from_name = api_get_person_name(
5331
            $teacher_info['firstname'],
5332
            $teacher_info['lastname'],
5333
            null,
5334
            PERSON_NAME_EMAIL_ADDRESS
5335
        );
5336
5337
        $view = new Template('', false, false, false, false, false, false);
5338
        $view->assign('course_title', Security::remove_XSS($course_info['name']));
5339
        $view->assign('test_title', Security::remove_XSS($test));
5340
        $view->assign('url', $url);
5341
        $view->assign('teacher_name', $from_name);
5342
        $template = $view->get_template('mail/exercise_result_alert_body.tpl');
5343
5344
        return $view->fetch($template);
5345
    }
5346
5347
    /**
5348
     * @return string
5349
     */
5350
    public static function getNotCorrectedYetText()
5351
    {
5352
        return Display::return_message(get_lang('notCorrectedYet'), 'warning');
5353
    }
5354
5355
    /**
5356
     * @param string $message
5357
     *
5358
     * @return string
5359
     */
5360
    public static function getFeedbackText($message)
5361
    {
5362
        return Display::return_message($message, 'warning', false);
5363
    }
5364
5365
    /**
5366
     * Get the recorder audio component for save a teacher audio feedback.
5367
     *
5368
     * @param int $attemptId
5369
     * @param int $questionId
5370
     * @param int $userId
5371
     *
5372
     * @return string
5373
     */
5374
    public static function getOralFeedbackForm($attemptId, $questionId, $userId)
5375
    {
5376
        $view = new Template('', false, false, false, false, false, false);
5377
        $view->assign('user_id', $userId);
5378
        $view->assign('question_id', $questionId);
5379
        $view->assign('directory', "/../exercises/teacher_audio/$attemptId/");
5380
        $view->assign('file_name', "{$questionId}_{$userId}");
5381
        $template = $view->get_template('exercise/oral_expression.tpl');
5382
5383
        return $view->fetch($template);
5384
    }
5385
5386
    /**
5387
     * Get the audio componen for a teacher audio feedback.
5388
     *
5389
     * @param int $attemptId
5390
     * @param int $questionId
5391
     * @param int $userId
5392
     *
5393
     * @return string
5394
     */
5395
    public static function getOralFeedbackAudio($attemptId, $questionId, $userId)
5396
    {
5397
        $courseInfo = api_get_course_info();
5398
        $sessionId = api_get_session_id();
5399
        $groupId = api_get_group_id();
5400
        $sysCourseDir = api_get_path(SYS_COURSE_PATH).$courseInfo['path'];
5401
        $webCourseDir = api_get_path(WEB_COURSE_PATH).$courseInfo['path'];
5402
        $fileName = "{$questionId}_{$userId}".DocumentManager::getDocumentSuffix($courseInfo, $sessionId, $groupId);
5403
        $filePath = null;
5404
5405
        $relFilePath = "/exercises/teacher_audio/$attemptId/$fileName";
5406
5407
        if (file_exists($sysCourseDir.$relFilePath.'.ogg')) {
5408
            $filePath = $webCourseDir.$relFilePath.'.ogg';
5409
        } elseif (file_exists($sysCourseDir.$relFilePath.'.wav.wav')) {
5410
            $filePath = $webCourseDir.$relFilePath.'.wav.wav';
5411
        } elseif (file_exists($sysCourseDir.$relFilePath.'.wav')) {
5412
            $filePath = $webCourseDir.$relFilePath.'.wav';
5413
        }
5414
5415
        if (!$filePath) {
5416
            return '';
5417
        }
5418
5419
        return Display::tag(
5420
            'audio',
5421
            null,
5422
            ['src' => $filePath]
5423
        );
5424
    }
5425
5426
    /**
5427
     * @return array
5428
     */
5429
    public static function getNotificationSettings()
5430
    {
5431
        $emailAlerts = [
5432
            2 => get_lang('SendEmailToTeacherWhenStudentStartQuiz'),
5433
            1 => get_lang('SendEmailToTeacherWhenStudentEndQuiz'), // default
5434
            3 => get_lang('SendEmailToTeacherWhenStudentEndQuizOnlyIfOpenQuestion'),
5435
            4 => get_lang('SendEmailToTeacherWhenStudentEndQuizOnlyIfOralQuestion'),
5436
        ];
5437
5438
        return $emailAlerts;
5439
    }
5440
5441
    /**
5442
     * Get the additional actions added in exercise_additional_teacher_modify_actions configuration.
5443
     *
5444
     * @param int $exerciseId
5445
     * @param int $iconSize
5446
     *
5447
     * @return string
5448
     */
5449
    public static function getAdditionalTeacherActions($exerciseId, $iconSize = ICON_SIZE_SMALL)
5450
    {
5451
        $additionalActions = api_get_configuration_value('exercise_additional_teacher_modify_actions') ?: [];
5452
        $actions = [];
5453
5454
        foreach ($additionalActions as $additionalAction) {
5455
            $actions[] = call_user_func(
5456
                $additionalAction,
5457
                $exerciseId,
5458
                $iconSize
5459
            );
5460
        }
5461
5462
        return implode(PHP_EOL, $actions);
5463
    }
5464
5465
    /**
5466
     * @param int $userId
5467
     * @param int $courseId
5468
     * @param int $sessionId
5469
     *
5470
     * @throws \Doctrine\ORM\Query\QueryException
5471
     *
5472
     * @return int
5473
     */
5474
    public static function countAnsweredQuestionsByUserAfterTime(DateTime $time, $userId, $courseId, $sessionId)
5475
    {
5476
        $em = Database::getManager();
5477
5478
        $time = api_get_utc_datetime($time->format('Y-m-d H:i:s'), false, true);
5479
5480
        $result = $em
5481
            ->createQuery('
5482
                SELECT COUNT(ea) FROM ChamiloCoreBundle:TrackEAttempt ea
5483
                WHERE ea.userId = :user AND ea.cId = :course AND ea.sessionId = :session
5484
                    AND ea.tms > :time
5485
            ')
5486
            ->setParameters(['user' => $userId, 'course' => $courseId, 'session' => $sessionId, 'time' => $time])
5487
            ->getSingleScalarResult();
5488
5489
        return $result;
5490
    }
5491
5492
    /**
5493
     * @param int $userId
5494
     * @param int $numberOfQuestions
5495
     * @param int $courseId
5496
     * @param int $sessionId
5497
     *
5498
     * @throws \Doctrine\ORM\Query\QueryException
5499
     *
5500
     * @return bool
5501
     */
5502
    public static function isQuestionsLimitPerDayReached($userId, $numberOfQuestions, $courseId, $sessionId)
5503
    {
5504
        $questionsLimitPerDay = (int) api_get_course_setting('quiz_question_limit_per_day');
5505
5506
        if ($questionsLimitPerDay <= 0) {
5507
            return false;
5508
        }
5509
5510
        $midnightTime = ChamiloApi::getServerMidnightTime();
5511
5512
        $answeredQuestionsCount = self::countAnsweredQuestionsByUserAfterTime(
5513
            $midnightTime,
5514
            $userId,
5515
            $courseId,
5516
            $sessionId
5517
        );
5518
5519
        return ($answeredQuestionsCount + $numberOfQuestions) > $questionsLimitPerDay;
5520
    }
5521
5522
    /**
5523
     * Check if an exercise complies with the requirements to be embedded in the mobile app or a video.
5524
     * By making sure it is set on one question per page and it only contains unique-answer or multiple-answer questions
5525
     * or unique-answer image. And that the exam does not have immediate feedback.
5526
     *
5527
     * @param array $exercise Exercise info
5528
     *
5529
     * @throws \Doctrine\ORM\Query\QueryException
5530
     *
5531
     * @return bool
5532
     */
5533
    public static function isQuizEmbeddable(array $exercise)
5534
    {
5535
        $em = Database::getManager();
5536
5537
        if (ONE_PER_PAGE != $exercise['type'] ||
5538
            in_array($exercise['feedback_type'], [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])
5539
        ) {
5540
            return false;
5541
        }
5542
5543
        $countAll = $em
5544
            ->createQuery('SELECT COUNT(qq)
5545
                FROM ChamiloCourseBundle:CQuizQuestion qq
5546
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5547
                   WITH qq.iid = qrq.questionId
5548
                WHERE qrq.exerciceId = :id'
5549
            )
5550
            ->setParameter('id', $exercise['iid'])
5551
            ->getSingleScalarResult();
5552
5553
        $countOfAllowed = $em
5554
            ->createQuery('SELECT COUNT(qq)
5555
                FROM ChamiloCourseBundle:CQuizQuestion qq
5556
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5557
                   WITH qq.iid = qrq.questionId
5558
                WHERE qrq.exerciceId = :id AND qq.type IN (:types)'
5559
            )
5560
            ->setParameters(
5561
                [
5562
                    'id' => $exercise['iid'],
5563
                    'types' => [UNIQUE_ANSWER, MULTIPLE_ANSWER, UNIQUE_ANSWER_IMAGE],
5564
                ]
5565
            )
5566
            ->getSingleScalarResult();
5567
5568
        return $countAll === $countOfAllowed;
5569
    }
5570
5571
    /**
5572
     * Generate a certificate linked to current quiz and.
5573
     * Return the HTML block with links to download and view the certificate.
5574
     *
5575
     * @param float  $totalScore
5576
     * @param float  $totalWeight
5577
     * @param int    $studentId
5578
     * @param string $courseCode
5579
     * @param int    $sessionId
5580
     *
5581
     * @return string
5582
     */
5583
    public static function generateAndShowCertificateBlock(
5584
        $totalScore,
5585
        $totalWeight,
5586
        Exercise $objExercise,
5587
        $studentId,
5588
        $courseCode,
5589
        $sessionId = 0
5590
    ) {
5591
        if (!api_get_configuration_value('quiz_generate_certificate_ending') ||
5592
            !self::isSuccessExerciseResult($totalScore, $totalWeight, $objExercise->selectPassPercentage())
5593
        ) {
5594
            return '';
5595
        }
5596
5597
        /** @var Category $category */
5598
        $category = Category::load(null, null, $courseCode, null, null, $sessionId, 'ORDER By id');
5599
5600
        if (empty($category)) {
5601
            return '';
5602
        }
5603
5604
        /** @var Category $category */
5605
        $category = $category[0];
5606
        $categoryId = $category->get_id();
5607
        $link = LinkFactory::load(
5608
            null,
5609
            null,
5610
            $objExercise->selectId(),
5611
            null,
5612
            $courseCode,
5613
            $categoryId
5614
        );
5615
5616
        if (empty($link)) {
5617
            return '';
5618
        }
5619
5620
        $resourceDeletedMessage = $category->show_message_resource_delete($courseCode);
5621
5622
        if (false !== $resourceDeletedMessage || api_is_allowed_to_edit() || api_is_excluded_user_type()) {
5623
            return '';
5624
        }
5625
5626
        $certificate = Category::generateUserCertificate($categoryId, $studentId);
5627
5628
        if (!is_array($certificate)) {
5629
            return '';
5630
        }
5631
5632
        return Category::getDownloadCertificateBlock($certificate);
5633
    }
5634
5635
    /**
5636
     * @param int $exerciseId
5637
     */
5638
    public static function getExerciseTitleById($exerciseId)
5639
    {
5640
        $em = Database::getManager();
5641
5642
        return $em
5643
            ->createQuery('SELECT cq.title
5644
                FROM ChamiloCourseBundle:CQuiz cq
5645
                WHERE cq.iid = :iid'
5646
            )
5647
            ->setParameter('iid', $exerciseId)
5648
            ->getSingleScalarResult();
5649
    }
5650
5651
    /**
5652
     * @param int $exeId      ID from track_e_exercises
5653
     * @param int $userId     User ID
5654
     * @param int $exerciseId Exercise ID
5655
     * @param int $courseId   Optional. Coure ID.
5656
     *
5657
     * @return TrackEExercises|null
5658
     */
5659
    public static function recalculateResult($exeId, $userId, $exerciseId, $courseId = 0)
5660
    {
5661
        if (empty($userId) || empty($exerciseId)) {
5662
            return null;
5663
        }
5664
5665
        $em = Database::getManager();
5666
        /** @var TrackEExercises $trackedExercise */
5667
        $trackedExercise = $em->getRepository('ChamiloCoreBundle:TrackEExercises')->find($exeId);
5668
5669
        if (empty($trackedExercise)) {
5670
            return null;
5671
        }
5672
5673
        if ($trackedExercise->getExeUserId() != $userId ||
5674
            $trackedExercise->getExeExoId() != $exerciseId
5675
        ) {
5676
            return null;
5677
        }
5678
5679
        $questionList = $trackedExercise->getDataTracking();
5680
5681
        if (empty($questionList)) {
5682
            return null;
5683
        }
5684
5685
        $questionList = explode(',', $questionList);
5686
5687
        $exercise = new Exercise($courseId);
5688
        $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : [];
5689
5690
        if ($exercise->read($exerciseId) === false) {
5691
            return null;
5692
        }
5693
5694
        $totalScore = 0;
5695
        $totalWeight = 0;
5696
5697
        $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
5698
5699
        $formula = 'true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)
5700
            ? $pluginEvaluation->getFormulaForExercise($exerciseId)
5701
            : 0;
5702
5703
        if (empty($formula)) {
5704
            foreach ($questionList as $questionId) {
5705
                $question = Question::read($questionId, $courseInfo);
5706
5707
                if (false === $question) {
5708
                    continue;
5709
                }
5710
5711
                $totalWeight += $question->selectWeighting();
5712
5713
                // We're inside *one* question. Go through each possible answer for this question
5714
                $result = $exercise->manage_answer(
5715
                    $exeId,
5716
                    $questionId,
5717
                    [],
5718
                    'exercise_result',
5719
                    [],
5720
                    false,
5721
                    true,
5722
                    false,
5723
                    $exercise->selectPropagateNeg(),
5724
                    [],
5725
                    [],
5726
                    true
5727
                );
5728
5729
                //  Adding the new score.
5730
                $totalScore += $result['score'];
5731
            }
5732
        } else {
5733
            $totalScore = $pluginEvaluation->getResultWithFormula($exeId, $formula);
5734
            $totalWeight = $pluginEvaluation->getMaxScore();
5735
        }
5736
5737
        $trackedExercise
5738
            ->setExeResult($totalScore)
5739
            ->setExeWeighting($totalWeight);
5740
5741
        $em->persist($trackedExercise);
5742
        $em->flush();
5743
5744
        return $trackedExercise;
5745
    }
5746
5747
    public static function getTotalQuestionAnswered($courseId, $exerciseId, $questionId)
5748
    {
5749
        $courseId = (int) $courseId;
5750
        $exerciseId = (int) $exerciseId;
5751
        $questionId = (int) $questionId;
5752
5753
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5754
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5755
5756
        $sql = "SELECT count(te.exe_id) total
5757
            FROM $attemptTable t
5758
            INNER JOIN $trackTable te
5759
            ON (te.c_id = t.c_id AND t.exe_id = te.exe_id)
5760
            WHERE
5761
                t.c_id = $courseId AND
5762
                exe_exo_id = $exerciseId AND
5763
                t.question_id = $questionId AND
5764
                status != 'incomplete'
5765
        ";
5766
        $queryTotal = Database::query($sql);
5767
        $totalRow = Database::fetch_array($queryTotal, 'ASSOC');
5768
        $total = 0;
5769
        if ($totalRow) {
5770
            $total = (int) $totalRow['total'];
5771
        }
5772
5773
        return $total;
5774
    }
5775
5776
    public static function getWrongQuestionResults($courseId, $exerciseId, $sessionId = 0, $limit = 10)
5777
    {
5778
        $courseId = (int) $courseId;
5779
        $exerciseId = (int) $exerciseId;
5780
        $limit = (int) $limit;
5781
5782
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
5783
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5784
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5785
5786
        $sessionCondition = '';
5787
        if (!empty($sessionId)) {
5788
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
5789
        }
5790
5791
        $sql = "SELECT q.question, question_id, count(q.iid) count
5792
                FROM $attemptTable t
5793
                INNER JOIN $questionTable q
5794
                ON (q.c_id = t.c_id AND q.id = t.question_id)
5795
                INNER JOIN $trackTable te
5796
                ON (te.c_id = q.c_id AND t.exe_id = te.exe_id)
5797
                WHERE
5798
                    t.c_id = $courseId AND
5799
                    t.marks != q.ponderation AND
5800
                    exe_exo_id = $exerciseId AND
5801
                    status != 'incomplete'
5802
                    $sessionCondition
5803
                GROUP BY q.iid
5804
                ORDER BY count DESC
5805
                LIMIT $limit
5806
        ";
5807
5808
        $result = Database::query($sql);
5809
5810
        return Database::store_result($result, 'ASSOC');
5811
    }
5812
5813
    public static function getExerciseResultsCount($type, $courseId, $exerciseId, $sessionId = 0)
5814
    {
5815
        $courseId = (int) $courseId;
5816
        $exerciseId = (int) $exerciseId;
5817
5818
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5819
5820
        $sessionCondition = '';
5821
        if (!empty($sessionId)) {
5822
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
5823
        }
5824
5825
        $selectCount = 'count(DISTINCT te.exe_id)';
5826
        $scoreCondition = '';
5827
        switch ($type) {
5828
            case 'correct_student':
5829
                $selectCount = 'count(DISTINCT te.exe_user_id)';
5830
                $scoreCondition = ' AND exe_result = exe_weighting ';
5831
                break;
5832
            case 'wrong_student':
5833
                $selectCount = 'count(DISTINCT te.exe_user_id)';
5834
                $scoreCondition = ' AND  exe_result != exe_weighting ';
5835
                break;
5836
            case 'correct':
5837
                $scoreCondition = ' AND exe_result = exe_weighting ';
5838
                break;
5839
            case 'wrong':
5840
                $scoreCondition = ' AND exe_result != exe_weighting ';
5841
                break;
5842
        }
5843
5844
        $sql = "SELECT $selectCount count
5845
                FROM $trackTable te
5846
                WHERE
5847
                    c_id = $courseId AND
5848
                    exe_exo_id = $exerciseId AND
5849
                    status != 'incomplete'
5850
                    $scoreCondition
5851
                    $sessionCondition
5852
        ";
5853
        $result = Database::query($sql);
5854
        $totalRow = Database::fetch_array($result, 'ASSOC');
5855
        $total = 0;
5856
        if ($totalRow) {
5857
            $total = (int) $totalRow['count'];
5858
        }
5859
5860
        return $total;
5861
    }
5862
5863
    public static function parseContent($content, $stats, $exercise, $trackInfo, $currentUserId = 0)
5864
    {
5865
        $wrongAnswersCount = $stats['failed_answers_count'];
5866
        $attemptDate = substr($trackInfo['exe_date'], 0, 10);
5867
5868
        $content = str_replace(
5869
            [
5870
                '((exercise_error_count))',
5871
                '((all_answers_html))',
5872
                '((exercise_title))',
5873
                '((exercise_attempt_date))',
5874
            ],
5875
            [
5876
                $wrongAnswersCount,
5877
                $stats['all_answers_html'],
5878
                $exercise->get_formated_title(),
5879
                $attemptDate,
5880
            ],
5881
            $content
5882
        );
5883
5884
        $currentUserId = empty($currentUserId) ? api_get_user_id() : (int) $currentUserId;
5885
5886
        $content = AnnouncementManager::parseContent(
5887
            $currentUserId,
5888
            $content,
5889
            api_get_course_id(),
5890
            api_get_session_id()
5891
        );
5892
5893
        return $content;
5894
    }
5895
5896
    public static function sendNotification(
5897
        $currentUserId,
5898
        $objExercise,
5899
        $exercise_stat_info,
5900
        $courseInfo,
5901
        $attemptCountToSend,
5902
        $stats
5903
    ) {
5904
        $notifications = api_get_configuration_value('exercise_finished_notification_settings');
5905
        if (empty($notifications)) {
5906
            return false;
5907
        }
5908
5909
        $studentId = $exercise_stat_info['exe_user_id'];
5910
        $exerciseExtraFieldValue = new ExtraFieldValue('exercise');
5911
        $wrongAnswersCount = $stats['failed_answers_count'];
5912
        $exercisePassed = $stats['exercise_passed'];
5913
        $countPendingQuestions = $stats['count_pending_questions'];
5914
5915
        // If there are no pending questions (Open questions).
5916
        if (0 === $countPendingQuestions) {
5917
            $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5918
                $objExercise->iId,
5919
                'signature_mandatory'
5920
            );
5921
5922
            if ($extraFieldData && isset($extraFieldData['value']) && 1 === (int) $extraFieldData['value']) {
5923
                if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($objExercise)) {
5924
                    $signature = ExerciseSignaturePlugin::getSignature($studentId, $exercise_stat_info);
5925
                    if (false !== $signature) {
5926
                        //return false;
5927
                    }
5928
                }
5929
            }
5930
5931
            $subject = sprintf(get_lang('WrongAttemptXInCourseX'), $attemptCountToSend, $courseInfo['title']);
5932
            if ($exercisePassed) {
5933
                $subject = sprintf(get_lang('ExerciseValidationInCourseX'), $courseInfo['title']);
5934
            }
5935
5936
            if ($exercisePassed) {
5937
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5938
                    $objExercise->iId,
5939
                    'MailSuccess'
5940
                );
5941
            } else {
5942
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5943
                    $objExercise->iId,
5944
                    'MailAttempt'.$attemptCountToSend
5945
                );
5946
            }
5947
5948
            if ($extraFieldData && isset($extraFieldData['value'])) {
5949
                $content = $extraFieldData['value'];
5950
                $content = self::parseContent($content, $stats, $objExercise, $exercise_stat_info, $studentId);
5951
                if (false === $exercisePassed) {
5952
                    if (0 !== $wrongAnswersCount) {
5953
                        $content .= $stats['failed_answers_html'];
5954
                    }
5955
                }
5956
5957
                // Send to student.
5958
                MessageManager::send_message($currentUserId, $subject, $content);
5959
            }
5960
5961
            // Notifications.
5962
            $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5963
                $objExercise->iId,
5964
                'notifications'
5965
            );
5966
            $exerciseNotification = '';
5967
            if ($extraFieldData && isset($extraFieldData['value'])) {
5968
                $exerciseNotification = $extraFieldData['value'];
5969
            }
5970
5971
            // Blocking exercise.
5972
            $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
5973
                $objExercise->iId,
5974
                'blocking_percentage'
5975
            );
5976
            $blockPercentage = false;
5977
            if ($extraFieldData && isset($extraFieldData['value']) && $extraFieldData['value']) {
5978
                $blockPercentage = $extraFieldData['value'];
5979
            }
5980
5981
            $extraFieldValueUser = new ExtraFieldValue('user');
5982
5983
            if (!empty($exerciseNotification) && !empty($notifications)) {
5984
                foreach ($notifications as $name => $notificationList) {
5985
                    if ($exerciseNotification !== $name) {
5986
                        continue;
5987
                    }
5988
                    foreach ($notificationList as $attemptData) {
5989
                        $skipNotification = false;
5990
                        $skipNotificationList = isset($attemptData['skip_notification_if_user_in_extra_field']) ? $attemptData['skip_notification_if_user_in_extra_field'] : [];
5991
                        if (!empty($skipNotificationList)) {
5992
                            foreach ($skipNotificationList as $skipVariable => $skipValues) {
5993
                                $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
5994
                                    $studentId,
5995
                                    $skipVariable
5996
                                );
5997
                                if (!empty($userExtraFieldValue) && isset($userExtraFieldValue['value'])) {
5998
                                    if (in_array($userExtraFieldValue['value'], $skipValues)) {
5999
                                        $skipNotification = true;
6000
                                        break;
6001
                                    }
6002
                                }
6003
                            }
6004
                        }
6005
6006
                        if ($skipNotification) {
6007
                            continue;
6008
                        }
6009
6010
                        $email = isset($attemptData['email']) ? $attemptData['email'] : '';
6011
                        $emailList = explode(',', $email);
6012
                        if (empty($emailList)) {
6013
                            continue;
6014
                        }
6015
                        $attempts = $attemptData['attempts'];
6016
                        foreach ($attempts as $attempt) {
6017
                            $sendMessage = false;
6018
                            if (isset($attempt['attempt']) && $attemptCountToSend !== (int) $attempt['attempt']) {
6019
                                continue;
6020
                            }
6021
6022
                            if (!isset($attempt['status'])) {
6023
                                continue;
6024
                            }
6025
6026
                            if ($blockPercentage && isset($attempt['is_block_by_percentage'])) {
6027
                                $passBlock = $stats['total_percentage'] > $blockPercentage;
6028
                                if ($attempt['is_block_by_percentage']) {
6029
                                    if ($passBlock) {
6030
                                        continue;
6031
                                    }
6032
                                } else {
6033
                                    if (false === $passBlock) {
6034
                                        continue;
6035
                                    }
6036
                                }
6037
                            }
6038
6039
                            switch ($attempt['status']) {
6040
                                case 'passed':
6041
                                    if ($exercisePassed) {
6042
                                        $sendMessage = true;
6043
                                    }
6044
                                    break;
6045
                                case 'failed':
6046
                                    if (false === $exercisePassed) {
6047
                                        $sendMessage = true;
6048
                                    }
6049
                                    break;
6050
                                case 'all':
6051
                                    $sendMessage = true;
6052
                                    break;
6053
                            }
6054
6055
                            if ($sendMessage) {
6056
                                $attachments = [];
6057
                                if (isset($attempt['add_pdf']) && $attempt['add_pdf']) {
6058
                                    // Get pdf content
6059
                                    $pdfExtraData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6060
                                        $objExercise->iId,
6061
                                        $attempt['add_pdf']
6062
                                    );
6063
6064
                                    if ($pdfExtraData && isset($pdfExtraData['value'])) {
6065
                                        $pdfContent = self::parseContent(
6066
                                            $pdfExtraData['value'],
6067
                                            $stats,
6068
                                            $objExercise,
6069
                                            $exercise_stat_info,
6070
                                            $studentId
6071
                                        );
6072
6073
                                        @$pdf = new PDF();
6074
                                        $filename = get_lang('Exercise');
6075
                                        $cssFile = api_get_path(SYS_CSS_PATH).'themes/chamilo/default.css';
6076
                                        $pdfPath = @$pdf->content_to_pdf(
6077
                                            "<html><body>$pdfContent</body></html>",
6078
                                            file_get_contents($cssFile),
6079
                                            $filename,
6080
                                            api_get_course_id(),
6081
                                            'F',
6082
                                            false,
6083
                                            null,
6084
                                            false,
6085
                                            true
6086
                                        );
6087
                                        $attachments[] = ['filename' => $filename, 'path' => $pdfPath];
6088
                                    }
6089
                                }
6090
6091
                                $content = isset($attempt['content_default']) ? $attempt['content_default'] : '';
6092
                                if (isset($attempt['content'])) {
6093
                                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6094
                                        $objExercise->iId,
6095
                                        $attempt['content']
6096
                                    );
6097
                                    if ($extraFieldData && isset($extraFieldData['value']) && !empty($extraFieldData['value'])) {
6098
                                        $content = $extraFieldData['value'];
6099
                                    }
6100
                                }
6101
6102
                                if (!empty($content)) {
6103
                                    $content = self::parseContent(
6104
                                        $content,
6105
                                        $stats,
6106
                                        $objExercise,
6107
                                        $exercise_stat_info,
6108
                                        $studentId
6109
                                    );
6110
                                    foreach ($emailList as $email) {
6111
                                        if (empty($email)) {
6112
                                            continue;
6113
                                        }
6114
                                        api_mail_html(
6115
                                            null,
6116
                                            $email,
6117
                                            $subject,
6118
                                            $content,
6119
                                            null,
6120
                                            null,
6121
                                            [],
6122
                                            $attachments
6123
                                        );
6124
                                    }
6125
                                }
6126
6127
                                if (isset($attempt['post_actions'])) {
6128
                                    foreach ($attempt['post_actions'] as $action => $params) {
6129
                                        switch ($action) {
6130
                                            case 'subscribe_student_to_courses':
6131
                                                foreach ($params as $code) {
6132
                                                    CourseManager::subscribeUser($currentUserId, $code);
6133
                                                    break;
6134
                                                }
6135
                                                break;
6136
                                        }
6137
                                    }
6138
                                }
6139
                            }
6140
                        }
6141
                    }
6142
                }
6143
            }
6144
        }
6145
    }
6146
6147
    /**
6148
     * Delete an exercise attempt.
6149
     *
6150
     * Log the exe_id deleted with the exe_user_id related.
6151
     *
6152
     * @param int $exeId
6153
     */
6154
    public static function deleteExerciseAttempt($exeId)
6155
    {
6156
        $exeId = (int) $exeId;
6157
6158
        $trackExerciseInfo = ExerciseLib::get_exercise_track_exercise_info($exeId);
6159
6160
        if (empty($trackExerciseInfo)) {
6161
            return;
6162
        }
6163
6164
        $tblTrackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6165
        $tblTrackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
6166
6167
        Database::query("DELETE FROM $tblTrackExercises WHERE exe_id = $exeId");
6168
        Database::query("DELETE FROM $tblTrackAttempt WHERE exe_id = $exeId");
6169
6170
        Event::addEvent(
6171
            LOG_EXERCISE_ATTEMPT_DELETE,
6172
            LOG_EXERCISE_ATTEMPT,
6173
            $exeId,
6174
            api_get_utc_datetime()
6175
        );
6176
        Event::addEvent(
6177
            LOG_EXERCISE_ATTEMPT_DELETE,
6178
            LOG_EXERCISE_AND_USER_ID,
6179
            $exeId.'-'.$trackExerciseInfo['exe_user_id'],
6180
            api_get_utc_datetime()
6181
        );
6182
    }
6183
6184
    public static function scorePassed($score, $total)
6185
    {
6186
        //$scorePassed = $score >= $total;
6187
        $compareResult = bccomp($score, $total, 3);
6188
        $scorePassed = 1 === $compareResult || 0 === $compareResult;
6189
        if (-1 === $scorePassed) {
6190
            $epsilon = 0.00001;
6191
            if (abs($score - $total) < $epsilon) {
6192
                $scorePassed = true;
6193
            }
6194
        }
6195
6196
        return $scorePassed;
6197
    }
6198
}
6199