Passed
Push — master ( ccb5fd...57fe34 )
by Julito
13:03
created

ExerciseLib::generateAndShowCertificateBlock()   B

Complexity

Conditions 9

Size

Total Lines 50
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
2716
                    $hp_title = GetQuizName($hpresults[$i][3], $documentPath);
2717
                    if ($hp_title == '') {
2718
                        $hp_title = basename($hpresults[$i][3]);
2719
                    }
2720
2721
                    $hp_date = api_get_local_time(
2722
                        $hpresults[$i][6],
2723
                        null,
2724
                        date_default_timezone_get()
2725
                    );
2726
                    $hp_result = round(($hpresults[$i][4] / ($hpresults[$i][5] != 0 ? $hpresults[$i][5] : 1)) * 100, 2);
2727
                    $hp_result .= '% ('.$hpresults[$i][4].' / '.$hpresults[$i][5].')';
2728
2729
                    if ($is_allowedToEdit) {
2730
                        $listInfo[] = [
2731
                            $hpresults[$i][0],
2732
                            $hpresults[$i][1],
2733
                            $hpresults[$i][2],
2734
                            '',
2735
                            $hp_title,
2736
                            '-',
2737
                            $hp_date,
2738
                            $hp_result,
2739
                            '-',
2740
                        ];
2741
                    } else {
2742
                        $listInfo[] = [
2743
                            $hp_title,
2744
                            '-',
2745
                            $hp_date,
2746
                            $hp_result,
2747
                            '-',
2748
                        ];
2749
                    }
2750
                }
2751
            }
2752
        }
2753
2754
        return $listInfo;
2755
    }
2756
2757
    /**
2758
     * @param $score
2759
     * @param $weight
2760
     *
2761
     * @return array
2762
     */
2763
    public static function convertScoreToPlatformSetting($score, $weight)
2764
    {
2765
        $result = ['score' => $score, 'weight' => $weight];
2766
2767
        $maxNote = api_get_setting('exercise_max_score');
2768
        $minNote = api_get_setting('exercise_min_score');
2769
2770
        if ($maxNote != '' && $minNote != '') {
2771
            if (!empty($weight) && intval($weight) != 0) {
2772
                $score = $minNote + ($maxNote - $minNote) * $score / $weight;
2773
            } else {
2774
                $score = $minNote;
2775
            }
2776
            $weight = $maxNote;
2777
        }
2778
2779
        return ['score' => $score, 'weight' => $weight];
2780
    }
2781
2782
    /**
2783
     * Converts the score with the exercise_max_note and exercise_min_score
2784
     * the platform settings + formats the results using the float_format function.
2785
     *
2786
     * @param float  $score
2787
     * @param float  $weight
2788
     * @param bool   $show_percentage       show percentage or not
2789
     * @param bool   $use_platform_settings use or not the platform settings
2790
     * @param bool   $show_only_percentage
2791
     * @param bool   $hidePercentageSign    hide "%" sign
2792
     * @param string $decimalSeparator
2793
     * @param string $thousandSeparator
2794
     * @param bool   $roundValues           This option rounds the float values into a int using ceil()
2795
     *
2796
     * @return string an html with the score modified
2797
     */
2798
    public static function show_score(
2799
        $score,
2800
        $weight,
2801
        $show_percentage = true,
2802
        $use_platform_settings = true,
2803
        $show_only_percentage = false,
2804
        $hidePercentageSign = false,
2805
        $decimalSeparator = '.',
2806
        $thousandSeparator = ',',
2807
        $roundValues = false
2808
    ) {
2809
        if (is_null($score) && is_null($weight)) {
2810
            return '-';
2811
        }
2812
2813
        if ($use_platform_settings) {
2814
            $result = self::convertScoreToPlatformSetting($score, $weight);
2815
            $score = $result['score'];
2816
            $weight = $result['weight'];
2817
        }
2818
        $percentage = (100 * $score) / ($weight != 0 ? $weight : 1);
2819
        // Formats values
2820
        $percentage = float_format($percentage, 1);
2821
        $score = float_format($score, 1);
2822
        $weight = float_format($weight, 1);
2823
        if ($roundValues) {
2824
            $whole = floor($percentage); // 1
2825
            $fraction = $percentage - $whole; // .25
2826
2827
            // Formats values
2828
            if ($fraction >= 0.5) {
2829
                $percentage = ceil($percentage);
2830
            } else {
2831
                $percentage = round($percentage);
2832
            }
2833
2834
            $whole = floor($score); // 1
2835
            $fraction = $score - $whole; // .25
2836
            if ($fraction >= 0.5) {
2837
                $score = ceil($score);
2838
            } else {
2839
                $score = round($score);
2840
            }
2841
2842
            $whole = floor($weight); // 1
2843
            $fraction = $weight - $whole; // .25
2844
            if ($fraction >= 0.5) {
2845
                $weight = ceil($weight);
2846
            } else {
2847
                $weight = round($weight);
2848
            }
2849
        } else {
2850
            // Formats values
2851
            $percentage = float_format($percentage, 1, $decimalSeparator, $thousandSeparator);
2852
            $score = float_format($score, 1, $decimalSeparator, $thousandSeparator);
2853
            $weight = float_format($weight, 1, $decimalSeparator, $thousandSeparator);
2854
        }
2855
2856
        if ($show_percentage) {
2857
            $percentageSign = '%';
2858
            if ($hidePercentageSign) {
2859
                $percentageSign = '';
2860
            }
2861
            $html = $percentage."$percentageSign ($score / $weight)";
2862
            if ($show_only_percentage) {
2863
                $html = $percentage.$percentageSign;
2864
            }
2865
        } else {
2866
            $html = $score.' / '.$weight;
2867
        }
2868
2869
        // Over write score
2870
        $scoreBasedInModel = self::convertScoreToModel($percentage);
2871
        if (!empty($scoreBasedInModel)) {
2872
            $html = $scoreBasedInModel;
2873
        }
2874
2875
        $html = Display::span($html, ['class' => 'score_exercise']);
2876
2877
        return $html;
2878
    }
2879
2880
    /**
2881
     * @param array $model
2882
     * @param float $percentage
2883
     *
2884
     * @return string
2885
     */
2886
    public static function getModelStyle($model, $percentage)
2887
    {
2888
        $modelWithStyle = '<span class="'.$model['css_class'].'">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>';
2889
2890
        return $modelWithStyle;
2891
    }
2892
2893
    /**
2894
     * @param float $percentage value between 0 and 100
2895
     *
2896
     * @return string
2897
     */
2898
    public static function convertScoreToModel($percentage)
2899
    {
2900
        $model = self::getCourseScoreModel();
2901
        if (!empty($model)) {
2902
            $scoreWithGrade = [];
2903
            foreach ($model['score_list'] as $item) {
2904
                if ($percentage >= $item['min'] && $percentage <= $item['max']) {
2905
                    $scoreWithGrade = $item;
2906
                    break;
2907
                }
2908
            }
2909
2910
            if (!empty($scoreWithGrade)) {
2911
                return self::getModelStyle($scoreWithGrade, $percentage);
2912
            }
2913
        }
2914
2915
        return '';
2916
    }
2917
2918
    /**
2919
     * @return array
2920
     */
2921
    public static function getCourseScoreModel()
2922
    {
2923
        $modelList = self::getScoreModels();
2924
        if (empty($modelList)) {
2925
            return [];
2926
        }
2927
2928
        $courseInfo = api_get_course_info();
2929
        if (!empty($courseInfo)) {
2930
            $scoreModelId = api_get_course_setting('score_model_id');
2931
            if ($scoreModelId != -1) {
2932
                $modelIdList = array_column($modelList['models'], 'id');
2933
                if (in_array($scoreModelId, $modelIdList)) {
2934
                    foreach ($modelList['models'] as $item) {
2935
                        if ($item['id'] == $scoreModelId) {
2936
                            return $item;
2937
                        }
2938
                    }
2939
                }
2940
            }
2941
        }
2942
2943
        return [];
2944
    }
2945
2946
    /**
2947
     * @return array
2948
     */
2949
    public static function getScoreModels()
2950
    {
2951
        return api_get_configuration_value('score_grade_model');
2952
    }
2953
2954
    /**
2955
     * @param float  $score
2956
     * @param float  $weight
2957
     * @param string $pass_percentage
2958
     *
2959
     * @return bool
2960
     */
2961
    public static function isSuccessExerciseResult($score, $weight, $pass_percentage)
2962
    {
2963
        $percentage = float_format(
2964
            ($score / ($weight != 0 ? $weight : 1)) * 100,
2965
            1
2966
        );
2967
        if (isset($pass_percentage) && !empty($pass_percentage)) {
2968
            if ($percentage >= $pass_percentage) {
2969
                return true;
2970
            }
2971
        }
2972
2973
        return false;
2974
    }
2975
2976
    /**
2977
     * @param FormValidator $form
2978
     * @param string        $name
2979
     * @param $weight
2980
     * @param $selected
2981
     *
2982
     * @return bool
2983
     */
2984
    public static function addScoreModelInput(
2985
        FormValidator &$form,
2986
        $name,
2987
        $weight,
2988
        $selected
2989
    ) {
2990
        $model = self::getCourseScoreModel();
2991
        if (empty($model)) {
2992
            return false;
2993
        }
2994
2995
        /** @var HTML_QuickForm_select $element */
2996
        $element = $form->createElement(
2997
            'select',
2998
            $name,
2999
            get_lang('Qualification'),
3000
            [],
3001
            ['class' => 'exercise_mark_select']
3002
        );
3003
3004
        foreach ($model['score_list'] as $item) {
3005
            $i = api_number_format($item['score_to_qualify'] / 100 * $weight, 2);
3006
            $label = self::getModelStyle($item, $i);
3007
            $attributes = [
3008
                'class' => $item['css_class'],
3009
            ];
3010
            if ($selected == $i) {
3011
                $attributes['selected'] = 'selected';
3012
            }
3013
            $element->addOption($label, $i, $attributes);
3014
        }
3015
        $form->addElement($element);
3016
    }
3017
3018
    /**
3019
     * @return string
3020
     */
3021
    public static function getJsCode()
3022
    {
3023
        // Filling the scores with the right colors.
3024
        $models = self::getCourseScoreModel();
3025
        $cssListToString = '';
3026
        if (!empty($models)) {
3027
            $cssList = array_column($models['score_list'], 'css_class');
3028
            $cssListToString = implode(' ', $cssList);
3029
        }
3030
3031
        if (empty($cssListToString)) {
3032
            return '';
3033
        }
3034
        $js = <<<EOT
3035
        
3036
        function updateSelect(element) {
3037
            var spanTag = element.parent().find('span.filter-option');
3038
            var value = element.val();
3039
            var selectId = element.attr('id');
3040
            var optionClass = $('#' + selectId + ' option[value="'+value+'"]').attr('class');
3041
            spanTag.removeClass('$cssListToString');
3042
            spanTag.addClass(optionClass);
3043
        }
3044
        
3045
        $(function() {
3046
            // Loading values
3047
            $('.exercise_mark_select').on('loaded.bs.select', function() {
3048
                updateSelect($(this));
3049
            });
3050
            // On change
3051
            $('.exercise_mark_select').on('changed.bs.select', function() {
3052
                updateSelect($(this));
3053
            });
3054
        });
3055
EOT;
3056
3057
        return $js;
3058
    }
3059
3060
    /**
3061
     * @param float  $score
3062
     * @param float  $weight
3063
     * @param string $pass_percentage
3064
     *
3065
     * @return string
3066
     */
3067
    public static function showSuccessMessage($score, $weight, $pass_percentage)
