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

ExerciseLib::get_number_students_answer_count()   C

Complexity

Conditions 12

Size

Total Lines 93
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 59
nop 8
dl 0
loc 93
rs 6.4678
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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