Passed
Push — 1.11.x ( ad358e...0b4b80 )
by Julito
10:37 queued 12s
created

ExerciseLib::isPassPercentageEnabled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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