Passed
Push — ofaj ( d9b422...0f9380 )
by
unknown
11:25 queued 11s
created

ExerciseLib::show_score()   F

Complexity

Conditions 14
Paths 721

Size

Total Lines 91
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

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

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