3068
    {
3069
        $res = '';
3070
        if (self::isPassPercentageEnabled($pass_percentage)) {
3071
            $isSuccess = self::isSuccessExerciseResult(
3072
                $score,
3073
                $weight,
3074
                $pass_percentage
3075
            );
3076
3077
            if ($isSuccess) {
3078
                $html = get_lang('CongratulationsYouPassedTheTest');
3079
                $icon = Display::return_icon(
3080
                    'completed.png',
3081
                    get_lang('Correct'),
3082
                    [],
3083
                    ICON_SIZE_MEDIUM
3084
                );
3085
            } else {
3086
                $html = get_lang('YouDidNotReachTheMinimumScore');
3087
                $icon = Display::return_icon(
3088
                    'warning.png',
3089
                    get_lang('Wrong'),
3090
                    [],
3091
                    ICON_SIZE_MEDIUM
3092
                );
3093
            }
3094
            $html = Display::tag('h4', $html);
3095
            $html .= Display::tag(
3096
                'h5',
3097
                $icon,
3098
                ['style' => 'width:40px; padding:2px 10px 0px 0px']
3099
            );
3100
            $res = $html;
3101
        }
3102
3103
        return $res;
3104
    }
3105
3106
    /**
3107
     * Return true if pass_pourcentage activated (we use the pass pourcentage feature
3108
     * return false if pass_percentage = 0 (we don't use the pass pourcentage feature.
3109
     *
3110
     * @param $value
3111
     *
3112
     * @return bool
3113
     *              In this version, pass_percentage and show_success_message are disabled if
3114
     *              pass_percentage is set to 0
3115
     */
3116
    public static function isPassPercentageEnabled($value)
3117
    {
3118
        return $value > 0;
3119
    }
3120
3121
    /**
3122
     * Converts a numeric value in a percentage example 0.66666 to 66.67 %.
3123
     *
3124
     * @param $value
3125
     *
3126
     * @return float Converted number
3127
     */
3128
    public static function convert_to_percentage($value)
3129
    {
3130
        $return = '-';
3131
        if ($value != '') {
3132
            $return = float_format($value * 100, 1).' %';
3133
        }
3134
3135
        return $return;
3136
    }
3137
3138
    /**
3139
     * Converts a score/weight values to the platform scale.
3140
     *
3141
     * @param float $score
3142
     * @param float $weight
3143
     *
3144
     * @deprecated seem not to be used
3145
     *
3146
     * @return float the score rounded converted to the new range
3147
     */
3148
    public static function convert_score($score, $weight)
3149
    {
3150
        $maxNote = api_get_setting('exercise_max_score');
3151
        $minNote = api_get_setting('exercise_min_score');
3152
3153
        if ($score != '' && $weight != '') {
3154
            if ($maxNote != '' && $minNote != '') {
3155
                if (!empty($weight)) {
3156
                    $score = $minNote + ($maxNote - $minNote) * $score / $weight;
3157
                } else {
3158
                    $score = $minNote;
3159
                }
3160
            }
3161
        }
3162
        $score_rounded = float_format($score, 1);
3163
3164
        return $score_rounded;
3165
    }
3166
3167
    /**
3168
     * Getting all active exercises from a course from a session
3169
     * (if a session_id is provided we will show all the exercises in the course +
3170
     * all exercises in the session).
3171
     *
3172
     * @param array  $course_info
3173
     * @param int    $session_id
3174
     * @param bool   $check_publication_dates
3175
     * @param string $search                  Search exercise name
3176
     * @param bool   $search_all_sessions     Search exercises in all sessions
3177
     * @param   int 0 = only inactive exercises
0 ignored issues
show
Documentation Bug introduced by
The doc comment 0 at position 0 could not be parsed: Unknown type name '0' at position 0 in 0.
Loading history...
3178
     *                  1 = only active exercises,
3179
     *                  2 = all exercises
3180
     *                  3 = active <> -1
3181
     *
3182
     * @return array array with exercise data
3183
     */
3184
    public static function get_all_exercises(
3185
        $course_info = null,
3186
        $session_id = 0,
3187
        $check_publication_dates = false,
3188
        $search = '',
3189
        $search_all_sessions = false,
3190
        $active = 2
3191
    ) {
3192
        $course_id = api_get_course_int_id();
3193
3194
        if (!empty($course_info) && !empty($course_info['real_id'])) {
3195
            $course_id = $course_info['real_id'];
3196
        }
3197
3198
        if ($session_id == -1) {
3199
            $session_id = 0;
3200
        }
3201
3202
        $now = api_get_utc_datetime();
3203
        $time_conditions = '';
3204
3205
        if ($check_publication_dates) {
3206
            //start and end are set
3207
            $time_conditions = " AND ((start_time <> '' AND start_time < '$now' AND end_time <> '' AND end_time > '$now' )  OR ";
3208
            // only start is set
3209
            $time_conditions .= " (start_time <> '' AND start_time < '$now' AND end_time is NULL) OR ";
3210
            // only end is set
3211
            $time_conditions .= " (start_time IS NULL AND end_time <> '' AND end_time > '$now') OR ";
3212
            // nothing is set
3213
            $time_conditions .= ' (start_time IS NULL AND end_time IS NULL)) ';
3214
        }
3215
3216
        $needle_where = !empty($search) ? " AND title LIKE '?' " : '';
3217
        $needle = !empty($search) ? "%".$search."%" : '';
3218
3219
        // Show courses by active status
3220
        $active_sql = '';
3221
        if ($active == 3) {
3222
            $active_sql = ' active <> -1 AND';
3223
        } else {
3224
            if ($active != 2) {
3225
                $active_sql = sprintf(' active = %d AND', $active);
3226
            }
3227
        }
3228
3229
        if ($search_all_sessions == true) {
3230
            $conditions = [
3231
                'where' => [
3232
                    $active_sql.' c_id = ? '.$needle_where.$time_conditions => [
3233
                        $course_id,
3234
                        $needle,
3235
                    ],
3236
                ],
3237
                'order' => 'title',
3238
            ];
3239
        } else {
3240
            if (empty($session_id)) {
3241
                $conditions = [
3242
                    'where' => [
3243
                        $active_sql.' (session_id = 0 OR session_id IS NULL) AND c_id = ? '.$needle_where.$time_conditions => [
3244
                            $course_id,
3245
                            $needle,
3246
                        ],
3247
                    ],
3248
                    'order' => 'title',
3249
                ];
3250
            } else {
3251
                $conditions = [
3252
                    'where' => [
3253
                        $active_sql.' (session_id = 0 OR session_id IS NULL OR session_id = ? ) AND c_id = ? '.$needle_where.$time_conditions => [
3254
                            $session_id,
3255
                            $course_id,
3256
                            $needle,
3257
                        ],
3258
                    ],
3259
                    'order' => 'title',
3260
                ];
3261
            }
3262
        }
3263
3264
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
3265
3266
        return Database::select('*', $table, $conditions);
3267
    }
3268
3269
    /**
3270
     * Get exercise information by id.
3271
     *
3272
     * @param int $exerciseId Exercise Id
3273
     * @param int $courseId   The course ID (necessary as c_quiz.id is not unique)
3274
     *
3275
     * @return array Exercise info
3276
     */
3277
    public static function get_exercise_by_id($exerciseId = 0, $courseId = 0)
3278
    {
3279
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
3280
        if (empty($courseId)) {
3281
            $courseId = api_get_course_int_id();
3282
        } else {
3283
            $courseId = intval($courseId);
3284
        }
3285
        $conditions = [
3286
            'where' => [
3287
                'id = ?' => [$exerciseId],
3288
                ' AND c_id = ? ' => $courseId,
3289
            ],
3290
        ];
3291
3292
        return Database::select('*', $table, $conditions);
3293
    }
3294
3295
    /**
3296
     * Getting all exercises (active only or all)
3297
     * from a course from a session
3298
     * (if a session_id is provided we will show all the exercises in the
3299
     * course + all exercises in the session).
3300
     *
3301
     * @param   array   course data
3302
     * @param   int     session id
3303
     * @param    int        course c_id
3304
     * @param bool $only_active_exercises
3305
     *
3306
     * @return array array with exercise data
3307
     *               modified by Hubert Borderiou
3308
     */
3309
    public static function get_all_exercises_for_course_id(
3310
        $course_info = null,
3311
        $session_id = 0,
3312
        $course_id = 0,
3313
        $only_active_exercises = true
3314
    ) {
3315
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
3316
3317
        if ($only_active_exercises) {
3318
            // Only active exercises.
3319
            $sql_active_exercises = "active = 1 AND ";
3320
        } else {
3321
            // Not only active means visible and invisible NOT deleted (-2)
3322
            $sql_active_exercises = "active IN (1, 0) AND ";
3323
        }
3324
3325
        if ($session_id == -1) {
3326
            $session_id = 0;
3327
        }
3328
3329
        $params = [
3330
            $session_id,
3331
            $course_id,
3332
        ];
3333
3334
        if (empty($session_id)) {
3335
            $conditions = [
3336
                'where' => ["$sql_active_exercises (session_id = 0 OR session_id IS NULL) AND c_id = ?" => [$course_id]],
3337
                'order' => 'title',
3338
            ];
3339
        } else {
3340
            // All exercises
3341
            $conditions = [
3342
                'where' => ["$sql_active_exercises (session_id = 0 OR session_id IS NULL OR session_id = ? ) AND c_id=?" => $params],
3343
                'order' => 'title',
3344
            ];
3345
        }
3346
3347
        return Database::select('*', $table, $conditions);
3348
    }
3349
3350
    /**
3351
     * Gets the position of the score based in a given score (result/weight)
3352
     * and the exe_id based in the user list
3353
     * (NO Exercises in LPs ).
3354
     *
3355
     * @param float  $my_score      user score to be compared *attention*
3356
     *                              $my_score = score/weight and not just the score
3357
     * @param int    $my_exe_id     exe id of the exercise
3358
     *                              (this is necessary because if 2 students have the same score the one
3359
     *                              with the minor exe_id will have a best position, just to be fair and FIFO)
3360
     * @param int    $exercise_id
3361
     * @param string $course_code
3362
     * @param int    $session_id
3363
     * @param array  $user_list
3364
     * @param bool   $return_string
3365
     *
3366
     * @return int the position of the user between his friends in a course
3367
     *             (or course within a session)
3368
     */
3369
    public static function get_exercise_result_ranking(
3370
        $my_score,
3371
        $my_exe_id,
3372
        $exercise_id,
3373
        $course_code,
3374
        $session_id = 0,
3375
        $user_list = [],
3376
        $return_string = true
3377
    ) {
3378
        //No score given we return
3379
        if (is_null($my_score)) {
3380
            return '-';
3381
        }
3382
        if (empty($user_list)) {
3383
            return '-';
3384
        }
3385
3386
        $best_attempts = [];
3387
        foreach ($user_list as $user_data) {
3388
            $user_id = $user_data['user_id'];
3389
            $best_attempts[$user_id] = self::get_best_attempt_by_user(
3390
                $user_id,
3391
                $exercise_id,
3392
                $course_code,
3393
                $session_id
3394
            );
3395
        }
3396
3397
        if (empty($best_attempts)) {
3398
            return 1;
3399
        } else {
3400
            $position = 1;
3401
            $my_ranking = [];
3402
            foreach ($best_attempts as $user_id => $result) {
3403
                if (!empty($result['max_score']) && intval($result['max_score']) != 0) {
3404
                    $my_ranking[$user_id] = $result['score'] / $result['max_score'];
3405
                } else {
3406
                    $my_ranking[$user_id] = 0;
3407
                }
3408
            }
3409
            //if (!empty($my_ranking)) {
3410
            asort($my_ranking);
3411
            $position = count($my_ranking);
3412
            if (!empty($my_ranking)) {
3413
                foreach ($my_ranking as $user_id => $ranking) {
3414
                    if ($my_score >= $ranking) {
3415
                        if ($my_score == $ranking && isset($best_attempts[$user_id]['exe_id'])) {
3416
                            $exe_id = $best_attempts[$user_id]['exe_id'];
3417
                            if ($my_exe_id < $exe_id) {
3418
                                $position--;
3419
                            }
3420
                        } else {
3421
                            $position--;
3422
                        }
3423
                    }
3424
                }
3425
            }
3426
            //}
3427
            $return_value = [
3428
                'position' => $position,
3429
                'count' => count($my_ranking),
3430
            ];
3431
3432
            if ($return_string) {
3433
                if (!empty($position) && !empty($my_ranking)) {
3434
                    $return_value = $position.'/'.count($my_ranking);
3435
                } else {
3436
                    $return_value = '-';
3437
                }
3438
            }
3439
3440
            return $return_value;
3441
        }
3442
    }
