Passed
Push — master ( 292add...d81919 )
by Julito
11:50
created

get_exercise_result_ranking_by_attempt()   C

Complexity

Conditions 15

Size

Total Lines 60
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 37
nop 6
dl 0
loc 60
rs 5.9166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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