Passed
Push — master ( 271415...ff556a )
by Julito
09:26
created

ExerciseLib::displayQuestionListByAttempt()   F

Complexity

Conditions 84

Size

Total Lines 523
Code Lines 335

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 84
eloc 335
c 2
b 0
f 0
nop 4
dl 0
loc 523
rs 3.3333

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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