3443
3444
    /**
3445
     * Gets the position of the score based in a given score (result/weight) and the exe_id based in all attempts
3446
     * (NO Exercises in LPs ) old functionality by attempt.
3447
     *
3448
     * @param   float   user score to be compared attention => score/weight
3449
     * @param   int     exe id of the exercise
3450
     * (this is necessary because if 2 students have the same score the one
3451
     * with the minor exe_id will have a best position, just to be fair and FIFO)
3452
     * @param   int     exercise id
3453
     * @param   string  course code
3454
     * @param   int     session id
3455
     * @param bool $return_string
3456
     *
3457
     * @return int the position of the user between his friends in a course (or course within a session)
3458
     */
3459
    public static function get_exercise_result_ranking_by_attempt(
3460
        $my_score,
3461
        $my_exe_id,
3462
        $exercise_id,
3463
        $courseId,
3464
        $session_id = 0,
3465
        $return_string = true
3466
    ) {
3467
        if (empty($session_id)) {
3468
            $session_id = 0;
3469
        }
3470
        if (is_null($my_score)) {
3471
            return '-';
3472
        }
3473
        $user_results = Event::get_all_exercise_results(
3474
            $exercise_id,
3475
            $courseId,
3476
            $session_id,
3477
            false
3478
        );
3479
        $position_data = [];
3480
        if (empty($user_results)) {
3481
            return 1;
3482
        } else {
3483
            $position = 1;
3484
            $my_ranking = [];
3485
            foreach ($user_results as $result) {
3486
                if (!empty($result['max_score']) && intval($result['max_score']) != 0) {
3487
                    $my_ranking[$result['exe_id']] = $result['score'] / $result['max_score'];
3488
                } else {
3489
                    $my_ranking[$result['exe_id']] = 0;
3490
                }
3491
            }
3492
            asort($my_ranking);
3493
            $position = count($my_ranking);
3494
            if (!empty($my_ranking)) {
3495
                foreach ($my_ranking as $exe_id => $ranking) {
3496
                    if ($my_score >= $ranking) {
3497
                        if ($my_score == $ranking) {
3498
                            if ($my_exe_id < $exe_id) {
3499
                                $position--;
3500
                            }
3501
                        } else {
3502
                            $position--;
3503
                        }
3504
                    }
3505
                }
3506
            }
3507
            $return_value = [
3508
                'position' => $position,
3509
                'count' => count($my_ranking),
3510
            ];
3511
3512
            if ($return_string) {
3513
                if (!empty($position) && !empty($my_ranking)) {
3514
                    return $position.'/'.count($my_ranking);
3515
                }
3516
            }
3517
3518
            return $return_value;
3519
        }
3520
    }
3521
3522
    /**
3523
     * Get the best attempt in a exercise (NO Exercises in LPs ).
3524
     *
3525
     * @param int $exercise_id
3526
     * @param int $courseId
3527
     * @param int $session_id
3528
     *
3529
     * @return array
3530
     */
3531
    public static function get_best_attempt_in_course($exercise_id, $courseId, $session_id)
3532
    {
3533
        $user_results = Event::get_all_exercise_results(
3534
            $exercise_id,
3535
            $courseId,
3536
            $session_id,
3537
            false
3538
        );
3539
3540
        $best_score_data = [];
3541
        $best_score = 0;
3542
        if (!empty($user_results)) {
3543
            foreach ($user_results as $result) {
3544
                if (!empty($result['max_score']) &&
3545
                    intval($result['max_score']) != 0
3546
                ) {
3547
                    $score = $result['score'] / $result['max_score'];
3548
                    if ($score >= $best_score) {
3549
                        $best_score = $score;
3550
                        $best_score_data = $result;
3551
                    }
3552
                }
3553
            }
3554
        }
3555
3556
        return $best_score_data;
3557
    }
3558
3559
    /**
3560
     * Get the best score in a exercise (NO Exercises in LPs ).
3561
     *
3562
     * @param int $user_id
3563
     * @param int $exercise_id
3564
     * @param int $courseId
3565
     * @param int $session_id
3566
     *
3567
     * @return array
3568
     */
3569
    public static function get_best_attempt_by_user(
3570
        $user_id,
3571
        $exercise_id,
3572
        $courseId,
3573
        $session_id
3574
    ) {
3575
        $user_results = Event::get_all_exercise_results(
3576
            $exercise_id,
3577
            $courseId,
3578
            $session_id,
3579
            false,
3580
            $user_id
3581
        );
3582
        $best_score_data = [];
3583
        $best_score = 0;
3584
        if (!empty($user_results)) {
3585
            foreach ($user_results as $result) {
3586
                if (!empty($result['max_score']) && intval($result['max_score']) != 0) {
3587
                    $score = $result['score'] / $result['max_score'];
3588
                    if ($score >= $best_score) {
3589
                        $best_score = $score;
3590
                        $best_score_data = $result;
3591
                    }
3592
                }
3593
            }
3594
        }
3595
3596
        return $best_score_data;
3597
    }
3598
3599
    /**
3600
     * Get average score (NO Exercises in LPs ).
3601
     *
3602
     * @param    int    exercise id
3603
     * @param int $courseId
3604
     * @param    int    session id
3605
     *
3606
     * @return float Average score
3607
     */
3608
    public static function get_average_score($exercise_id, $courseId, $session_id)
3609
    {
3610
        $user_results = Event::get_all_exercise_results(
3611
            $exercise_id,
3612
            $courseId,
3613
            $session_id
3614
        );
3615
        $avg_score = 0;
3616
        if (!empty($user_results)) {
3617
            foreach ($user_results as $result) {
3618
                if (!empty($result['max_score']) && intval($result['max_score']) != 0) {
3619
                    $score = $result['score'] / $result['max_score'];
3620
                    $avg_score += $score;
3621
                }
3622
            }
3623
            $avg_score = float_format($avg_score / count($user_results), 1);
3624
        }
3625
3626
        return $avg_score;
3627
    }
3628
3629
    /**
3630
     * Get average score by score (NO Exercises in LPs ).
3631
     *
3632
     * @param int $courseId
3633
     * @param    int    session id
3634
     *
3635
     * @return float Average score
3636
     */
3637
    public static function get_average_score_by_course($courseId, $session_id)
3638
    {
3639
        $user_results = Event::get_all_exercise_results_by_course(
3640
            $courseId,
3641
            $session_id,
3642
            false
3643
        );
3644
        $avg_score = 0;
3645
        if (!empty($user_results)) {
3646
            foreach ($user_results as $result) {
3647
                if (!empty($result['max_score']) && intval(
3648
                        $result['max_score']
3649
                    ) != 0
3650
                ) {
3651
                    $score = $result['score'] / $result['max_score'];
3652
                    $avg_score += $score;
3653
                }
3654
            }
3655
            // We assume that all max_score
3656
            $avg_score = $avg_score / count($user_results);
3657
        }
3658
3659
        return $avg_score;
3660
    }
3661
3662
    /**
3663
     * @param int $user_id
3664
     * @param int $courseId
3665
     * @param int $session_id
3666
     *
3667
     * @return float|int
3668
     */
3669
    public static function get_average_score_by_course_by_user(
3670
        $user_id,
3671
        $courseId,
3672
        $session_id
3673
    ) {
3674
        $user_results = Event::get_all_exercise_results_by_user(
3675
            $user_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 assume that all max_score
3688
            $avg_score = ($avg_score / count($user_results));
3689
        }
3690
3691
        return $avg_score;
3692
    }
3693
3694
    /**
3695
     * Get average score by score (NO Exercises in LPs ).
3696
     *
3697
     * @param    int        exercise id
3698
     * @param int $courseId
3699
     * @param    int        session id
3700
     *
3701
     * @return float Best average score
3702
     */
3703
    public static function get_best_average_score_by_exercise(
3704
        $exercise_id,
3705
        $courseId,
3706
        $session_id,
3707
        $user_count
3708
    ) {
3709
        $user_results = Event::get_best_exercise_results_by_user(
3710
            $exercise_id,
3711
            $courseId,
3712
            $session_id
3713
        );
3714
        $avg_score = 0;
3715
        if (!empty($user_results)) {
3716
            foreach ($user_results as $result) {
3717
                if (!empty($result['max_score']) && intval($result['max_score']) != 0) {
3718
                    $score = $result['score'] / $result['max_score'];
3719
                    $avg_score += $score;
3720
                }
3721
            }
3722
            // We asumme that all max_score
3723
            if (!empty($user_count)) {
3724
                $avg_score = float_format($avg_score / $user_count, 1) * 100;
3725
            } else {
3726
                $avg_score = 0;
3727
            }
3728
        }
3729
3730
        return $avg_score;
3731
    }
3732
3733
    /**
3734
     * Get average score by score (NO Exercises in LPs ).
3735
     *
3736
     * @param    int        exercise id
3737
     * @param int $courseId
3738
     * @param    int        session id
3739
     *
3740
     * @return float Best average score
3741
     */
3742
    public static function getBestScoreByExercise(
3743
        $exercise_id,
3744
        $courseId,
3745
        $session_id
3746
    ) {
3747
        $user_results = Event::get_best_exercise_results_by_user(
3748
            $exercise_id,
3749
            $courseId,
3750
            $session_id
3751
        );
3752
        $avg_score = 0;
3753
        if (!empty($user_results)) {
3754
            foreach ($user_results as $result) {
3755
                if (!empty($result['max_score']) && intval($result['max_score']) != 0) {
3756
                    $score = $result['score'] / $result['max_score'];
3757
                    $avg_score += $score;
3758
                }
3759
            }
3760
        }
3761
3762
        return $avg_score;
3763
    }
3764
3765
    /**
3766
     * @param string $course_code
3767
     * @param int    $session_id
3768
     *
3769
     * @return array
3770
     */
3771
    public static function get_exercises_to_be_taken($course_code, $session_id)
3772
    {
3773
        $course_info = api_get_course_info($course_code);
3774
        $exercises = self::get_all_exercises($course_info, $session_id);
3775
        $result = [];
3776
        $now = time() + 15 * 24 * 60 * 60;
3777
        foreach ($exercises as $exercise_item) {
3778
            if (isset($exercise_item['end_time']) &&
3779
                !empty($exercise_item['end_time']) &&
3780
                api_strtotime($exercise_item['end_time'], 'UTC') < $now
3781
            ) {
3782
                $result[] = $exercise_item;
3783
            }
3784
        }
3785
3786
        return $result;
3787
    }
3788
3789
    /**
3790
     * Get student results (only in completed exercises) stats by question.
3791
     *
3792
     * @param int    $question_id
3793
     * @param int    $exercise_id
3794
     * @param string $course_code
3795
     * @param int    $session_id
3796
     *
3797
     * @return array
3798
     */
