Passed
Push — master ( 15378b...a2dd4f )
by Julito
09:24
created

ExerciseLib::addScoreModelInput()   A

Complexity

Conditions 4

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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