Passed
Push — 1.11.x ( d172cc...40332a )
by Julito
11:21
created

ExerciseLib::displayQuestionListByAttempt()   F

Complexity

Conditions 85
Paths 5

Size

Total Lines 556
Code Lines 361

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 85
eloc 361
c 3
b 0
f 0
nc 5
nop 4
dl 0
loc 556
rs 3.3333

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

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

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

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

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

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

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

Loading history...
4174
                case MATCHING:
4175
                case MATCHING_DRAGGABLE:
4176
                default:
4177
                    $return = Database::num_rows($result);
4178
            }
4179
        }
4180
4181
        return $return;
4182
    }
4183
4184
    /**
4185
     * @param array  $answer
4186
     * @param string $user_answer
4187
     *
4188
     * @return array
4189
     */
4190
    public static function check_fill_in_blanks($answer, $user_answer, $current_answer)
4191
    {
4192
        // the question is encoded like this
4193
        // [A] B [C] D [E] F::10,10,10@1
4194
        // number 1 before the "@" means that is a switchable fill in blank question
4195
        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4196
        // means that is a normal fill blank question
4197
        // first we explode the "::"
4198
        $pre_array = explode('::', $answer);
4199
        // is switchable fill blank or not
4200
        $last = count($pre_array) - 1;
4201
        $is_set_switchable = explode('@', $pre_array[$last]);
4202
        $switchable_answer_set = false;
4203
        if (isset($is_set_switchable[1]) && $is_set_switchable[1] == 1) {
4204
            $switchable_answer_set = true;
4205
        }
4206
        $answer = '';
4207
        for ($k = 0; $k < $last; $k++) {
4208
            $answer .= $pre_array[$k];
4209
        }
4210
        // splits weightings that are joined with a comma
4211
        $answerWeighting = explode(',', $is_set_switchable[0]);
4212
4213
        // we save the answer because it will be modified
4214
        //$temp = $answer;
4215
        $temp = $answer;
4216
4217
        $answer = '';
4218
        $j = 0;
4219
        //initialise answer tags
4220
        $user_tags = $correct_tags = $real_text = [];
4221
        // the loop will stop at the end of the text
4222
        while (1) {
4223
            // quits the loop if there are no more blanks (detect '[')
4224
            if (($pos = api_strpos($temp, '[')) === false) {
4225
                // adds the end of the text
4226
                $answer = $temp;
4227
                $real_text[] = $answer;
4228
                break; //no more "blanks", quit the loop
4229
            }
4230
            // adds the piece of text that is before the blank
4231
            //and ends with '[' into a general storage array
4232
            $real_text[] = api_substr($temp, 0, $pos + 1);
4233
            $answer .= api_substr($temp, 0, $pos + 1);
4234
            //take the string remaining (after the last "[" we found)
4235
            $temp = api_substr($temp, $pos + 1);
4236
            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4237
            if (($pos = api_strpos($temp, ']')) === false) {
4238
                // adds the end of the text
4239
                $answer .= $temp;
4240
                break;
4241
            }
4242
4243
            $str = $user_answer;
4244
4245
            preg_match_all('#\[([^[]*)\]#', $str, $arr);
4246
            $str = str_replace('\r\n', '', $str);
4247
            $choices = $arr[1];
4248
            $choice = [];
4249
            $check = false;
4250
            $i = 0;
4251
            foreach ($choices as $item) {
4252
                if ($current_answer === $item) {
4253
                    $check = true;
4254
                }
4255
                if ($check) {
4256
                    $choice[] = $item;
4257
                    $i++;
4258
                }
4259
                if ($i == 3) {
4260
                    break;
4261
                }
4262
            }
4263
            $tmp = api_strrpos($choice[$j], ' / ');
4264
4265
            if ($tmp !== false) {
4266
                $choice[$j] = api_substr($choice[$j], 0, $tmp);
4267
            }
4268
4269
            $choice[$j] = trim($choice[$j]);
4270
4271
            //Needed to let characters ' and " to work as part of an answer
4272
            $choice[$j] = stripslashes($choice[$j]);
4273
4274
            $user_tags[] = api_strtolower($choice[$j]);
4275
            //put the contents of the [] answer tag into correct_tags[]
4276
            $correct_tags[] = api_strtolower(api_substr($temp, 0, $pos));
4277
            $j++;
4278
            $temp = api_substr($temp, $pos + 1);
4279
        }
4280
4281
        $answer = '';
4282
        $real_correct_tags = $correct_tags;
4283
        $chosen_list = [];
4284
        $good_answer = [];
4285
4286
        for ($i = 0; $i < count($real_correct_tags); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

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

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

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