3799
    public static function get_student_stats_by_question(
3800
        $question_id,
3801
        $exercise_id,
3802
        $course_code,
3803
        $session_id
3804
    ) {
3805
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3806
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3807
3808
        $question_id = intval($question_id);
3809
        $exercise_id = intval($exercise_id);
3810
        $course_code = Database::escape_string($course_code);
3811
        $session_id = intval($session_id);
3812
        $courseId = api_get_course_int_id($course_code);
3813
3814
        $sql = "SELECT MAX(marks) as max, MIN(marks) as min, AVG(marks) as average
3815
    		FROM $track_exercises e
3816
    		INNER JOIN $track_attempt a
3817
    		ON (
3818
    		    a.exe_id = e.exe_id AND
3819
    		    e.c_id = a.c_id AND
3820
    		    e.session_id  = a.session_id
3821
            )
3822
    		WHERE
3823
    		    exe_exo_id 	= $exercise_id AND
3824
                a.c_id = $courseId AND
3825
                e.session_id = $session_id AND
3826
                question_id = $question_id AND
3827
                status = ''
3828
            LIMIT 1";
3829
        $result = Database::query($sql);
3830
        $return = [];
3831
        if ($result) {
3832
            $return = Database::fetch_array($result, 'ASSOC');
3833
        }
3834
3835
        return $return;
3836
    }
3837
3838
    /**
3839
     * Get the correct answer count for a fill blanks question.
3840
     *
3841
     * @param int $question_id
3842
     * @param int $exercise_id
3843
     *
3844
     * @return array
3845
     */
3846
    public static function getNumberStudentsFillBlanksAnswerCount(
3847
        $question_id,
3848
        $exercise_id
3849
    ) {
3850
        $listStudentsId = [];
3851
        $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
3852
            api_get_course_id(),
3853
            true
3854
        );
3855
        foreach ($listAllStudentInfo as $i => $listStudentInfo) {
3856
            $listStudentsId[] = $listStudentInfo['user_id'];
3857
        }
3858
3859
        $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3860
            $exercise_id,
3861
            $question_id,
3862
            $listStudentsId,
3863
            '1970-01-01',
3864
            '3000-01-01'
3865
        );
3866
3867
        $arrayCount = [];
3868
3869
        foreach ($listFillTheBlankResult as $resultCount) {
3870
            foreach ($resultCount as $index => $count) {
3871
                //this is only for declare the array index per answer
3872
                $arrayCount[$index] = 0;
3873
            }
3874
        }
3875
3876
        foreach ($listFillTheBlankResult as $resultCount) {
3877
            foreach ($resultCount as $index => $count) {
3878
                $count = ($count === 0) ? 1 : 0;
3879
                $arrayCount[$index] += $count;
3880
            }
3881
        }
3882
3883
        return $arrayCount;
3884
    }
3885
3886
    /**
3887
     * Get the number of questions with answers.
3888
     *
3889
     * @param int    $question_id
3890
     * @param int    $exercise_id
3891
     * @param string $course_code
3892
     * @param int    $session_id
3893
     * @param string $questionType
3894
     *
3895
     * @return int
3896
     */
3897
    public static function get_number_students_question_with_answer_count(
3898
        $question_id,
3899
        $exercise_id,
3900
        $course_code,
3901
        $session_id,
3902
        $questionType = ''
3903
    ) {
3904
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3905
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3906
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3907
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
3908
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3909
3910
        $question_id = intval($question_id);
3911
        $exercise_id = intval($exercise_id);
3912
        $courseId = api_get_course_int_id($course_code);
3913
        $session_id = intval($session_id);
3914
3915
        if ($questionType == FILL_IN_BLANKS) {
3916
            $listStudentsId = [];
3917
            $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
3918
                api_get_course_id(),
3919
                true
3920
            );
3921
            foreach ($listAllStudentInfo as $i => $listStudentInfo) {
3922
                $listStudentsId[] = $listStudentInfo['user_id'];
3923
            }
3924
3925
            $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3926
                $exercise_id,
3927
                $question_id,
3928
                $listStudentsId,
3929
                '1970-01-01',
3930
                '3000-01-01'
3931
            );
3932
3933
            return FillBlanks::getNbResultFillBlankAll($listFillTheBlankResult);
3934
        }
3935
3936
        if (empty($session_id)) {
3937
            $courseCondition = "
3938
            INNER JOIN $courseUser cu
3939
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
3940
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3941
        } else {
3942
            $courseCondition = "
3943
            INNER JOIN $courseUserSession cu
3944
            ON cu.c_id = c.id AND cu.user_id = exe_user_id";
3945
            $courseConditionWhere = " AND cu.status = 0 ";
3946
        }
3947
3948
        $sql = "SELECT DISTINCT exe_user_id
3949
    		FROM $track_exercises e
3950
    		INNER JOIN $track_attempt a
3951
    		ON (
3952
    		    a.exe_id = e.exe_id AND
3953
    		    e.c_id = a.c_id AND
3954
    		    e.session_id  = a.session_id
3955
            )
3956
            INNER JOIN $courseTable c
3957
            ON (c.id = a.c_id)
3958
    		$courseCondition
3959
    		WHERE
3960
    		    exe_exo_id = $exercise_id AND
3961
                a.c_id = $courseId AND
3962
                e.session_id = $session_id AND
3963
                question_id = $question_id AND
3964
                answer <> '0' AND
3965
                e.status = ''
3966
                $courseConditionWhere
3967
            ";
3968
        $result = Database::query($sql);
3969
        $return = 0;
3970
        if ($result) {
3971
            $return = Database::num_rows($result);
3972
        }
3973
3974
        return $return;
3975
    }
3976
3977
    /**
3978
     * Get number of answers to hotspot questions.
3979
     *
3980
     * @param int $answer_id
3981
     * @param int $question_id
3982
     * @param int $exercise_id
3983
     * @param int $courseId
3984
     * @param int $session_id
3985
     *
3986
     * @return int
3987
     */
3988
    public static function get_number_students_answer_hotspot_count(
3989
        $answer_id,
3990
        $question_id,
3991
        $exercise_id,
3992
        $courseId,
3993
        $session_id
3994
    ) {
3995
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3996
        $track_hotspot = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
3997
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3998
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
3999
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4000
4001
        $question_id = (int) $question_id;
4002
        $answer_id = (int) $answer_id;
4003
        $exercise_id = (int) $exercise_id;
4004
        $courseId = (int) $courseId;
4005
        $session_id = (int) $session_id;
4006
4007
        if (empty($session_id)) {
4008
            $courseCondition = "
4009
            INNER JOIN $courseUser cu
4010
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
4011
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4012
        } else {
4013
            $courseCondition = "
4014
            INNER JOIN $courseUserSession cu
4015
            ON cu.c_id = c.id AND cu.user_id = exe_user_id";
4016
            $courseConditionWhere = ' AND cu.status = 0 ';
4017
        }
4018
4019
        $sql = "SELECT DISTINCT exe_user_id
4020
    		FROM $track_exercises e
4021
    		INNER JOIN $track_hotspot a
4022
    		ON (a.hotspot_exe_id = e.exe_id)
4023
    		INNER JOIN $courseTable c
4024
    		ON (a.c_id = c.id)
4025
    		$courseCondition
4026
    		WHERE
4027
    		    exe_exo_id              = $exercise_id AND
4028
                a.c_id 	= $courseId AND
4029
                e.session_id            = $session_id AND
4030
                hotspot_answer_id       = $answer_id AND
4031
                hotspot_question_id     = $question_id AND
4032
                hotspot_correct         =  1 AND
4033
                e.status                = ''
4034
                $courseConditionWhere
4035
            ";
4036
4037
        $result = Database::query($sql);
4038
        $return = 0;
4039
        if ($result) {
4040
            $return = Database::num_rows($result);
4041
        }
4042
4043
        return $return;
4044
    }
4045
4046
    /**
4047
     * @param int    $answer_id
4048
     * @param int    $question_id
4049
     * @param int    $exercise_id
4050
     * @param string $course_code
4051
     * @param int    $session_id
4052
     * @param string $question_type
4053
     * @param string $correct_answer
4054
     * @param string $current_answer
4055
     *
4056
     * @return int
4057
     */
4058
    public static function get_number_students_answer_count(
4059
        $answer_id,
4060
        $question_id,
4061
        $exercise_id,
4062
        $course_code,
4063
        $session_id,
4064
        $question_type = null,
4065
        $correct_answer = null,
4066
        $current_answer = null
4067
    ) {
4068
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4069
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4070
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4071
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4072
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4073
4074
        $question_id = (int) $question_id;
4075
        $answer_id = (int) $answer_id;
4076
        $exercise_id = (int) $exercise_id;
4077
        $courseId = api_get_course_int_id($course_code);
4078
        $session_id = (int) $session_id;
4079
4080
        switch ($question_type) {
4081
            case FILL_IN_BLANKS:
4082
                $answer_condition = '';
4083
                $select_condition = " e.exe_id, answer ";
4084
                break;
4085
            case MATCHING:
4086
            case MATCHING_DRAGGABLE:
4087
            default:
4088
                $answer_condition = " answer = $answer_id AND ";
4089
                $select_condition = ' DISTINCT exe_user_id ';
4090
        }
4091
4092
        if (empty($session_id)) {
4093
            $courseCondition = "
4094
            INNER JOIN $courseUser cu
4095
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
4096
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4097
        } else {
4098
            $courseCondition = "
4099
            INNER JOIN $courseUserSession cu
4100
            ON cu.c_id = a.c_id AND cu.user_id = exe_user_id";
4101
            $courseConditionWhere = ' AND cu.status = 0 ';
4102
        }
4103
4104
        $sql = "SELECT $select_condition
4105
    		FROM $track_exercises e
4106
    		INNER JOIN $track_attempt a
4107
    		ON (
4108
    		    a.exe_id = e.exe_id AND
4109
    		    e.c_id = a.c_id AND
4110
    		    e.session_id  = a.session_id
4111
            )
4112
            INNER JOIN $courseTable c
4113
            ON c.id = a.c_id
4114
    		$courseCondition
4115
    		WHERE
4116
    		    exe_exo_id = $exercise_id AND
4117
                a.c_id = $courseId AND
4118
                e.session_id = $session_id AND
4119
                $answer_condition
4120
                question_id = $question_id AND
4121
                e.status = ''
4122
                $courseConditionWhere
4123
            ";
4124
        $result = Database::query($sql);
4125
        $return = 0;
4126
        if ($result) {
4127
            $good_answers = 0;
4128
            switch ($question_type) {
4129
                case FILL_IN_BLANKS:
4130
                    while ($row = Database::fetch_array($result, 'ASSOC')) {
4131
                        $fill_blank = self::check_fill_in_blanks(
4132
                            $correct_answer,
4133
                            $row['answer'],
4134
                            $current_answer
4135
                        );
4136
                        if (isset($fill_blank[$current_answer]) && $fill_blank[$current_answer] == 1) {
4137
                            $good_answers++;
4138
                        }
4139
                    }
4140
4141
                    return $good_answers;
4142
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
4143
                case MATCHING:
4144
                case MATCHING_DRAGGABLE:
4145
                default:
4146
                    $return = Database::num_rows($result);
4147
            }
4148
        }
4149
4150
        return $return;
4151
    }
4152
4153
    /**
4154
     * @param array  $answer
4155
     * @param string $user_answer
4156
     *
4157
     * @return array
4158
     */
4159
    public static function check_fill_in_blanks($answer, $user_answer, $current_answer)
