Completed
Push — master ( 5f79f5...efcbbc )
by Julito
11:10
created

ExerciseLib::get_exercise_result_ranking_by_attempt()   C

Complexity

Conditions 15

Size

Total Lines 60
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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