Passed
Push — 1.11.x ( 9d130a...b12ed6 )
by Julito
09:42
created

ExerciseLib::showQuestion()   F

Complexity

Conditions 225
Paths > 20000

Size

Total Lines 1574
Code Lines 934

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 225
eloc 934
nop 11
dl 0
loc 1574
rs 0
c 0
b 0
f 0
nc 3624464

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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