4160
    {
4161
        // the question is encoded like this
4162
        // [A] B [C] D [E] F::10,10,10@1
4163
        // number 1 before the "@" means that is a switchable fill in blank question
4164
        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4165
        // means that is a normal fill blank question
4166
        // first we explode the "::"
4167
        $pre_array = explode('::', $answer);
4168
        // is switchable fill blank or not
4169
        $last = count($pre_array) - 1;
4170
        $is_set_switchable = explode('@', $pre_array[$last]);
4171
        $switchable_answer_set = false;
4172
        if (isset($is_set_switchable[1]) && $is_set_switchable[1] == 1) {
4173
            $switchable_answer_set = true;
4174
        }
4175
        $answer = '';
4176
        for ($k = 0; $k < $last; $k++) {
4177
            $answer .= $pre_array[$k];
4178
        }
4179
        // splits weightings that are joined with a comma
4180
        $answerWeighting = explode(',', $is_set_switchable[0]);
4181
4182
        // we save the answer because it will be modified
4183
        //$temp = $answer;
4184
        $temp = $answer;
4185
4186
        $answer = '';
4187
        $j = 0;
4188
        //initialise answer tags
4189
        $user_tags = $correct_tags = $real_text = [];
4190
        // the loop will stop at the end of the text
4191
        while (1) {
4192
            // quits the loop if there are no more blanks (detect '[')
4193
            if (($pos = api_strpos($temp, '[')) === false) {
4194
                // adds the end of the text
4195
                $answer = $temp;
4196
                $real_text[] = $answer;
4197
                break; //no more "blanks", quit the loop
4198
            }
4199
            // adds the piece of text that is before the blank
4200
            //and ends with '[' into a general storage array
4201
            $real_text[] = api_substr($temp, 0, $pos + 1);
4202
            $answer .= api_substr($temp, 0, $pos + 1);
4203
            //take the string remaining (after the last "[" we found)
4204
            $temp = api_substr($temp, $pos + 1);
4205
            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4206
            if (($pos = api_strpos($temp, ']')) === false) {
4207
                // adds the end of the text
4208
                $answer .= $temp;
4209
                break;
4210
            }
4211
4212
            $str = $user_answer;
4213
4214
            preg_match_all('#\[([^[]*)\]#', $str, $arr);
4215
            $str = str_replace('\r\n', '', $str);
4216
            $choices = $arr[1];
4217
            $choice = [];
4218
            $check = false;
4219
            $i = 0;
4220
            foreach ($choices as $item) {
4221
                if ($current_answer === $item) {
4222
                    $check = true;
4223
                }
4224
                if ($check) {
4225
                    $choice[] = $item;
4226
                    $i++;
4227
                }
4228
                if ($i == 3) {
4229
                    break;
4230
                }
4231
            }
4232
            $tmp = api_strrpos($choice[$j], ' / ');
4233
4234
            if ($tmp !== false) {
4235
                $choice[$j] = api_substr($choice[$j], 0, $tmp);
4236
            }
4237
4238
            $choice[$j] = trim($choice[$j]);
4239
4240
            //Needed to let characters ' and " to work as part of an answer
4241
            $choice[$j] = stripslashes($choice[$j]);
4242
4243
            $user_tags[] = api_strtolower($choice[$j]);
4244
            //put the contents of the [] answer tag into correct_tags[]
4245
            $correct_tags[] = api_strtolower(api_substr($temp, 0, $pos));
4246
            $j++;
4247
            $temp = api_substr($temp, $pos + 1);
4248
        }
4249
4250
        $answer = '';
4251
        $real_correct_tags = $correct_tags;
4252
        $chosen_list = [];
4253
        $good_answer = [];
4254
4255
        for ($i = 0; $i < count($real_correct_tags); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

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

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

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4256
            if (!$switchable_answer_set) {
4257
                //needed to parse ' and " characters
4258
                $user_tags[$i] = stripslashes($user_tags[$i]);
4259
                if ($correct_tags[$i] == $user_tags[$i]) {
4260
                    $good_answer[$correct_tags[$i]] = 1;
4261
                } elseif (!empty($user_tags[$i])) {
4262
                    $good_answer[$correct_tags[$i]] = 0;
4263
                } else {
4264
                    $good_answer[$correct_tags[$i]] = 0;
4265
                }
4266
            } else {
4267
                // switchable fill in the blanks
4268
                if (in_array($user_tags[$i], $correct_tags)) {
4269
                    $correct_tags = array_diff($correct_tags, $chosen_list);
4270
                    $good_answer[$correct_tags[$i]] = 1;
4271
                } elseif (!empty($user_tags[$i])) {
4272
                    $good_answer[$correct_tags[$i]] = 0;
4273
                } else {
4274
                    $good_answer[$correct_tags[$i]] = 0;
4275
                }
4276
            }
4277
            // adds the correct word, followed by ] to close the blank
4278
            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4279
            if (isset($real_text[$i + 1])) {
4280
                $answer .= $real_text[$i + 1];
4281
            }
4282
        }
4283
4284
        return $good_answer;
4285
    }
4286
4287
    /**
4288
     * @param int    $exercise_id
4289
     * @param string $course_code
4290
     * @param int    $session_id
4291
     *
4292
     * @return int
4293
     */
4294
    public static function get_number_students_finish_exercise(
4295
        $exercise_id,
4296
        $course_code,
4297
        $session_id
4298
    ) {
4299
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4300
        $track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4301
4302
        $exercise_id = (int) $exercise_id;
4303
        $course_code = Database::escape_string($course_code);
4304
        $session_id = (int) $session_id;
4305
4306
        $sql = "SELECT DISTINCT exe_user_id
4307
                FROM $track_exercises e
4308
                INNER JOIN $track_attempt a
4309
                ON (a.exe_id = e.exe_id)
4310
                WHERE
4311
                    exe_exo_id 	 = $exercise_id AND
4312
                    course_code  = '$course_code' AND
4313
                    e.session_id = $session_id AND
4314
                    status = ''";
4315
        $result = Database::query($sql);
4316
        $return = 0;
4317
        if ($result) {
4318
            $return = Database::num_rows($result);
4319
        }
4320
4321
        return $return;
4322
    }
4323
4324
    /**
4325
     * Return an HTML select menu with the student groups.
4326
     *
4327
     * @param string $name     is the name and the id of the <select>
4328
     * @param string $default  default value for option
4329
     * @param string $onchange
4330
     *
4331
     * @return string the html code of the <select>
4332
     */
4333
    public static function displayGroupMenu($name, $default, $onchange = "")
4334
    {
4335
        // check the default value of option
4336
        $tabSelected = [$default => " selected='selected' "];
4337
        $res = "";
4338
        $res .= "<select name='$name' id='$name' onchange='".$onchange."' >";
4339
        $res .= "<option value='-1'".$tabSelected["-1"].">-- ".get_lang(
4340
                'AllGroups'
4341
            )." --</option>";
4342
        $res .= "<option value='0'".$tabSelected["0"].">- ".get_lang(
4343
                'NotInAGroup'
4344
            )." -</option>";
4345
        $tabGroups = GroupManager::get_group_list();
4346
        $currentCatId = 0;
4347
        $countGroups = count($tabGroups);
4348
        for ($i = 0; $i < $countGroups; $i++) {
4349
            $tabCategory = GroupManager::get_category_from_group(
4350
                $tabGroups[$i]['iid']
4351
            );
4352
            if ($tabCategory["id"] != $currentCatId) {
4353
                $res .= "<option value='-1' disabled='disabled'>".$tabCategory["title"]."</option>";
4354
                $currentCatId = $tabCategory["id"];
4355
            }
4356
            $res .= "<option ".$tabSelected[$tabGroups[$i]["id"]]."style='margin-left:40px' value='".
4357
                $tabGroups[$i]["id"]."'>".
4358
                $tabGroups[$i]["name"].
4359
                "</option>";
4360
        }
4361
        $res .= "</select>";
4362
4363
        return $res;
4364
    }
4365
4366
    /**
4367
     * @param int $exe_id
4368
     */
4369
    public static function create_chat_exercise_session($exe_id)
4370
    {
4371
        if (!isset($_SESSION['current_exercises'])) {
4372
            $_SESSION['current_exercises'] = [];
4373
        }
4374
        $_SESSION['current_exercises'][$exe_id] = true;
4375
    }
4376
4377
    /**
4378
     * @param int $exe_id
4379
     */
4380
    public static function delete_chat_exercise_session($exe_id)
4381
    {
4382
        if (isset($_SESSION['current_exercises'])) {
4383
            $_SESSION['current_exercises'][$exe_id] = false;
4384
        }
4385
    }
4386
4387
    /**
4388
     * Display the exercise results.
4389
     *
4390
     * @param Exercise $objExercise
4391
     * @param int      $exeId
4392
     * @param bool     $save_user_result save users results (true) or just show the results (false)
4393
     * @param string   $remainingMessage
4394
     */
