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

ExerciseLib::check_fill_in_blanks()   D

Complexity

Conditions 19
Paths 156

Size

Total Lines 126
Code Lines 74

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 74
nc 156
nop 3
dl 0
loc 126
rs 4.05
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/* 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