4395
    public static function displayQuestionListByAttempt(
4396
        $objExercise,
4397
        $exeId,
4398
        $save_user_result = false,
4399
        $remainingMessage = ''
4400
    ) {
4401
        $origin = api_get_origin();
4402
        $courseCode = api_get_course_id();
4403
        $courseId = api_get_course_int_id();
4404
        $sessionId = api_get_session_id();
4405
4406
        // Getting attempt info
4407
        $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
4408
        $studentInfo = api_get_user_info($exercise_stat_info['exe_user_id']);
4409
4410
        // Getting question list
4411
        $question_list = [];
4412
        if (!empty($exercise_stat_info['data_tracking'])) {
4413
            $question_list = explode(',', $exercise_stat_info['data_tracking']);
4414
        } else {
4415
            // Try getting the question list only if save result is off
4416
            if ($save_user_result == false) {
4417
                $question_list = $objExercise->get_validated_question_list();
4418
            }
4419
            if ($objExercise->selectFeedbackType() == EXERCISE_FEEDBACK_TYPE_DIRECT) {
4420
                $question_list = $objExercise->get_validated_question_list();
4421
            }
4422
        }
4423
4424
        $counter = 1;
4425
        $total_score = $total_weight = 0;
4426
        $exercise_content = null;
4427
4428
        // Hide results
4429
        $show_results = false;
4430
        $show_only_score = false;
4431
4432
        if (in_array($objExercise->results_disabled,
4433
            [
4434
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4435
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
4436
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4437
            ]
4438
        )) {
4439
            $show_results = true;
4440
        }
4441
4442
        if (in_array(
4443
            $objExercise->results_disabled,
4444
            [
4445
                RESULT_DISABLE_SHOW_SCORE_ONLY,
4446
                RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES,
4447
                RESULT_DISABLE_RANKING,
4448
            ]
4449
        )
4450
        ) {
4451
            $show_only_score = true;
4452
        }
4453
4454
        // Not display expected answer, but score, and feedback
4455
        $show_all_but_expected_answer = false;
4456
        if ($objExercise->results_disabled == RESULT_DISABLE_SHOW_SCORE_ONLY &&
4457
            $objExercise->feedback_type == EXERCISE_FEEDBACK_TYPE_END
4458
        ) {
4459
            $show_all_but_expected_answer = true;
4460
            $show_results = true;
4461
            $show_only_score = false;
4462
        }
4463
4464
        $showTotalScoreAndUserChoicesInLastAttempt = true;
4465
        $showTotalScore = true;
4466
        $showQuestionScore = true;
4467
4468
        if (in_array(
4469
            $objExercise->results_disabled,
4470
            [
4471
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
4472
                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
4473
            ])
4474
        ) {
4475
            $show_only_score = true;
4476
            $show_results = true;
4477
            $numberAttempts = 0;
4478
            if ($objExercise->attempts > 0) {
4479
                $attempts = Event::getExerciseResultsByUser(
4480
                    api_get_user_id(),
4481
                    $objExercise->id,
4482
                    api_get_course_int_id(),
4483
                    api_get_session_id(),
4484
                    $exercise_stat_info['orig_lp_id'],
4485
                    $exercise_stat_info['orig_lp_item_id'],
4486
                    'desc'
4487
                );
4488
                if ($attempts) {
4489
                    $numberAttempts = count($attempts);
4490
                }
4491
4492
                if ($save_user_result) {
4493
                    $numberAttempts++;
4494
                }
4495
                $showTotalScore = false;
4496
                $showTotalScoreAndUserChoicesInLastAttempt = false;
4497
                if ($numberAttempts >= $objExercise->attempts) {
4498
                    $showTotalScore = true;
4499
                    $show_results = true;
4500
                    $show_only_score = false;
4501
                    $showTotalScoreAndUserChoicesInLastAttempt = true;
4502
                }
4503
            }
4504
4505
            if ($objExercise->results_disabled ==
4506
                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK
4507
            ) {
4508
                $show_only_score = false;
4509
                $show_results = true;
4510
                $show_all_but_expected_answer = false;
4511
                $showTotalScore = false;
4512
                $showQuestionScore = false;
4513
                if ($numberAttempts >= $objExercise->attempts) {
4514
                    $showTotalScore = true;
4515
                    $showQuestionScore = true;
4516
                }
4517
            }
4518
        }
4519
4520
        if (($show_results || $show_only_score) && $origin !== 'embeddable') {
4521
            if (isset($exercise_stat_info['exe_user_id'])) {
4522
                if ($studentInfo) {
4523
                    // Shows exercise header
4524
                    echo $objExercise->showExerciseResultHeader(
4525
                        $studentInfo,
4526
                        $exercise_stat_info
4527
                    );
4528
                }
4529
            }
4530
        }
4531
4532
        // Display text when test is finished #4074 and for LP #4227
4533
        $endOfMessage = $objExercise->selectTextWhenFinished();
4534
        if (!empty($endOfMessage)) {
4535
            echo Display::return_message($endOfMessage, 'normal', false);
4536
            echo "<div class='clear'>&nbsp;</div>";
4537
        }
4538
4539
        $question_list_answers = [];
4540
        $media_list = [];
4541
        $category_list = [];
4542
        $loadChoiceFromSession = false;
4543
        $fromDatabase = true;
4544
        $exerciseResult = null;
4545
        $exerciseResultCoordinates = null;
4546
        $delineationResults = null;
4547
4548
        if ($objExercise->selectFeedbackType() == EXERCISE_FEEDBACK_TYPE_DIRECT) {
4549
            $loadChoiceFromSession = true;
4550
            $fromDatabase = false;
4551
            $exerciseResult = Session::read('exerciseResult');
4552
            $exerciseResultCoordinates = Session::read('exerciseResultCoordinates');
4553
            $delineationResults = Session::read('hotspot_delineation_result');
4554
            $delineationResults = isset($delineationResults[$objExercise->id]) ? $delineationResults[$objExercise->id] : null;
4555
        }
4556
4557
        $countPendingQuestions = 0;
4558
        $result = [];
4559
        // Loop over all question to show results for each of them, one by one
4560
        if (!empty($question_list)) {
4561
            foreach ($question_list as $questionId) {
4562
                // Creates a temporary Question object
4563
                $objQuestionTmp = Question::read($questionId, $objExercise->course);
4564
                // This variable came from exercise_submit_modal.php
4565
                ob_start();
4566
                $choice = null;
4567
                $delineationChoice = null;
4568
                if ($loadChoiceFromSession) {
4569
                    $choice = isset($exerciseResult[$questionId]) ? $exerciseResult[$questionId] : null;
4570
                    $delineationChoice = isset($delineationResults[$questionId]) ? $delineationResults[$questionId] : null;
4571
                }
4572
4573
                // We're inside *one* question. Go through each possible answer for this question
4574
                $result = $objExercise->manage_answer(
4575
                    $exeId,
4576
                    $questionId,
4577
                    $choice,
4578
                    'exercise_result',
4579
                    $exerciseResultCoordinates,
4580
                    $save_user_result,
4581
                    $fromDatabase,
4582
                    $show_results,
4583
                    $objExercise->selectPropagateNeg(),
4584
                    $delineationChoice,
4585
                    $showTotalScoreAndUserChoicesInLastAttempt
4586
                );
4587
4588
                if (empty($result)) {
4589
                    continue;
4590
                }
4591
4592
                $total_score += $result['score'];
4593
                $total_weight += $result['weight'];
4594
4595
                $question_list_answers[] = [
4596
                    'question' => $result['open_question'],
4597
                    'answer' => $result['open_answer'],
4598
                    'answer_type' => $result['answer_type'],
4599
                    'generated_oral_file' => $result['generated_oral_file'],
4600
                ];
4601
4602
                $my_total_score = $result['score'];
4603
                $my_total_weight = $result['weight'];
4604
4605
                // Category report
4606
                $category_was_added_for_this_test = false;
4607
                if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
4608
                    if (!isset($category_list[$objQuestionTmp->category]['score'])) {
4609
                        $category_list[$objQuestionTmp->category]['score'] = 0;
4610
                    }
4611
                    if (!isset($category_list[$objQuestionTmp->category]['total'])) {
4612
                        $category_list[$objQuestionTmp->category]['total'] = 0;
4613
                    }
4614
                    $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
4615
                    $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
4616
                    $category_was_added_for_this_test = true;
4617
                }
4618
4619
                if (isset($objQuestionTmp->category_list) && !empty($objQuestionTmp->category_list)) {
4620
                    foreach ($objQuestionTmp->category_list as $category_id) {
4621
                        $category_list[$category_id]['score'] += $my_total_score;
4622
                        $category_list[$category_id]['total'] += $my_total_weight;
4623
                        $category_was_added_for_this_test = true;
4624
                    }
4625
                }
4626
4627
                // No category for this question!
4628
                if ($category_was_added_for_this_test == false) {
4629
                    if (!isset($category_list['none']['score'])) {
4630
                        $category_list['none']['score'] = 0;
4631
                    }
4632
                    if (!isset($category_list['none']['total'])) {
4633
                        $category_list['none']['total'] = 0;
4634
                    }
4635
4636
                    $category_list['none']['score'] += $my_total_score;
4637
                    $category_list['none']['total'] += $my_total_weight;
4638
                }
4639
4640
                if ($objExercise->selectPropagateNeg() == 0 && $my_total_score < 0) {
4641
                    $my_total_score = 0;
4642
                }
4643
4644
                $comnt = null;
4645
                if ($show_results) {
4646
                    $comnt = Event::get_comments($exeId, $questionId);
4647
                    $teacherAudio = self::getOralFeedbackAudio(
4648
                        $exeId,
4649
                        $questionId,
4650
                        api_get_user_id()
4651
                    );
4652
4653
                    if (!empty($comnt) || $teacherAudio) {
4654
                        echo '<b>'.get_lang('Feedback').'</b>';
4655
                    }
4656
4657
                    if (!empty($comnt)) {
4658
                        echo self::getFeedbackText($comnt);
4659
                    }
4660
4661
                    if ($teacherAudio) {
4662
                        echo $teacherAudio;
4663
                    }
4664
                }
4665
4666
                $score = [];
4667
                if ($show_results) {
4668
                    $scorePassed = $my_total_score >= $my_total_weight;
4669
                    if (function_exists('bccomp')) {
4670
                        $compareResult = bccomp($my_total_score, $my_total_weight, 3);
4671
                        $scorePassed = $compareResult === 1 || $compareResult === 0;
4672
                    }
4673
                    $score = [
4674
                        'result' => self::show_score(
4675
                            $my_total_score,
4676
                            $my_total_weight,
4677
                            false
4678
                        ),
4679
                        'pass' => $scorePassed,
4680
                        'score' => $my_total_score,
4681
                        'weight' => $my_total_weight,
4682
                        'comments' => $comnt,
4683
                        'user_answered' => $result['user_answered'],
4684
                    ];
4685
                }
4686
4687
                if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION])) {
4688
                    $reviewScore = [
4689
                        'score' => $my_total_score,
4690
                        'comments' => Event::get_comments($exeId, $questionId),
4691
                    ];
4692
                    $check = $objQuestionTmp->isQuestionWaitingReview($reviewScore);
4693
                    if ($check === false) {
4694
                        $countPendingQuestions++;
4695
                    }
4696
                }
4697
4698
                $contents = ob_get_clean();
4699
                $question_content = '';
4700
                if ($show_results) {
4701
                    $question_content = '<div class="question_row_answer">';
4702
                    if ($showQuestionScore == false) {
4703
                        $score = [];
4704
                    }
4705
4706
                    // Shows question title an description
4707
                    $question_content .= $objQuestionTmp->return_header(
4708
                        $objExercise,
4709
                        $counter,
4710
                        $score
4711
                    );
4712
                }
4713
                $counter++;
4714
                $question_content .= $contents;
4715
                if ($show_results) {
4716
                    $question_content .= '</div>';
4717
                }
4718
                if ($objExercise->showExpectedChoice()) {
4719
                    $exercise_content .= Display::div(
4720
                        Display::panel($question_content),
4721
                        ['class' => 'question-panel']
4722
                    );
4723
                } else {
4724
                    // $show_all_but_expected_answer should not happen at
4725
                    // the same time as $show_results
4726
                    if ($show_results && !$show_only_score) {
4727
                        $exercise_content .= Display::div(
4728
                            Display::panel($question_content),
4729
                            ['class' => 'question-panel']
4730
                        );
4731
                    }
4732
                }
4733
            } // end foreach() block that loops over all questions
4734
        }
4735
4736
        $totalScoreText = null;
4737
        $certificateBlock = '';
4738
4739
        if (($show_results || $show_only_score) && $showTotalScore) {
4740
            if ($result['answer_type'] == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
4741
                echo '<h1 style="text-align : center; margin : 20px 0;">'.get_lang('YourResults').'</h1><br />';
4742
            }
4743
            $totalScoreText .= '<div class="question_row_score">';
4744
            if ($result['answer_type'] == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
4745
                $totalScoreText .= self::getQuestionDiagnosisRibbon(
4746
                    $objExercise,
4747
                    $total_score,
4748
                    $total_weight,
4749
                    true
4750
                );
4751
            } else {
4752
                $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
4753
4754
                if ('true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)) {
4755
                    $formula = $pluginEvaluation->getFormulaForExercise($objExercise->selectId());
4756
4757
                    if (!empty($formula)) {
4758
                        $total_score = $pluginEvaluation->getResultWithFormula($exeId, $formula);
4759
                        $total_weight = $pluginEvaluation->getMaxScore();
4760
                    }
4761
                }
4762
4763
                $totalScoreText .= self::getTotalScoreRibbon(
4764
                    $objExercise,
4765
                    $total_score,
4766
                    $total_weight,
4767
                    true,
4768
                    $countPendingQuestions
4769
                );
4770
            }
4771
            $totalScoreText .= '</div>';
4772
4773
            $certificateBlock = self::generateAndShowCertificateBlock(
4774
                $total_score,
4775
                $total_weight,
4776
                $objExercise,
4777
                $studentInfo['id'],
4778
                $courseCode,
4779
                $sessionId
4780
            );
4781
        }
4782
4783
        if ($result['answer_type'] == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
4784
            $chartMultiAnswer = MultipleAnswerTrueFalseDegreeCertainty::displayStudentsChartResults(
4785
                $exeId,
4786
                $objExercise
4787
            );
4788
            echo $chartMultiAnswer;
4789
        }
4790
4791
        if (!empty($category_list) && ($show_results || $show_only_score)) {
4792
            // Adding total
4793
            $category_list['total'] = [
4794
                'score' => $total_score,
4795
                'total' => $total_weight,
4796
            ];
4797
            echo TestCategory::get_stats_table_by_attempt(
4798
                $objExercise->id,
4799
                $category_list
4800
            );
4801
        }
4802
4803
        if ($show_all_but_expected_answer) {
4804
            $exercise_content .= Display::return_message(get_lang('ExerciseWithFeedbackWithoutCorrectionComment'));
4805
        }
4806
4807
        // Remove audio auto play from questions on results page - refs BT#7939
4808
        $exercise_content = preg_replace(
4809
            ['/autoplay[\=\".+\"]+/', '/autostart[\=\".+\"]+/'],
4810
            '',
4811
            $exercise_content
4812
        );
4813
4814
        echo $totalScoreText;
4815
        echo $certificateBlock;
4816
4817
        // Ofaj change BT#11784
4818
        if (api_get_configuration_value('quiz_show_description_on_results_page') &&
4819
            !empty($objExercise->description)
4820
        ) {
4821
            echo Display::div($objExercise->description, ['class' => 'exercise_description']);
4822
        }
4823
4824
        echo $exercise_content;
4825
4826
        if (!$show_only_score) {
4827
            echo $totalScoreText;
4828
        }
4829
4830
        if ($save_user_result) {
4831
            // Tracking of results
4832
            if ($exercise_stat_info) {
4833
                $learnpath_id = $exercise_stat_info['orig_lp_id'];
4834
                $learnpath_item_id = $exercise_stat_info['orig_lp_item_id'];
4835
                $learnpath_item_view_id = $exercise_stat_info['orig_lp_item_view_id'];
4836
4837
                if (api_is_allowed_to_session_edit()) {
4838
                    Event::updateEventExercise(
4839
                        $exercise_stat_info['exe_id'],
4840
                        $objExercise->selectId(),
4841
                        $total_score,
4842
                        $total_weight,
4843
                        api_get_session_id(),
4844
                        $learnpath_id,
4845
                        $learnpath_item_id,
4846
                        $learnpath_item_view_id,
4847
                        $exercise_stat_info['exe_duration'],
4848
                        $question_list
4849
                    );
4850
4851
                    $allowStats = api_get_configuration_value('allow_gradebook_stats');
4852
                    if ($allowStats) {
4853
                        $objExercise->generateStats(
4854
                            $objExercise->selectId(),
4855
                            api_get_course_info(),
4856
                            api_get_session_id()
4857
                        );
4858
                    }
4859
                }
4860
            }
4861
4862
            // Send notification at the end
4863
            if (!api_is_allowed_to_edit(null, true) &&
4864
                !api_is_excluded_user_type()
4865
            ) {
4866
                $objExercise->send_mail_notification_for_exam(
4867
                    'end',
4868
                    $question_list_answers,
4869
                    $origin,
4870
                    $exeId,
4871
                    $total_score,
4872
                    $total_weight
4873
                );
4874
            }
4875
        }
4876
4877
        if (in_array(
4878
            $objExercise->selectResultsDisabled(),
4879
            [RESULT_DISABLE_RANKING, RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING]
4880
        )) {
4881
            echo Display::page_header(get_lang('Ranking'), null, 'h4');
4882
            echo self::displayResultsInRanking(
4883
                $objExercise->iId,
4884
                api_get_user_id(),
4885
                api_get_course_int_id(),
4886
                api_get_session_id()
4887
            );
4888
        }
4889
4890
        if (!empty($remainingMessage)) {
4891
            echo Display::return_message($remainingMessage, 'normal', false);
4892
        }
4893
    }
4894
4895
    /**
4896
     * Display the ranking of results in a exercise.
4897
     *
4898
     * @param int $exerciseId
4899
     * @param int $currentUserId
4900
     * @param int $courseId
4901
     * @param int $sessionId
4902
     *
4903
     * @return string
4904
     */
4905
    public static function displayResultsInRanking($exerciseId, $currentUserId, $courseId, $sessionId = 0)
4906
    {
4907
        $data = self::exerciseResultsInRanking($exerciseId, $courseId, $sessionId);
4908
4909
        $table = new HTML_Table(['class' => 'table table-hover table-bordered']);
4910
        $table->setHeaderContents(0, 0, get_lang('Position'), ['class' => 'text-right']);
4911
        $table->setHeaderContents(0, 1, get_lang('Username'));
4912
        $table->setHeaderContents(0, 2, get_lang('Score'), ['class' => 'text-right']);
4913
        $table->setHeaderContents(0, 3, get_lang('Date'), ['class' => 'text-center']);
4914
4915
        foreach ($data as $r => $item) {
4916
            if (!isset($item[1])) {
4917
                continue;
4918
            }
4919
            $selected = $item[1]->getId() == $currentUserId;
4920
4921
            foreach ($item as $c => $value) {
4922
                $table->setCellContents($r + 1, $c, $value);
4923
4924
                $attrClass = '';
4925
4926
                if (in_array($c, [0, 2])) {
4927
                    $attrClass = 'text-right';
4928
                } elseif (3 == $c) {
4929
                    $attrClass = 'text-center';
4930
                }
4931
4932
                if ($selected) {
4933
                    $attrClass .= ' warning';
4934
                }
4935
4936
                $table->setCellAttributes($r + 1, $c, ['class' => $attrClass]);
4937
            }
4938
        }
4939
4940
        return $table->toHtml();
4941
    }
4942
4943
    /**
4944
     * Get the ranking for results in a exercise.
4945
     * Function used internally by ExerciseLib::displayResultsInRanking.
4946
     *
4947
     * @param int $exerciseId
4948
     * @param int $courseId
4949
     * @param int $sessionId
4950
     *
4951
     * @return array
4952
     */
4953
    public static function exerciseResultsInRanking($exerciseId, $courseId, $sessionId = 0)
4954
    {
4955
        $em = Database::getManager();
4956
4957
        $dql = 'SELECT DISTINCT te.exeUserId FROM ChamiloCoreBundle:TrackEExercises te WHERE te.exeExoId = :id AND te.cId = :cId';
4958
        $dql .= api_get_session_condition($sessionId, true, false, 'te.sessionId');
4959
4960
        $result = $em
4961
            ->createQuery($dql)
4962
            ->setParameters(['id' => $exerciseId, 'cId' => $courseId])
4963
            ->getScalarResult();
4964
4965
        $data = [];
4966
4967
        /** @var TrackEExercises $item */
4968
        foreach ($result as $item) {
4969
            $bestAttemp = self::get_best_attempt_by_user($item['exeUserId'], $exerciseId, $courseId, $sessionId = 0);
4970
4971
            $data[] = $bestAttemp;
4972
        }
4973
4974
        usort(
4975
            $data,
4976
            function ($a, $b) {
4977
                if ($a['exe_result'] != $b['exe_result']) {
4978
                    return $a['exe_result'] > $b['exe_result'] ? -1 : 1;
4979
                }
4980
4981
                if ($a['exe_date'] != $b['exe_date']) {
4982
                    return $a['exe_date'] < $b['exe_date'] ? -1 : 1;
4983
                }
4984
4985
                return 0;
4986
            }
4987
        );
4988
4989
        // flags to display the same position in case of tie
4990
        $lastScore = $data[0]['exe_result'];
4991
        $position = 1;
4992
4993
        $data = array_map(
4994
            function ($item) use (&$lastScore, &$position) {
4995
                if ($item['exe_result'] < $lastScore) {
4996
                    $position++;
4997
                }
4998
4999
                $lastScore = $item['exe_result'];
5000
5001
                return [
5002
                    $position,
5003
                    api_get_user_entity($item['exe_user_id']),
5004
                    self::show_score($item['exe_result'], $item['exe_weighting'], true, true, true),
5005
                    api_convert_and_format_date($item['exe_date'], DATE_TIME_FORMAT_SHORT),
5006
                ];
5007
            },
5008
            $data
5009
        );
5010
5011
        return $data;
5012
    }
5013
5014
    /**
5015
     * Get a special ribbon on top of "degree of certainty" questions (
5016
     * variation from getTotalScoreRibbon() for other question types).
5017
     *
5018
     * @param Exercise $objExercise
5019
     * @param float    $score
5020
     * @param float    $weight
5021
     * @param bool     $checkPassPercentage
5022
     *
5023
     * @return string
5024
     */
5025
    public static function getQuestionDiagnosisRibbon($objExercise, $score, $weight, $checkPassPercentage = false)
5026
    {
5027
        $displayChartDegree = true;
5028
        $ribbon = $displayChartDegree ? '<div class="ribbon">' : '';
5029
5030
        if ($checkPassPercentage) {
5031
            $isSuccess = self::isSuccessExerciseResult(
5032
                $score, $weight, $objExercise->selectPassPercentage()
5033
            );
5034
            // Color the final test score if pass_percentage activated
5035
            $ribbonTotalSuccessOrError = '';
5036
            if (self::isPassPercentageEnabled($objExercise->selectPassPercentage())) {
5037
                if ($isSuccess) {
5038
                    $ribbonTotalSuccessOrError = ' ribbon-total-success';
5039
                } else {
5040
                    $ribbonTotalSuccessOrError = ' ribbon-total-error';
5041
                }
5042
            }
5043
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total '.$ribbonTotalSuccessOrError.'">' : '';
5044
        } else {
5045
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total">' : '';
5046
        }
5047
5048
        if ($displayChartDegree) {
5049
            $ribbon .= '<h3>'.get_lang('YourTotalScore').':&nbsp;';
5050
            $ribbon .= self::show_score($score, $weight, false, true);
5051
            $ribbon .= '</h3>';
5052
            $ribbon .= '</div>';
5053
        }
5054
5055
        if ($checkPassPercentage) {
5056
            $ribbon .= self::showSuccessMessage(
5057
                $score,
5058
                $weight,
5059
                $objExercise->selectPassPercentage()
5060
            );
5061
        }
5062
5063
        $ribbon .= $displayChartDegree ? '</div>' : '';
5064
5065
        return $ribbon;
5066
    }
5067
5068
    /**
5069
     * @param Exercise $objExercise
5070
     * @param float    $score
5071
     * @param float    $weight
5072
     * @param bool     $checkPassPercentage
5073
     * @param int      $countPendingQuestions
5074
     *
5075
     * @return string
5076
     */
5077
    public static function getTotalScoreRibbon(
5078
        Exercise $objExercise,
5079
        $score,
5080
        $weight,
5081
        $checkPassPercentage = false,
5082
        $countPendingQuestions = 0
5083
    ) {
5084
        $passPercentage = $objExercise->selectPassPercentage();
5085
        $ribbon = '<div class="title-score">';
5086
        if ($checkPassPercentage) {
5087
            $isSuccess = self::isSuccessExerciseResult(
5088
                $score,
5089
                $weight,
5090
                $passPercentage
5091
            );
5092
            // Color the final test score if pass_percentage activated
5093
            $class = '';
5094
            if (self::isPassPercentageEnabled($passPercentage)) {
5095
                if ($isSuccess) {
5096
                    $class = ' ribbon-total-success';
5097
                } else {
5098
                    $class = ' ribbon-total-error';
5099
                }
5100
            }
5101
            $ribbon .= '<div class="total '.$class.'">';
5102
        } else {
5103
            $ribbon .= '<div class="total">';
5104
        }
5105
        $ribbon .= '<h3>'.get_lang('YourTotalScore').':&nbsp;';
5106
        $ribbon .= self::show_score($score, $weight, false, true);
5107
        $ribbon .= '</h3>';
5108
        $ribbon .= '</div>';
5109
        if ($checkPassPercentage) {
5110
            $ribbon .= self::showSuccessMessage(
5111
                $score,
5112
                $weight,
5113
                $passPercentage
5114
            );
5115
        }
5116
        $ribbon .= '</div>';
5117
5118
        if (!empty($countPendingQuestions)) {
5119
            $ribbon .= '<br />';
5120
            $ribbon .= Display::return_message(
5121
                sprintf(
5122
                    get_lang('TempScoreXQuestionsNotCorrectedYet'),
5123
                    $countPendingQuestions
5124
                ),
5125
                'warning'
5126
            );
5127
        }
5128
5129
        return $ribbon;
5130
    }
5131
5132
    /**
5133
     * @param int $countLetter
5134
     *
5135
     * @return mixed
5136
     */
5137
    public static function detectInputAppropriateClass($countLetter)
5138
    {
5139
        $limits = [
5140
            0 => 'input-mini',
5141
            10 => 'input-mini',
5142
            15 => 'input-medium',
5143
            20 => 'input-xlarge',
5144
            40 => 'input-xlarge',
5145
            60 => 'input-xxlarge',
5146
            100 => 'input-xxlarge',
5147
            200 => 'input-xxlarge',
5148
        ];
5149
5150
        foreach ($limits as $size => $item) {
5151
            if ($countLetter <= $size) {
5152
                return $item;
5153
            }
5154
        }
5155
5156
        return $limits[0];
5157
    }
5158
5159
    /**
5160
     * @param int    $senderId
5161
     * @param array  $course_info
5162
     * @param string $test
5163
     * @param string $url
5164
     *
5165
     * @return string
5166
     */
5167
    public static function getEmailNotification($senderId, $course_info, $test, $url)
5168
    {
5169
        $teacher_info = api_get_user_info($senderId);
5170
        $fromName = api_get_person_name(
5171
            $teacher_info['firstname'],
5172
            $teacher_info['lastname'],
5173
            null,
5174
            PERSON_NAME_EMAIL_ADDRESS
5175
        );
5176
5177
        $params = [
5178
            'course_title' => Security::remove_XSS($course_info['name']),
5179
            'test_title' => Security::remove_XSS($test),
5180
            'url' => $url,
5181
            'teacher_name' => $fromName,
5182
        ];
5183
5184
        return Container::getTwig()->render(
5185
            '@ChamiloTheme/Mailer/Exercise/result_alert_body.html.twig',
5186
            $params
5187
        );
5188
    }
5189
5190
    /**
5191
     * @return string
5192
     */
5193
    public static function getNotCorrectedYetText()
5194
    {
5195
        return Display::return_message(get_lang('notCorrectedYet'), 'warning');
5196
    }
5197
5198
    /**
5199
     * @param string $message
5200
     *
5201
     * @return string
5202
     */
5203
    public static function getFeedbackText($message)
5204
    {
5205
        return Display::return_message($message, 'warning', false);
5206
    }
5207
5208
    /**
5209
     * Get the recorder audio component for save a teacher audio feedback.
5210
     *
5211
     * @param int $attemptId
5212
     * @param int $questionId
5213
     * @param int $userId
5214
     *
5215
     * @return string
5216
     */
5217
    public static function getOralFeedbackForm($attemptId, $questionId, $userId)
5218
    {
5219
        $view = new Template('', false, false, false, false, false, false);
5220
        $view->assign('user_id', $userId);
5221
        $view->assign('question_id', $questionId);
5222
        $view->assign('directory', "/../exercises/teacher_audio/$attemptId/");
5223
        $view->assign('file_name', "{$questionId}_{$userId}");
5224
        $template = $view->get_template('exercise/oral_expression.tpl');
5225
5226
        return $view->fetch($template);
5227
    }
5228
5229
    /**
5230
     * Get the audio componen for a teacher audio feedback.
5231
     *
5232
     * @param int $attemptId
5233
     * @param int $questionId
5234
     * @param int $userId
5235
     *
5236
     * @return string
5237
     */
5238
    public static function getOralFeedbackAudio($attemptId, $questionId, $userId)
5239
    {
5240
        $courseInfo = api_get_course_info();
5241
        $sessionId = api_get_session_id();
5242
        $groupId = api_get_group_id();
5243
        $sysCourseDir = api_get_path(SYS_COURSE_PATH).$courseInfo['path'];
5244
        $webCourseDir = api_get_path(WEB_COURSE_PATH).$courseInfo['path'];
5245
        $fileName = "{$questionId}_{$userId}".DocumentManager::getDocumentSuffix($courseInfo, $sessionId, $groupId);
5246
        $filePath = null;
5247
5248
        $relFilePath = "/exercises/teacher_audio/$attemptId/$fileName";
5249
5250
        if (file_exists($sysCourseDir.$relFilePath.'.ogg')) {
5251
            $filePath = $webCourseDir.$relFilePath.'.ogg';
5252
        } elseif (file_exists($sysCourseDir.$relFilePath.'.wav.wav')) {
5253
            $filePath = $webCourseDir.$relFilePath.'.wav.wav';
5254
        } elseif (file_exists($sysCourseDir.$relFilePath.'.wav')) {
5255
            $filePath = $webCourseDir.$relFilePath.'.wav';
5256
        }
5257
5258
        if (!$filePath) {
5259
            return '';
5260
        }
5261
5262
        return Display::tag(
5263
            'audio',
5264
            null,
5265
            ['src' => $filePath]
5266
        );
5267
    }
5268
5269
    /**
5270
     * @return array
5271
     */
5272
    public static function getNotificationSettings()
5273
    {
5274
        $emailAlerts = [
5275
            2 => get_lang('SendEmailToTeacherWhenStudentStartQuiz'),
5276
            1 => get_lang('SendEmailToTeacherWhenStudentEndQuiz'), // default
5277
            3 => get_lang('SendEmailToTeacherWhenStudentEndQuizOnlyIfOpenQuestion'),
5278
            4 => get_lang('SendEmailToTeacherWhenStudentEndQuizOnlyIfOralQuestion'),
5279
        ];
5280
5281
        return $emailAlerts;
5282
    }
5283
5284
    /**
5285
     * Get the additional actions added in exercise_additional_teacher_modify_actions configuration.
5286
     *
5287
     * @param int $exerciseId
5288
     * @param int $iconSize
5289
     *
5290
     * @return string
5291
     */
5292
    public static function getAdditionalTeacherActions($exerciseId, $iconSize = ICON_SIZE_SMALL)
5293
    {
5294
        $additionalActions = api_get_configuration_value('exercise_additional_teacher_modify_actions') ?: [];
5295
        $actions = [];
5296
5297
        foreach ($additionalActions as $additionalAction) {
5298
            $actions[] = call_user_func(
5299
                $additionalAction,
5300
                $exerciseId,
5301
                $iconSize
5302
            );
5303
        }
5304
5305
        return implode(PHP_EOL, $actions);
5306
    }
5307
5308
    /**
5309
     * @param DateTime $time
5310
     * @param int      $userId
5311
     * @param int      $courseId
5312
     * @param int      $sessionId
5313
     *
5314
     * @throws \Doctrine\ORM\Query\QueryException
5315
     *
5316
     * @return int
5317
     */
5318
    public static function countAnsweredQuestionsByUserAfterTime(DateTime $time, $userId, $courseId, $sessionId)
5319
    {
5320
        $em = Database::getManager();
5321
5322
        $time = api_get_utc_datetime($time->format('Y-m-d H:i:s'), false, true);
5323
5324
        $result = $em
5325
            ->createQuery('
5326
                SELECT COUNT(ea) FROM ChamiloCoreBundle:TrackEAttempt ea
5327
                WHERE ea.userId = :user AND ea.cId = :course AND ea.sessionId = :session
5328
                    AND ea.tms > :time
5329
            ')
5330
            ->setParameters(['user' => $userId, 'course' => $courseId, 'session' => $sessionId, 'time' => $time])
5331
            ->getSingleScalarResult();
5332
5333
        return $result;
5334
    }
5335
5336
    /**
5337
     * @param int $userId
5338
     * @param int $numberOfQuestions
5339
     * @param int $courseId
5340
     * @param int $sessionId
5341
     *
5342
     * @throws \Doctrine\ORM\Query\QueryException
5343
     *
5344
     * @return bool
5345
     */
5346
    public static function isQuestionsLimitPerDayReached($userId, $numberOfQuestions, $courseId, $sessionId)
5347
    {
5348
        $questionsLimitPerDay = (int) api_get_course_setting('quiz_question_limit_per_day');
5349
5350
        if ($questionsLimitPerDay <= 0) {
5351
            return false;
5352
        }
5353
5354
        $midnightTime = ChamiloApi::getServerMidnightTime();
5355
5356
        $answeredQuestionsCount = self::countAnsweredQuestionsByUserAfterTime(
5357
            $midnightTime,
5358
            $userId,
5359
            $courseId,
5360
            $sessionId
5361
        );
5362
5363
        return ($answeredQuestionsCount + $numberOfQuestions) > $questionsLimitPerDay;
5364
    }
5365
5366
    /**
5367
     * Check if an exercise complies with the requirements to be embedded in the mobile app or a video.
5368
     * By making sure it is set on one question per page and it only contains unique-answer or multiple-answer questions
5369
     * or unique-answer image. And that the exam does not have immediate feedback.
5370
     *
5371
     * @param array $exercise Exercise info
5372
     *
5373
     * @throws \Doctrine\ORM\Query\QueryException
5374
     *
5375
     * @return bool
5376
     */
5377
    public static function isQuizEmbeddable(array $exercise)
5378
    {
5379
        $em = Database::getManager();
5380
5381
        if (ONE_PER_PAGE != $exercise['type'] || EXERCISE_FEEDBACK_TYPE_DIRECT == $exercise['feedback_type']) {
5382
            return false;
5383
        }
5384
5385
        $countAll = $em
5386
            ->createQuery('SELECT COUNT(qq)
5387
                FROM ChamiloCourseBundle:CQuizQuestion qq
5388
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5389
                   WITH qq.iid = qrq.questionId
5390
                WHERE qrq.exerciceId = :id'
5391
            )
5392
            ->setParameter('id', $exercise['iid'])
5393
            ->getSingleScalarResult();
5394
5395
        $countOfAllowed = $em
5396
            ->createQuery('SELECT COUNT(qq)
5397
                FROM ChamiloCourseBundle:CQuizQuestion qq
5398
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5399
                   WITH qq.iid = qrq.questionId
5400
                WHERE qrq.exerciceId = :id AND qq.type IN (:types)'
5401
            )
5402
            ->setParameters(
5403
                [
5404
                    'id' => $exercise['iid'],
5405
                    'types' => [UNIQUE_ANSWER, MULTIPLE_ANSWER, UNIQUE_ANSWER_IMAGE],
5406
                ]
5407
            )
5408
            ->getSingleScalarResult();
5409
5410
        return $countAll === $countOfAllowed;
5411
    }
5412
5413
    /**
5414
     * Generate a certificate linked to current quiz and.
5415
     * Return the HTML block with links to download and view the certificate.
5416
     *
5417
     * @param float    $totalScore
5418
     * @param float    $totalWeight
5419
     * @param Exercise $objExercise
5420
     * @param int      $studentId
5421
     * @param string   $courseCode
5422
     * @param int      $sessionId
5423
     *
5424
     * @return string
5425
     */
5426
    public static function generateAndShowCertificateBlock(
5427
        $totalScore,
5428
        $totalWeight,
5429
        Exercise $objExercise,
5430
        $studentId,
5431
        $courseCode,
5432
        $sessionId = 0
5433
    ) {
5434
        if (!api_get_configuration_value('quiz_generate_certificate_ending') ||
5435
            !self::isSuccessExerciseResult($totalScore, $totalWeight, $objExercise->selectPassPercentage())
5436
        ) {
5437
            return '';
5438
        }
5439
5440
        /** @var Category $category */
5441
        $category = Category::load(null, null, $courseCode, null, null, $sessionId, 'ORDER By id');
5442
5443
        if (empty($category)) {
5444
            return '';
5445
        }
5446
5447
        /** @var Category $category */
5448
        $category = $category[0];
5449
        $categoryId = $category->get_id();
5450
        $link = LinkFactory::load(
5451
            null,
5452
            null,
5453
            $objExercise->selectId(),
5454
            null,
5455
            $courseCode,
5456
            $categoryId
5457
        );
5458
5459
        if (empty($link)) {
5460
            return '';
5461
        }
5462
5463
        $resourceDeletedMessage = $category->show_message_resource_delete($courseCode);
5464
5465
        if (false !== $resourceDeletedMessage || api_is_allowed_to_edit() || api_is_excluded_user_type()) {
5466
            return '';
5467
        }
5468
5469
        $certificate = Category::generateUserCertificate($categoryId, $studentId);
5470
5471
        if (!is_array($certificate)) {
5472
            return '';
5473
        }
5474
5475
        return Category::getDownloadCertificateBlock($certificate);
5476
    }
5477
}
5478