Exercise::getCorrectAnswersInAllAttempts()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\GradebookLink;
6
use Chamilo\CoreBundle\Entity\TrackEExercise;
7
use Chamilo\CoreBundle\Entity\TrackEExerciseConfirmation;
8
use Chamilo\CoreBundle\Entity\TrackEHotspot;
9
use Chamilo\CoreBundle\Enums\ActionIcon;
10
use Chamilo\CoreBundle\Enums\StateIcon;
11
use Chamilo\CoreBundle\Enums\ToolIcon;
12
use Chamilo\CoreBundle\Framework\Container;
13
use Chamilo\CoreBundle\Repository\ResourceLinkRepository;
14
use Chamilo\CoreBundle\Repository\TrackEDefaultRepository;
15
use Chamilo\CoreBundle\Helpers\ChamiloHelper;
16
use Chamilo\CourseBundle\Entity\CQuiz;
17
use Chamilo\CourseBundle\Entity\CQuizCategory;
18
use Chamilo\CourseBundle\Entity\CQuizRelQuestionCategory;
19
use ChamiloSession as Session;
20
21
22
/**
23
 * @author Olivier Brouckaert
24
 * @author Julio Montoya Cleaning exercises
25
 * @author Hubert Borderiou #294
26
 */
27
class Exercise
28
{
29
    public const PAGINATION_ITEMS_PER_PAGE = 20;
30
    public $iId;
31
    public $id;
32
    public $name;
33
    public $title;
34
    public $exercise;
35
    public $description;
36
    public $sound;
37
    public $type; //ALL_ON_ONE_PAGE or ONE_PER_PAGE
38
    public $random;
39
    public $random_answers;
40
    public $active;
41
    public $timeLimit;
42
    public $attempts;
43
    public $feedback_type;
44
    public $end_time;
45
    public $start_time;
46
    public $questionList; // array with the list of this exercise's questions
47
    /* including question list of the media */
48
    public $questionListUncompressed;
49
    public $results_disabled;
50
    public $expired_time;
51
    public $course;
52
    public $course_id;
53
    public $propagate_neg;
54
    public $saveCorrectAnswers;
55
    public $review_answers;
56
    public $randomByCat;
57
    public $text_when_finished;
58
    public $text_when_finished_failure;
59
    public $display_category_name;
60
    public $pass_percentage;
61
    public $edit_exercise_in_lp = false;
62
    public $is_gradebook_locked = false;
63
    public $exercise_was_added_in_lp = false;
64
    public $lpList = [];
65
    public $force_edit_exercise_in_lp = false;
66
    public $categories;
67
    public $categories_grouping = true;
68
    public $endButton = 0;
69
    public $categoryWithQuestionList;
70
    public $mediaList;
71
    public $loadQuestionAJAX = false;
72
    // Notification send to the teacher.
73
    public $emailNotificationTemplate = null;
74
    // Notification send to the student.
75
    public $emailNotificationTemplateToUser = null;
76
    public $countQuestions = 0;
77
    public $fastEdition = false;
78
    public $modelType = 1;
79
    public $questionSelectionType = EX_Q_SELECTION_ORDERED;
80
    public $hideQuestionTitle = 0;
81
    public $scoreTypeModel = 0;
82
    public $categoryMinusOne = true; // Shows the category -1: See BT#6540
83
    public $globalCategoryId = null;
84
    public $onSuccessMessage = null;
85
    public $onFailedMessage = null;
86
    public $emailAlert;
87
    public $notifyUserByEmail = '';
88
    public $sessionId = 0;
89
    public $questionFeedbackEnabled = false;
90
    public $questionTypeWithFeedback;
91
    public $showPreviousButton;
92
    public $notifications;
93
    public $export = false;
94
    public $autolaunch;
95
    public $quizCategoryId;
96
    public $pageResultConfiguration;
97
    public $hideQuestionNumber;
98
    public $preventBackwards;
99
    public $currentQuestion;
100
    public $hideComment;
101
    public $hideNoAnswer;
102
    public $hideExpectedAnswer;
103
    public $forceShowExpectedChoiceColumn;
104
    public $disableHideCorrectAnsweredQuestions;
105
106
    /**
107
     * @param int $courseId
108
     */
109
    public function __construct($courseId = 0)
110
    {
111
        $this->iId = 0;
112
        $this->id = 0;
113
        $this->exercise = '';
114
        $this->description = '';
115
        $this->sound = '';
116
        $this->type = ALL_ON_ONE_PAGE;
117
        $this->random = 0;
118
        $this->random_answers = 0;
119
        $this->active = 1;
120
        $this->questionList = [];
121
        $this->timeLimit = 0;
122
        $this->end_time = '';
123
        $this->start_time = '';
124
        $this->results_disabled = 1;
125
        $this->expired_time = 0;
126
        $this->propagate_neg = 0;
127
        $this->saveCorrectAnswers = 0;
128
        $this->review_answers = false;
129
        $this->randomByCat = 0;
130
        $this->text_when_finished = '';
131
        $this->text_when_finished_failure = '';
132
        $this->display_category_name = 0;
133
        $this->pass_percentage = 0;
134
        $this->modelType = 1;
135
        $this->questionSelectionType = EX_Q_SELECTION_ORDERED;
136
        $this->endButton = 0;
137
        $this->scoreTypeModel = 0;
138
        $this->globalCategoryId = null;
139
        $this->notifications = [];
140
        $this->quizCategoryId = 0;
141
        $this->pageResultConfiguration = null;
142
        $this->hideQuestionNumber = 0;
143
        $this->preventBackwards = 0;
144
        $this->hideComment = false;
145
        $this->hideNoAnswer = false;
146
        $this->hideExpectedAnswer = false;
147
        $this->disableHideCorrectAnsweredQuestions = false;
148
149
        if (!empty($courseId)) {
150
            $courseInfo = api_get_course_info_by_id($courseId);
151
        } else {
152
            $courseInfo = api_get_course_info();
153
        }
154
        $this->course_id = $courseInfo['real_id'];
155
        $this->course = $courseInfo;
156
        $this->sessionId = api_get_session_id();
157
158
        // ALTER TABLE c_quiz_question ADD COLUMN feedback text;
159
        $this->questionFeedbackEnabled = ('true' === api_get_setting('exercise.allow_quiz_question_feedback'));
160
        $this->showPreviousButton = true;
161
    }
162
163
    /**
164
     * Reads exercise information from the data base.
165
     *
166
     * @param int  $id                - exercise Id
167
     * @param bool $parseQuestionList
168
     *
169
     * @return bool - true if exercise exists, otherwise false
170
     */
171
    public function read($id, $parseQuestionList = true)
172
    {
173
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
174
175
        $id = (int) $id;
176
        if (empty($this->course_id) || empty($id)) {
177
            return false;
178
        }
179
180
        $sql = "SELECT * FROM $table
181
                WHERE iid = $id";
182
        $result = Database::query($sql);
183
184
        // if the exercise has been found
185
        if ($object = Database::fetch_object($result)) {
186
            $this->id = $this->iId = (int) $object->iid;
187
            $this->exercise = $object->title;
188
            $this->name = $object->title;
189
            $this->title = $object->title;
190
            $this->description = $object->description;
191
            $this->sound = $object->sound;
192
            $this->type = $object->type;
193
            if (empty($this->type)) {
194
                $this->type = ONE_PER_PAGE;
195
            }
196
            $this->random = $object->random;
197
            $this->random_answers = $object->random_answers;
198
            $this->active = $object->active;
199
            $this->results_disabled = $object->results_disabled;
200
            $this->attempts = $object->max_attempt;
201
            $this->feedback_type = $object->feedback_type;
202
            //$this->sessionId = $object->session_id;
203
            $this->propagate_neg = $object->propagate_neg;
204
            $this->saveCorrectAnswers = $object->save_correct_answers;
205
            $this->randomByCat = $object->random_by_category;
206
            $this->text_when_finished = $object->text_when_finished;
207
            $this->text_when_finished_failure = $object->text_when_finished_failure;
208
            $this->display_category_name = $object->display_category_name;
209
            $this->pass_percentage = $object->pass_percentage;
210
            $this->is_gradebook_locked = api_resource_is_locked_by_gradebook($id, LINK_EXERCISE);
211
            $this->review_answers = isset($object->review_answers) && 1 == $object->review_answers ? true : false;
212
            $this->globalCategoryId = isset($object->global_category_id) ? $object->global_category_id : null;
213
            $this->questionSelectionType = isset($object->question_selection_type) ? (int) $object->question_selection_type : null;
214
            $this->hideQuestionTitle = isset($object->hide_question_title) ? (int) $object->hide_question_title : 0;
215
            $this->autolaunch = isset($object->autolaunch) ? (int) $object->autolaunch : 0;
216
            $this->quizCategoryId = isset($object->quiz_category_id) ? (int) $object->quiz_category_id : null;
217
            $this->preventBackwards = isset($object->prevent_backwards) ? (int) $object->prevent_backwards : 0;
218
            $this->exercise_was_added_in_lp = false;
219
            $this->lpList = [];
220
            $this->notifications = [];
221
            if (!empty($object->notifications)) {
222
                $this->notifications = explode(',', $object->notifications);
223
            }
224
225
            if (!empty($object->page_result_configuration)) {
226
                //$this->pageResultConfiguration = $object->page_result_configuration;
227
            }
228
229
            $this->hideQuestionNumber = 1 == $object->hide_question_number;
230
231
            if (isset($object->show_previous_button)) {
232
                $this->showPreviousButton = 1 == $object->show_previous_button ? true : false;
233
            }
234
235
            $list = self::getLpListFromExercise($id, $this->course_id);
236
            if (!empty($list)) {
237
                $this->exercise_was_added_in_lp = true;
238
                $this->lpList = $list;
239
            }
240
241
            $this->force_edit_exercise_in_lp = api_get_setting('lp.force_edit_exercise_in_lp');
242
            $this->edit_exercise_in_lp = true;
243
            if ($this->exercise_was_added_in_lp) {
244
                $this->edit_exercise_in_lp = true == $this->force_edit_exercise_in_lp;
245
            }
246
247
            if (!empty($object->end_time)) {
248
                $this->end_time = $object->end_time;
249
            }
250
            if (!empty($object->start_time)) {
251
                $this->start_time = $object->start_time;
252
            }
253
254
            // Control time
255
            $this->expired_time = $object->expired_time;
256
257
            // Checking if question_order is correctly set
258
            if ($parseQuestionList) {
259
                $this->setQuestionList(true);
260
            }
261
262
            //overload questions list with recorded questions list
263
            //load questions only for exercises of type 'one question per page'
264
            //this is needed only is there is no questions
265
266
            // @todo not sure were in the code this is used somebody mess with the exercise tool
267
            // @todo don't know who add that config and why $_configuration['live_exercise_tracking']
268
            /*global $_configuration, $questionList;
269
            if ($this->type == ONE_PER_PAGE && $_SERVER['REQUEST_METHOD'] != 'POST'
270
                && defined('QUESTION_LIST_ALREADY_LOGGED') &&
271
                isset($_configuration['live_exercise_tracking']) && $_configuration['live_exercise_tracking']
272
            ) {
273
                $this->questionList = $questionList;
274
            }*/
275
276
            return true;
277
        }
278
279
        return false;
280
    }
281
282
    public function getCutTitle(): string
283
    {
284
        $title = $this->getUnformattedTitle();
285
286
        return cut($title, EXERCISE_MAX_NAME_SIZE);
287
    }
288
289
    public function getId()
290
    {
291
        return (int) $this->iId;
292
    }
293
294
    /**
295
     * returns the exercise title.
296
     *
297
     * @param bool $unformattedText Optional. Get the title without HTML tags
298
     *
299
     * @return string - exercise title
300
     */
301
    public function selectTitle($unformattedText = false)
302
    {
303
        if ($unformattedText) {
304
            return $this->getUnformattedTitle();
305
        }
306
307
        return $this->exercise;
308
    }
309
310
    /**
311
     * returns the number of attempts setted.
312
     *
313
     * @return int - exercise attempts
314
     */
315
    public function selectAttempts()
316
    {
317
        return $this->attempts;
318
    }
319
320
    /**
321
     * Returns the number of FeedbackType
322
     *  0: Feedback , 1: DirectFeedback, 2: NoFeedback.
323
     *
324
     * @return int - exercise attempts
325
     */
326
    public function getFeedbackType()
327
    {
328
        return (int) $this->feedback_type;
329
    }
330
331
    /**
332
     * returns the time limit.
333
     *
334
     * @return int
335
     */
336
    public function selectTimeLimit()
337
    {
338
        return $this->timeLimit;
339
    }
340
341
    /**
342
     * returns the exercise description.
343
     *
344
     * @return string - exercise description
345
     */
346
    public function selectDescription()
347
    {
348
        return $this->description;
349
    }
350
351
    /**
352
     * returns the exercise sound file.
353
     */
354
    public function getSound()
355
    {
356
        return $this->sound;
357
    }
358
359
    /**
360
     * returns the exercise type.
361
     *
362
     * @return int - exercise type
363
     *
364
     * @author Olivier Brouckaert
365
     */
366
    public function selectType()
367
    {
368
        return $this->type;
369
    }
370
371
    /**
372
     * @return int
373
     */
374
    public function getModelType()
375
    {
376
        return $this->modelType;
377
    }
378
379
    /**
380
     * @return int
381
     */
382
    public function selectEndButton()
383
    {
384
        return $this->endButton;
385
    }
386
387
    /**
388
     * @return int : do we display the question category name for students
389
     *
390
     * @author hubert borderiou 30-11-11
391
     */
392
    public function selectDisplayCategoryName()
393
    {
394
        return $this->display_category_name;
395
    }
396
397
    /**
398
     * @return int
399
     */
400
    public function selectPassPercentage()
401
    {
402
        return $this->pass_percentage;
403
    }
404
405
    /**
406
     * Modify object to update the switch display_category_name.
407
     *
408
     * @param int $value is an integer 0 or 1
409
     *
410
     * @author hubert borderiou 30-11-11
411
     */
412
    public function updateDisplayCategoryName($value)
413
    {
414
        $this->display_category_name = $value;
415
    }
416
417
    /**
418
     * @return string html text : the text to display ay the end of the test
419
     *
420
     * @author hubert borderiou 28-11-11
421
     */
422
    public function getTextWhenFinished(): string
423
    {
424
        return $this->text_when_finished;
425
    }
426
427
    /**
428
     * @param string $text
429
     *
430
     * @author hubert borderiou 28-11-11
431
     */
432
    public function setTextWhenFinished(string $text): void
433
    {
434
        $this->text_when_finished = $text;
435
    }
436
437
    /**
438
     * Get the text to display when the user has failed the test
439
     * @return string html text : the text to display ay the end of the test
440
     */
441
    public function getTextWhenFinishedFailure(): string
442
    {
443
        if (empty($this->text_when_finished_failure)) {
444
            return '';
445
        }
446
447
        return $this->text_when_finished_failure;
448
    }
449
450
    /**
451
     * Set the text to display when the user has succeeded in the test
452
     * @param string $text
453
     */
454
    public function setTextWhenFinishedFailure(string $text): void
455
    {
456
        $this->text_when_finished_failure = $text;
457
    }
458
459
    /**
460
     * return 1 or 2 if randomByCat.
461
     *
462
     * @return int - quiz random by category
463
     *
464
     * @author hubert borderiou
465
     */
466
    public function getRandomByCategory()
467
    {
468
        return $this->randomByCat;
469
    }
470
471
    /**
472
     * return 0 if no random by cat
473
     * return 1 if random by cat, categories shuffled
474
     * return 2 if random by cat, categories sorted by alphabetic order.
475
     *
476
     * @return int - quiz random by category
477
     *
478
     * @author hubert borderiou
479
     */
480
    public function isRandomByCat()
481
    {
482
        $res = EXERCISE_CATEGORY_RANDOM_DISABLED;
483
        if (EXERCISE_CATEGORY_RANDOM_SHUFFLED == $this->randomByCat) {
484
            $res = EXERCISE_CATEGORY_RANDOM_SHUFFLED;
485
        } elseif (EXERCISE_CATEGORY_RANDOM_ORDERED == $this->randomByCat) {
486
            $res = EXERCISE_CATEGORY_RANDOM_ORDERED;
487
        }
488
489
        return $res;
490
    }
491
492
    /**
493
     * return nothing
494
     * update randomByCat value for object.
495
     *
496
     * @param int $random
497
     *
498
     * @author hubert borderiou
499
     */
500
    public function updateRandomByCat($random)
501
    {
502
        $this->randomByCat = EXERCISE_CATEGORY_RANDOM_DISABLED;
503
        if (in_array(
504
            $random,
505
            [
506
                EXERCISE_CATEGORY_RANDOM_SHUFFLED,
507
                EXERCISE_CATEGORY_RANDOM_ORDERED,
508
                EXERCISE_CATEGORY_RANDOM_DISABLED,
509
            ]
510
        )) {
511
            $this->randomByCat = $random;
512
        }
513
    }
514
515
    /**
516
     * Tells if questions are selected randomly, and if so returns the draws.
517
     *
518
     * @return int - results disabled exercise
519
     *
520
     * @author Carlos Vargas
521
     */
522
    public function selectResultsDisabled()
523
    {
524
        return $this->results_disabled;
525
    }
526
527
    /**
528
     * tells if questions are selected randomly, and if so returns the draws.
529
     *
530
     * @return bool
531
     *
532
     * @author Olivier Brouckaert
533
     */
534
    public function isRandom()
535
    {
536
        $isRandom = false;
537
        // "-1" means all questions will be random
538
        if ($this->random > 0 || -1 == $this->random) {
539
            $isRandom = true;
540
        }
541
542
        return $isRandom;
543
    }
544
545
    /**
546
     * returns random answers status.
547
     *
548
     * @author Juan Carlos Rana
549
     */
550
    public function getRandomAnswers()
551
    {
552
        return $this->random_answers;
553
    }
554
555
    /**
556
     * Same as isRandom() but has a name applied to values different than 0 or 1.
557
     *
558
     * @return int
559
     */
560
    public function getShuffle()
561
    {
562
        return $this->random;
563
    }
564
565
    /**
566
     * returns the exercise status (1 = enabled ; 0 = disabled).
567
     *
568
     * @return int - 1 if enabled, otherwise 0
569
     *
570
     * @author Olivier Brouckaert
571
     */
572
    public function selectStatus()
573
    {
574
        return $this->active;
575
    }
576
577
    /**
578
     * If false the question list will be managed as always if true
579
     * the question will be filtered
580
     * depending of the exercise settings (table c_quiz_rel_category).
581
     *
582
     * @param bool $status active or inactive grouping
583
     */
584
    public function setCategoriesGrouping($status)
585
    {
586
        $this->categories_grouping = (bool) $status;
587
    }
588
589
    /**
590
     * @return int
591
     */
592
    public function getHideQuestionTitle()
593
    {
594
        return $this->hideQuestionTitle;
595
    }
596
597
    /**
598
     * @param $value
599
     */
600
    public function setHideQuestionTitle($value)
601
    {
602
        $this->hideQuestionTitle = (int) $value;
603
    }
604
605
    /**
606
     * @return int
607
     */
608
    public function getScoreTypeModel()
609
    {
610
        return $this->scoreTypeModel;
611
    }
612
613
    /**
614
     * @param int $value
615
     */
616
    public function setScoreTypeModel($value)
617
    {
618
        $this->scoreTypeModel = (int) $value;
619
    }
620
621
    /**
622
     * @return int
623
     */
624
    public function getGlobalCategoryId()
625
    {
626
        return $this->globalCategoryId;
627
    }
628
629
    /**
630
     * @param int $value
631
     */
632
    public function setGlobalCategoryId($value)
633
    {
634
        if (is_array($value) && isset($value[0])) {
635
            $value = $value[0];
636
        }
637
        $this->globalCategoryId = (int) $value;
638
    }
639
640
    /**
641
     * @param int    $start
642
     * @param int    $limit
643
     * @param string $sidx
644
     * @param string $sord
645
     * @param array  $whereCondition
646
     * @param array  $extraFields
647
     *
648
     * @return array
649
     */
650
    public function getQuestionListPagination(
651
        $start,
652
        $limit,
653
        $sidx,
654
        $sord,
655
        $whereCondition = [],
656
        $extraFields = []
657
    ) {
658
        if (!empty($this->id)) {
659
            $category_list = TestCategory::getListOfCategoriesNameForTest(
660
                $this->id,
661
                false
662
            );
663
            $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
664
            $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
665
666
            $sql = "SELECT q.iid
667
                    FROM $TBL_EXERCICE_QUESTION e
668
                    INNER JOIN $TBL_QUESTIONS  q
669
                    ON (e.question_id = q.iid)
670
					WHERE e.quiz_id	= '".$this->id."' ";
671
672
            $orderCondition = ' ORDER BY question_order ';
673
674
            if (!empty($sidx) && !empty($sord)) {
675
                if ('question' === $sidx) {
676
                    if (in_array(strtolower($sord), ['desc', 'asc'])) {
677
                        $orderCondition = " ORDER BY `q.$sidx` $sord";
678
                    }
679
                }
680
            }
681
682
            $sql .= $orderCondition;
683
            $limitCondition = null;
684
            if (isset($start) && isset($limit)) {
685
                $start = (int) $start;
686
                $limit = (int) $limit;
687
                $limitCondition = " LIMIT $start, $limit";
688
            }
689
            $sql .= $limitCondition;
690
            $result = Database::query($sql);
691
            $questions = [];
692
            if (Database::num_rows($result)) {
693
                if (!empty($extraFields)) {
694
                    $extraFieldValue = new ExtraFieldValue('question');
695
                }
696
                while ($question = Database::fetch_assoc($result)) {
697
                    /** @var Question $objQuestionTmp */
698
                    $objQuestionTmp = Question::read($question['iid']);
699
                    $category_labels = '';
700
                    // @todo not implemented in 1.11.x
701
                    /*$category_labels = TestCategory::return_category_labels(
702
                        $objQuestionTmp->category_list,
703
                        $category_list
704
                    );*/
705
706
                    if (empty($category_labels)) {
707
                        $category_labels = '-';
708
                    }
709
710
                    // Question type
711
                    $typeImg = $objQuestionTmp->getTypePicture();
712
                    $typeExpl = $objQuestionTmp->getExplanation();
713
714
                    $question_media = null;
715
                    if (!empty($objQuestionTmp->parent_id)) {
716
                        // @todo not implemented in 1.11.x
717
                        //$objQuestionMedia = Question::read($objQuestionTmp->parent_id);
718
                        //$question_media = Question::getMediaLabel($objQuestionMedia->question);
719
                    }
720
721
                    $questionType = Display::tag(
722
                        'div',
723
                        Display::return_icon($typeImg, $typeExpl, [], ICON_SIZE_MEDIUM).$question_media
724
                    );
725
726
                    $question = [
727
                        'id' => $question['iid'],
728
                        'question' => $objQuestionTmp->selectTitle(),
729
                        'type' => $questionType,
730
                        'category' => Display::tag(
731
                            'div',
732
                            '<a href="#" style="padding:0px; margin:0px;">'.$category_labels.'</a>'
733
                        ),
734
                        'score' => $objQuestionTmp->selectWeighting(),
735
                        'level' => $objQuestionTmp->level,
736
                    ];
737
738
                    if (!empty($extraFields)) {
739
                        foreach ($extraFields as $extraField) {
740
                            $value = $extraFieldValue->get_values_by_handler_and_field_id(
741
                                $question['id'],
742
                                $extraField['id']
743
                            );
744
                            $stringValue = null;
745
                            if ($value) {
746
                                $stringValue = $value['field_value'];
747
                            }
748
                            $question[$extraField['field_variable']] = $stringValue;
749
                        }
750
                    }
751
                    $questions[] = $question;
752
                }
753
            }
754
755
            return $questions;
756
        }
757
    }
758
759
    /**
760
     * Get question count per exercise from DB (any special treatment).
761
     *
762
     * @return int
763
     */
764
    public function getQuestionCount()
765
    {
766
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
767
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
768
        $sql = "SELECT count(q.iid) as count
769
                FROM $TBL_EXERCICE_QUESTION e
770
                INNER JOIN $TBL_QUESTIONS q
771
                ON (e.question_id = q.iid)
772
                WHERE
773
                    e.quiz_id = ".$this->getId();
774
        $result = Database::query($sql);
775
776
        $count = 0;
777
        if (Database::num_rows($result)) {
778
            $row = Database::fetch_array($result);
779
            $count = (int) $row['count'];
780
        }
781
782
        return $count;
783
    }
784
785
    /**
786
     * @return array
787
     */
788
    public function getQuestionOrderedListByName()
789
    {
790
        if (empty($this->course_id) || empty($this->getId())) {
791
            return [];
792
        }
793
794
        $exerciseQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
795
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
796
797
        // Getting question list from the order (question list drag n drop interface ).
798
        $sql = "SELECT e.question_id
799
                FROM $exerciseQuestionTable e
800
                INNER JOIN $questionTable q
801
                ON (e.question_id= q.iid)
802
                WHERE
803
                    e.quiz_id = '".$this->getId()."'
804
                ORDER BY q.question";
805
        $result = Database::query($sql);
806
        $list = [];
807
        if (Database::num_rows($result)) {
808
            $list = Database::store_result($result, 'ASSOC');
809
        }
810
811
        return $list;
812
    }
813
814
    /**
815
     * Selecting question list depending in the exercise-category
816
     * relationship (category table in exercise settings).
817
     *
818
     * @param array $questionList
819
     * @param int   $questionSelectionType
820
     *
821
     * @return array
822
     */
823
    public function getQuestionListWithCategoryListFilteredByCategorySettings(
824
        $questionList,
825
        $questionSelectionType
826
    ) {
827
        $result = [
828
            'question_list' => [],
829
            'category_with_questions_list' => [],
830
        ];
831
832
        // Order/random categories
833
        $cat = new TestCategory();
834
835
        // Setting category order.
836
        switch ($questionSelectionType) {
837
            case EX_Q_SELECTION_ORDERED: // 1
838
            case EX_Q_SELECTION_RANDOM:  // 2
839
                // This options are not allowed here.
840
                break;
841
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED: // 3
842
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
843
                    $this,
844
                    'title ASC',
845
                    false,
846
                    true
847
                );
848
849
                $questionsByCategory = TestCategory::getQuestionsByCat(
850
                    $this->getId(),
851
                    $questionList,
852
                    $categoriesAddedInExercise
853
                );
854
855
                $questionList = $this->pickQuestionsPerCategory(
856
                    $categoriesAddedInExercise,
857
                    $questionList,
858
                    $questionsByCategory,
859
                    true,
860
                    false
861
                );
862
863
                break;
864
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED: // 4
865
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7
866
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
867
                    $this,
868
                    null,
869
                    true,
870
                    true
871
                );
872
                $questionsByCategory = TestCategory::getQuestionsByCat(
873
                    $this->getId(),
874
                    $questionList,
875
                    $categoriesAddedInExercise
876
                );
877
                $questionList = $this->pickQuestionsPerCategory(
878
                    $categoriesAddedInExercise,
879
                    $questionList,
880
                    $questionsByCategory,
881
                    true,
882
                    false
883
                );
884
885
                break;
886
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM: // 5
887
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
888
                    $this,
889
                    'title ASC',
890
                    false,
891
                    true
892
                );
893
                $questionsByCategory = TestCategory::getQuestionsByCat(
894
                    $this->getId(),
895
                    $questionList,
896
                    $categoriesAddedInExercise
897
                );
898
                $questionsByCategoryMandatory = [];
899
                if (EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM == $this->getQuestionSelectionType() &&
900
                    ('true' === api_get_setting('exercise.allow_mandatory_question_in_category'))
901
                ) {
902
                    $questionsByCategoryMandatory = TestCategory::getQuestionsByCat(
903
                        $this->id,
904
                        $questionList,
905
                        $categoriesAddedInExercise,
906
                        true
907
                    );
908
                }
909
                $questionList = $this->pickQuestionsPerCategory(
910
                    $categoriesAddedInExercise,
911
                    $questionList,
912
                    $questionsByCategory,
913
                    true,
914
                    true,
915
                    $questionsByCategoryMandatory
916
                );
917
918
                break;
919
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM: // 6
920
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED:
921
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
922
                    $this,
923
                    null,
924
                    true,
925
                    true
926
                );
927
928
                $questionsByCategory = TestCategory::getQuestionsByCat(
929
                    $this->getId(),
930
                    $questionList,
931
                    $categoriesAddedInExercise
932
                );
933
934
                $questionList = $this->pickQuestionsPerCategory(
935
                    $categoriesAddedInExercise,
936
                    $questionList,
937
                    $questionsByCategory,
938
                    true,
939
                    true
940
                );
941
942
                break;
943
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED: // 9
944
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
945
                    $this,
946
                    'root ASC, lft ASC',
947
                    false,
948
                    true
949
                );
950
                $questionsByCategory = TestCategory::getQuestionsByCat(
951
                    $this->getId(),
952
                    $questionList,
953
                    $categoriesAddedInExercise
954
                );
955
                $questionList = $this->pickQuestionsPerCategory(
956
                    $categoriesAddedInExercise,
957
                    $questionList,
958
                    $questionsByCategory,
959
                    true,
960
                    false
961
                );
962
963
                break;
964
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM: // 10
965
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
966
                    $this,
967
                    'root, lft ASC',
968
                    false,
969
                    true
970
                );
971
                $questionsByCategory = TestCategory::getQuestionsByCat(
972
                    $this->getId(),
973
                    $questionList,
974
                    $categoriesAddedInExercise
975
                );
976
                $questionList = $this->pickQuestionsPerCategory(
977
                    $categoriesAddedInExercise,
978
                    $questionList,
979
                    $questionsByCategory,
980
                    true,
981
                    true
982
                );
983
984
                break;
985
        }
986
987
        $result['question_list'] = $questionList ?? [];
988
        $result['category_with_questions_list'] = $questionsByCategory ?? [];
989
        $parentsLoaded = [];
990
        // Adding category info in the category list with question list:
991
        if (!empty($questionsByCategory)) {
992
            $newCategoryList = [];
993
            $em = Database::getManager();
994
            $repo = $em->getRepository(CQuizRelQuestionCategory::class);
995
996
            foreach ($questionsByCategory as $categoryId => $questionList) {
997
                $category = new TestCategory();
998
                $cat = (array) $category->getCategory($categoryId);
999
                if ($cat) {
1000
                    $cat['iid'] = $cat['id'];
1001
                }
1002
1003
                $categoryParentInfo = null;
1004
                // Parent is not set no loop here
1005
                if (isset($cat['parent_id']) && !empty($cat['parent_id'])) {
1006
                    /** @var CQuizRelQuestionCategory $categoryEntity */
1007
                    if (!isset($parentsLoaded[$cat['parent_id']])) {
1008
                        $categoryEntity = $em->find(CQuizRelQuestionCategory::class, $cat['parent_id']);
1009
                        $parentsLoaded[$cat['parent_id']] = $categoryEntity;
1010
                    } else {
1011
                        $categoryEntity = $parentsLoaded[$cat['parent_id']];
1012
                    }
1013
                    $path = $repo->getPath($categoryEntity);
1014
1015
                    $index = 0;
1016
                    if ($this->categoryMinusOne) {
1017
                        //$index = 1;
1018
                    }
1019
1020
                    /** @var CQuizRelQuestionCategory $categoryParent */
1021
                    // @todo not implemented in 1.11.x
1022
                    /*foreach ($path as $categoryParent) {
1023
                        $visibility = $categoryParent->getVisibility();
1024
                        if (0 == $visibility) {
1025
                            $categoryParentId = $categoryId;
1026
                            $categoryTitle = $cat['title'];
1027
                            if (count($path) > 1) {
1028
                                continue;
1029
                            }
1030
                        } else {
1031
                            $categoryParentId = $categoryParent->getIid();
1032
                            $categoryTitle = $categoryParent->getTitle();
1033
                        }
1034
1035
                        $categoryParentInfo['id'] = $categoryParentId;
1036
                        $categoryParentInfo['iid'] = $categoryParentId;
1037
                        $categoryParentInfo['parent_path'] = null;
1038
                        $categoryParentInfo['title'] = $categoryTitle;
1039
                        $categoryParentInfo['name'] = $categoryTitle;
1040
                        $categoryParentInfo['parent_id'] = null;
1041
1042
                        break;
1043
                    }*/
1044
                }
1045
                $cat['parent_info'] = $categoryParentInfo;
1046
                $newCategoryList[$categoryId] = [
1047
                    'category' => $cat,
1048
                    'question_list' => $questionList,
1049
                ];
1050
            }
1051
1052
            $result['category_with_questions_list'] = $newCategoryList;
1053
        }
1054
1055
        return $result;
1056
    }
1057
1058
    /**
1059
     * returns the array with the question ID list.
1060
     *
1061
     * @param bool $fromDatabase Whether the results should be fetched in the database or just from memory
1062
     * @param bool $adminView    Whether we should return all questions (admin view) or
1063
     *                           just a list limited by the max number of random questions
1064
     *
1065
     * @return array - question ID list
1066
     */
1067
    public function selectQuestionList($fromDatabase = false, $adminView = false)
1068
    {
1069
        //var_dump($this->getId());exit;
1070
        if ($fromDatabase && !empty($this->getId())) {
1071
            $nbQuestions = $this->getQuestionCount();
1072
1073
            $questionSelectionType = $this->getQuestionSelectionType();
1074
1075
            switch ($questionSelectionType) {
1076
                case EX_Q_SELECTION_ORDERED:
1077
                    $questionList = $this->getQuestionOrderedList($adminView);
1078
1079
                    break;
1080
                case EX_Q_SELECTION_RANDOM:
1081
                    // Not a random exercise, or if there are not at least 2 questions
1082
                    if (0 == $this->random || $nbQuestions < 2) {
1083
                        $questionList = $this->getQuestionOrderedList($adminView);
1084
                    } else {
1085
                        $questionList = $this->getRandomList($adminView);
1086
                    }
1087
1088
                    break;
1089
                default:
1090
                    $questionList = $this->getQuestionOrderedList($adminView);
1091
                    $result = $this->getQuestionListWithCategoryListFilteredByCategorySettings(
1092
                        $questionList,
1093
                        $questionSelectionType
1094
                    );
1095
                    $this->categoryWithQuestionList = $result['category_with_questions_list'];
1096
                    $questionList = $result['question_list'];
1097
1098
                    break;
1099
            }
1100
1101
            return $questionList;
1102
        }
1103
1104
        return $this->questionList;
1105
    }
1106
1107
    /**
1108
     * returns the number of questions in this exercise.
1109
     *
1110
     * @return int - number of questions
1111
     */
1112
    public function selectNbrQuestions()
1113
    {
1114
        return count($this->questionList);
1115
    }
1116
1117
    /**
1118
     * @return int
1119
     */
1120
    public function selectPropagateNeg()
1121
    {
1122
        return $this->propagate_neg;
1123
    }
1124
1125
    /**
1126
     * @return int
1127
     */
1128
    public function getSaveCorrectAnswers()
1129
    {
1130
        return $this->saveCorrectAnswers;
1131
    }
1132
1133
    /**
1134
     * Selects questions randomly in the question list.
1135
     *
1136
     * @param bool $adminView Whether we should return all
1137
     *                        questions (admin view) or just a list limited by the max number of random questions
1138
     *
1139
     * @return array - if the exercise is not set to take questions randomly, returns the question list
1140
     *               without randomizing, otherwise, returns the list with questions selected randomly
1141
     *
1142
     * @author Olivier Brouckaert
1143
     * @author Hubert Borderiou 15 nov 2011
1144
     */
1145
    public function getRandomList($adminView = false)
1146
    {
1147
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1148
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
1149
        $random = isset($this->random) && !empty($this->random) ? $this->random : 0;
1150
1151
        // Random with limit
1152
        $randomLimit = " ORDER BY RAND() LIMIT $random";
1153
1154
        // Random with no limit
1155
        if (-1 == $random) {
1156
            $randomLimit = ' ORDER BY RAND() ';
1157
        }
1158
1159
        // Admin see the list in default order
1160
        if (true === $adminView) {
1161
            // If viewing it as admin for edition, don't show it randomly, use title + id
1162
            $randomLimit = 'ORDER BY e.question_order';
1163
        }
1164
1165
        $sql = "SELECT e.question_id
1166
                FROM $quizRelQuestion e
1167
                INNER JOIN $question q
1168
                ON (e.question_id= q.iid)
1169
                WHERE
1170
                    e.quiz_id = '".$this->getId()."'
1171
                    $randomLimit ";
1172
        $result = Database::query($sql);
1173
        $questionList = [];
1174
        while ($row = Database::fetch_object($result)) {
1175
            $questionList[] = $row->question_id;
1176
        }
1177
1178
        return $questionList;
1179
    }
1180
1181
    /**
1182
     * returns 'true' if the question ID is in the question list.
1183
     *
1184
     * @param int $questionId - question ID
1185
     *
1186
     * @return bool - true if in the list, otherwise false
1187
     *
1188
     * @author Olivier Brouckaert
1189
     */
1190
    public function isInList($questionId)
1191
    {
1192
        $inList = false;
1193
        if (is_array($this->questionList)) {
1194
            $inList = in_array($questionId, $this->questionList);
1195
        }
1196
1197
        return $inList;
1198
    }
1199
1200
    /**
1201
     * If current exercise has a question.
1202
     *
1203
     * @param int $questionId
1204
     *
1205
     * @return int
1206
     */
1207
    public function hasQuestion($questionId)
1208
    {
1209
        $questionId = (int) $questionId;
1210
1211
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1212
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1213
        $sql = "SELECT q.iid
1214
                FROM $TBL_EXERCICE_QUESTION e
1215
                INNER JOIN $TBL_QUESTIONS q
1216
                ON (e.question_id = q.iid)
1217
                WHERE
1218
                    q.iid = $questionId AND
1219
                    e.quiz_id = ".$this->getId();
1220
1221
        $result = Database::query($sql);
1222
1223
        return Database::num_rows($result) > 0;
1224
    }
1225
1226
    public function hasQuestionWithType($type)
1227
    {
1228
        $type = (int) $type;
1229
1230
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1231
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
1232
        $sql = "SELECT q.iid
1233
                FROM $table e
1234
                INNER JOIN $tableQuestion q
1235
                ON (e.question_id = q.iid)
1236
                WHERE
1237
                    q.type = $type AND
1238
                    e.quiz_id = ".$this->getId();
1239
1240
        $result = Database::query($sql);
1241
1242
        return Database::num_rows($result) > 0;
1243
    }
1244
1245
    public function hasQuestionWithTypeNotInList(array $questionTypeList)
1246
    {
1247
        if (empty($questionTypeList)) {
1248
            return false;
1249
        }
1250
1251
        $questionTypeToString = implode("','", array_map('intval', $questionTypeList));
1252
1253
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1254
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
1255
        $sql = "SELECT q.iid
1256
                FROM $table e
1257
                INNER JOIN $tableQuestion q
1258
                ON (e.question_id = q.iid)
1259
                WHERE
1260
                    q.type NOT IN ('$questionTypeToString')  AND
1261
1262
                    e.quiz_id = ".$this->getId();
1263
1264
        $result = Database::query($sql);
1265
1266
        return Database::num_rows($result) > 0;
1267
    }
1268
1269
    /**
1270
     * changes the exercise title.
1271
     *
1272
     * @param string $title - exercise title
1273
     *
1274
     * @author Olivier Brouckaert
1275
     */
1276
    public function updateTitle($title)
1277
    {
1278
        $this->title = $this->exercise = $title;
1279
    }
1280
1281
    /**
1282
     * changes the exercise max attempts.
1283
     *
1284
     * @param int $attempts - exercise max attempts
1285
     */
1286
    public function updateAttempts($attempts)
1287
    {
1288
        $this->attempts = $attempts;
1289
    }
1290
1291
    /**
1292
     * changes the exercise feedback type.
1293
     *
1294
     * @param int $feedback_type
1295
     */
1296
    public function updateFeedbackType($feedback_type)
1297
    {
1298
        $this->feedback_type = $feedback_type;
1299
    }
1300
1301
    /**
1302
     * changes the exercise description.
1303
     *
1304
     * @param string $description - exercise description
1305
     *
1306
     * @author Olivier Brouckaert
1307
     */
1308
    public function updateDescription($description)
1309
    {
1310
        $this->description = $description;
1311
    }
1312
1313
    /**
1314
     * changes the exercise expired_time.
1315
     *
1316
     * @param int $expired_time The expired time of the quiz
1317
     *
1318
     * @author Isaac flores
1319
     */
1320
    public function updateExpiredTime($expired_time)
1321
    {
1322
        $this->expired_time = $expired_time;
1323
    }
1324
1325
    /**
1326
     * @param $value
1327
     */
1328
    public function updatePropagateNegative($value)
1329
    {
1330
        $this->propagate_neg = $value;
1331
    }
1332
1333
    /**
1334
     * @param int $value
1335
     */
1336
    public function updateSaveCorrectAnswers($value)
1337
    {
1338
        $this->saveCorrectAnswers = (int) $value;
1339
    }
1340
1341
    /**
1342
     * @param $value
1343
     */
1344
    public function updateReviewAnswers($value)
1345
    {
1346
        $this->review_answers = isset($value) && $value ? true : false;
1347
    }
1348
1349
    /**
1350
     * @param $value
1351
     */
1352
    public function updatePassPercentage($value)
1353
    {
1354
        $this->pass_percentage = $value;
1355
    }
1356
1357
    /**
1358
     * @param string $text
1359
     */
1360
    public function updateEmailNotificationTemplate($text)
1361
    {
1362
        $this->emailNotificationTemplate = $text;
1363
    }
1364
1365
    /**
1366
     * @param string $text
1367
     */
1368
    public function setEmailNotificationTemplateToUser($text)
1369
    {
1370
        $this->emailNotificationTemplateToUser = $text;
1371
    }
1372
1373
    /**
1374
     * @param string $value
1375
     */
1376
    public function setNotifyUserByEmail($value)
1377
    {
1378
        $this->notifyUserByEmail = $value;
1379
    }
1380
1381
    /**
1382
     * @param int $value
1383
     */
1384
    public function updateEndButton($value)
1385
    {
1386
        $this->endButton = (int) $value;
1387
    }
1388
1389
    /**
1390
     * @param string $value
1391
     */
1392
    public function setOnSuccessMessage($value)
1393
    {
1394
        $this->onSuccessMessage = $value;
1395
    }
1396
1397
    /**
1398
     * @param string $value
1399
     */
1400
    public function setOnFailedMessage($value)
1401
    {
1402
        $this->onFailedMessage = $value;
1403
    }
1404
1405
    /**
1406
     * @param $value
1407
     */
1408
    public function setModelType($value)
1409
    {
1410
        $this->modelType = (int) $value;
1411
    }
1412
1413
    /**
1414
     * @param int $value
1415
     */
1416
    public function setQuestionSelectionType($value)
1417
    {
1418
        $this->questionSelectionType = (int) $value;
1419
    }
1420
1421
    /**
1422
     * @return int
1423
     */
1424
    public function getQuestionSelectionType()
1425
    {
1426
        return (int) $this->questionSelectionType;
1427
    }
1428
1429
    /**
1430
     * @param array $categories
1431
     */
1432
    public function updateCategories($categories)
1433
    {
1434
        if (!empty($categories)) {
1435
            $categories = array_map('intval', $categories);
1436
            $this->categories = $categories;
1437
        }
1438
    }
1439
1440
    /**
1441
     * changes the exercise sound file.
1442
     *
1443
     * @param string $sound  - exercise sound file
1444
     * @param string $delete - ask to delete the file
1445
     *
1446
     * @author Olivier Brouckaert
1447
     */
1448
    public function updateSound($sound, $delete)
1449
    {
1450
        global $audioPath, $documentPath;
1451
        $TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
1452
1453
        if ($sound['size'] &&
1454
            (strstr($sound['type'], 'audio') || strstr($sound['type'], 'video'))
1455
        ) {
1456
            $this->sound = $sound['name'];
1457
1458
            if (@move_uploaded_file($sound['tmp_name'], $audioPath.'/'.$this->sound)) {
1459
                $sql = "SELECT 1 FROM $TBL_DOCUMENT
1460
                        WHERE
1461
                            c_id = ".$this->course_id." AND
1462
                            path = '".str_replace($documentPath, '', $audioPath).'/'.$this->sound."'";
1463
                $result = Database::query($sql);
1464
1465
                if (!Database::num_rows($result)) {
1466
                    DocumentManager::addDocument(
1467
                        $this->course,
1468
                        str_replace($documentPath, '', $audioPath).'/'.$this->sound,
1469
                        'file',
1470
                        $sound['size'],
1471
                        $sound['name']
1472
                    );
1473
                }
1474
            }
1475
        } elseif ($delete && is_file($audioPath.'/'.$this->sound)) {
1476
            $this->sound = '';
1477
        }
1478
    }
1479
1480
    /**
1481
     * changes the exercise type.
1482
     *
1483
     * @param int $type - exercise type
1484
     *
1485
     * @author Olivier Brouckaert
1486
     */
1487
    public function updateType($type)
1488
    {
1489
        $this->type = $type;
1490
    }
1491
1492
    /**
1493
     * sets to 0 if questions are not selected randomly
1494
     * if questions are selected randomly, sets the draws.
1495
     *
1496
     * @param int $random - 0 if not random, otherwise the draws
1497
     *
1498
     * @author Olivier Brouckaert
1499
     */
1500
    public function setRandom($random)
1501
    {
1502
        $this->random = $random;
1503
    }
1504
1505
    /**
1506
     * sets to 0 if answers are not selected randomly
1507
     * if answers are selected randomly.
1508
     *
1509
     * @param int $random_answers - random answers
1510
     *
1511
     * @author Juan Carlos Rana
1512
     */
1513
    public function updateRandomAnswers($random_answers)
1514
    {
1515
        $this->random_answers = $random_answers;
1516
    }
1517
1518
    /**
1519
     * enables the exercise.
1520
     *
1521
     * @author Olivier Brouckaert
1522
     */
1523
    public function enable()
1524
    {
1525
        $this->active = 1;
1526
    }
1527
1528
    /**
1529
     * disables the exercise.
1530
     *
1531
     * @author Olivier Brouckaert
1532
     */
1533
    public function disable()
1534
    {
1535
        $this->active = 0;
1536
    }
1537
1538
    /**
1539
     * Set disable results.
1540
     */
1541
    public function disable_results()
1542
    {
1543
        $this->results_disabled = true;
1544
    }
1545
1546
    /**
1547
     * Enable results.
1548
     */
1549
    public function enable_results()
1550
    {
1551
        $this->results_disabled = false;
1552
    }
1553
1554
    /**
1555
     * @param int $results_disabled
1556
     */
1557
    public function updateResultsDisabled($results_disabled)
1558
    {
1559
        $this->results_disabled = (int) $results_disabled;
1560
    }
1561
1562
    /**
1563
     * updates the exercise in the data base.
1564
     *
1565
     * @author Olivier Brouckaert
1566
     */
1567
    public function save()
1568
    {
1569
        $id = $this->getId();
1570
        $title = $this->exercise;
1571
        $description = $this->description;
1572
        $sound = $this->sound;
1573
        $type = $this->type;
1574
        $attempts = isset($this->attempts) ? (int) $this->attempts : 0;
1575
        $feedback_type = isset($this->feedback_type) ? (int) $this->feedback_type : 0;
1576
        $random = $this->random;
1577
        $random_answers = $this->random_answers;
1578
        $active = $this->active;
1579
        $propagate_neg = (int) $this->propagate_neg;
1580
        $saveCorrectAnswers = isset($this->saveCorrectAnswers) ? (int) $this->saveCorrectAnswers : 0;
1581
        $review_answers = isset($this->review_answers) && $this->review_answers ? 1 : 0;
1582
        $randomByCat = (int) $this->randomByCat;
1583
        $text_when_finished = $this->text_when_finished;
1584
        $text_when_finished_failure = $this->text_when_finished_failure;
1585
        $display_category_name = (int) $this->display_category_name;
1586
        $pass_percentage = (int) $this->pass_percentage;
1587
1588
        // If direct we do not show results
1589
        $results_disabled = (int) $this->results_disabled;
1590
        if (in_array($feedback_type, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1591
            $results_disabled = 0;
1592
        }
1593
        $expired_time = (int) $this->expired_time;
1594
1595
        $repo = Container::getQuizRepository();
1596
        $repoCategory = Container::getQuizCategoryRepository();
1597
1598
        // we prepare date in the database using the api_get_utc_datetime() function
1599
        $start_time = null;
1600
        if (!empty($this->start_time)) {
1601
            $start_time = $this->start_time;
1602
        }
1603
1604
        $end_time = null;
1605
        if (!empty($this->end_time)) {
1606
            $end_time = $this->end_time;
1607
        }
1608
1609
        // Exercise already exists
1610
        if ($id) {
1611
            /** @var CQuiz $exercise */
1612
            $exercise = $repo->find($id);
1613
        } else {
1614
            $exercise = new CQuiz();
1615
        }
1616
1617
        $exercise
1618
            ->setStartTime($start_time)
1619
            ->setEndTime($end_time)
1620
            ->setTitle($title)
1621
            ->setDescription($description)
1622
            ->setSound($sound)
1623
            ->setType($type)
1624
            ->setRandom((int) $random)
1625
            ->setRandomAnswers((bool) $random_answers)
1626
            ->setActive((int) $active)
1627
            ->setResultsDisabled($results_disabled)
1628
            ->setMaxAttempt($attempts)
1629
            ->setFeedbackType($feedback_type)
1630
            ->setExpiredTime($expired_time)
1631
            ->setReviewAnswers($review_answers)
1632
            ->setRandomByCategory($randomByCat)
1633
            ->setTextWhenFinished($text_when_finished)
1634
            ->setTextWhenFinishedFailure($text_when_finished_failure)
1635
            ->setDisplayCategoryName($display_category_name)
1636
            ->setPassPercentage($pass_percentage)
1637
            ->setSaveCorrectAnswers($saveCorrectAnswers)
1638
            ->setPropagateNeg($propagate_neg)
1639
            ->setHideQuestionTitle(1 === (int) $this->getHideQuestionTitle())
1640
            ->setQuestionSelectionType($this->getQuestionSelectionType())
1641
            ->setHideQuestionNumber((int) $this->hideQuestionNumber)
1642
        ;
1643
1644
        $allow = ('true' === api_get_setting('exercise.allow_exercise_categories'));
1645
        if (true === $allow && !empty($this->getQuizCategoryId())) {
1646
            $exercise->setQuizCategory($repoCategory->find($this->getQuizCategoryId()));
1647
        }
1648
1649
        $exercise->setPreventBackwards($this->getPreventBackwards());
1650
1651
        $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
1652
        if (true === $allow) {
1653
            $exercise->setShowPreviousButton($this->showPreviousButton());
1654
        }
1655
1656
        $allow = ('true' === api_get_setting('exercise.allow_notification_setting_per_exercise'));
1657
        if (true === $allow) {
1658
            $notifications = $this->getNotifications();
1659
            if (!empty($notifications)) {
1660
                $notifications = implode(',', $notifications);
1661
                $exercise->setNotifications($notifications);
1662
            }
1663
        }
1664
1665
        if (!empty($this->pageResultConfiguration)) {
1666
            $exercise->setPageResultConfiguration($this->pageResultConfiguration);
1667
        }
1668
1669
        $em = Database::getManager();
1670
1671
        if ($id) {
1672
            $repo->updateNodeForResource($exercise);
1673
1674
            if ('true' === api_get_setting('search_enabled')) {
1675
                $this->search_engine_edit();
1676
            }
1677
            $em->persist($exercise);
1678
            $em->flush();
1679
        } else {
1680
            // Creates a new exercise
1681
            $courseEntity = api_get_course_entity($this->course_id);
1682
            $exercise
1683
                ->setParent($courseEntity)
1684
                ->addCourseLink($courseEntity, api_get_session_entity());
1685
            $em->persist($exercise);
1686
            $em->flush();
1687
            $id = $exercise->getIid();
1688
            $this->iId = $this->id = $id;
1689
            if ($id) {
1690
                if ('true' === api_get_setting('search_enabled') && extension_loaded('xapian')) {
1691
                    $this->search_engine_save();
1692
                }
1693
            }
1694
        }
1695
1696
        $this->saveCategoriesInExercise($this->categories);
1697
1698
        return $id;
1699
    }
1700
1701
    /**
1702
     * Updates question position.
1703
     *
1704
     * @return bool
1705
     */
1706
    public function update_question_positions()
1707
    {
1708
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1709
        // Fixes #3483 when updating order
1710
        $questionList = $this->selectQuestionList(true);
1711
1712
        if (empty($this->getId())) {
1713
            return false;
1714
        }
1715
1716
        if (!empty($questionList)) {
1717
            foreach ($questionList as $position => $questionId) {
1718
                $position = (int) $position;
1719
                $questionId = (int) $questionId;
1720
                $sql = "UPDATE $table SET
1721
                            question_order = $position
1722
                        WHERE
1723
                            question_id = $questionId AND
1724
                            quiz_id= ".$this->getId();
1725
                Database::query($sql);
1726
            }
1727
        }
1728
1729
        return true;
1730
    }
1731
1732
    /**
1733
     * Adds a question into the question list.
1734
     *
1735
     * @param int $questionId - question ID
1736
     *
1737
     * @return bool - true if the question has been added, otherwise false
1738
     *
1739
     * @author Olivier Brouckaert
1740
     */
1741
    public function addToList($questionId)
1742
    {
1743
        // checks if the question ID is not in the list
1744
        if (!$this->isInList($questionId)) {
1745
            // selects the max position
1746
            if (!$this->selectNbrQuestions()) {
1747
                $pos = 1;
1748
            } else {
1749
                if (is_array($this->questionList)) {
1750
                    $pos = max(array_keys($this->questionList)) + 1;
1751
                }
1752
            }
1753
            $this->questionList[$pos] = $questionId;
1754
1755
            return true;
1756
        }
1757
1758
        return false;
1759
    }
1760
1761
    /**
1762
     * removes a question from the question list.
1763
     *
1764
     * @param int $questionId - question ID
1765
     *
1766
     * @return bool - true if the question has been removed, otherwise false
1767
     *
1768
     * @author Olivier Brouckaert
1769
     */
1770
    public function removeFromList($questionId)
1771
    {
1772
        // searches the position of the question ID in the list
1773
        $pos = array_search($questionId, $this->questionList);
1774
        // question not found
1775
        if (false === $pos) {
1776
            return false;
1777
        } else {
1778
            // dont reduce the number of random question if we use random by category option, or if
1779
            // random all questions
1780
            if ($this->isRandom() && 0 == $this->isRandomByCat()) {
1781
                if (count($this->questionList) >= $this->random && $this->random > 0) {
1782
                    $this->random--;
1783
                    $this->save();
1784
                }
1785
            }
1786
            // deletes the position from the array containing the wanted question ID
1787
            unset($this->questionList[$pos]);
1788
1789
            return true;
1790
        }
1791
    }
1792
1793
    /**
1794
     * deletes the exercise from the database
1795
     * Notice : leaves the question in the data base.
1796
     *
1797
     * @author Olivier Brouckaert
1798
     */
1799
    public function delete()
1800
    {
1801
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
1802
1803
        if ($limitTeacherAccess && !api_is_platform_admin()) {
1804
            return false;
1805
        }
1806
1807
        $exerciseId = $this->iId;
1808
1809
        $repo = Container::getQuizRepository();
1810
        /** @var CQuiz $exercise */
1811
        $exercise = $repo->find($exerciseId);
1812
        $linksRepo = Container::$container->get(ResourceLinkRepository::class);
1813
1814
        if (null === $exercise) {
1815
            return false;
1816
        }
1817
1818
        $locked = api_resource_is_locked_by_gradebook(
1819
            $exerciseId,
1820
            LINK_EXERCISE
1821
        );
1822
1823
        if ($locked) {
1824
            return false;
1825
        }
1826
1827
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
1828
        $sql = "UPDATE $table SET active='-1'
1829
                WHERE iid = $exerciseId";
1830
        Database::query($sql);
1831
1832
        $course = api_get_course_entity();
1833
        $session = api_get_session_entity();
1834
1835
        $linksRepo->removeByResourceInContext($exercise, $course, $session);
1836
1837
        SkillModel::deleteSkillsFromItem($exerciseId, ITEM_TYPE_EXERCISE);
1838
1839
        if ('true' === api_get_setting('search_enabled') &&
1840
            extension_loaded('xapian')
1841
        ) {
1842
            $this->search_engine_delete();
1843
        }
1844
1845
        $linkInfo = GradebookUtils::isResourceInCourseGradebook(
1846
            $this->course_id,
1847
            LINK_EXERCISE,
1848
            $exerciseId,
1849
            $this->sessionId
1850
        );
1851
        if (!empty($linkInfo)) {
1852
            GradebookUtils::remove_resource_from_course_gradebook($linkInfo['id']);
1853
        }
1854
1855
        // Register resource deletion manually because this is a soft delete (active = -1)
1856
        // and Doctrine does not trigger postRemove in this case.
1857
        /* @var TrackEDefaultRepository $trackRepo */
1858
        $trackRepo = Container::$container->get(TrackEDefaultRepository::class);
1859
        $resourceNode = $exercise->getResourceNode();
1860
        if ($resourceNode) {
1861
            $trackRepo->registerResourceEvent(
1862
                $resourceNode,
1863
                'deletion',
1864
                api_get_user_id(),
1865
                api_get_course_int_id(),
1866
                api_get_session_id()
1867
            );
1868
        }
1869
1870
        return true;
1871
    }
1872
1873
    /**
1874
     * Creates the form to create / edit an exercise.
1875
     *
1876
     * @param FormValidator $form
1877
     * @param string|array        $type
1878
     */
1879
    public function createForm($form, $type = 'full')
1880
    {
1881
        if (empty($type)) {
1882
            $type = 'full';
1883
        }
1884
1885
        // Form title
1886
        $form_title = get_lang('Create a new test');
1887
        if (!empty($_GET['id'])) {
1888
            $form_title = get_lang('Edit test name and settings');
1889
        }
1890
1891
        $form->addHeader($form_title);
1892
1893
        // Title.
1894
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
1895
            $form->addHtmlEditor(
1896
                'exerciseTitle',
1897
                get_lang('Test name'),
1898
                false,
1899
                false,
1900
                ['ToolbarSet' => 'TitleAsHtml']
1901
            );
1902
        } else {
1903
            $form->addElement(
1904
                'text',
1905
                'exerciseTitle',
1906
                get_lang('Test name'),
1907
                ['id' => 'exercise_title']
1908
            );
1909
        }
1910
1911
        $form->addElement('advanced_settings', 'advanced_params', get_lang('Advanced settings'));
1912
        $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
1913
1914
        if ('true' === api_get_setting('exercise.allow_exercise_categories')) {
1915
            $categoryManager = new ExerciseCategoryManager();
1916
            $categories = $categoryManager->getCategories(api_get_course_int_id());
1917
            $options = [];
1918
            if (!empty($categories)) {
1919
                /** @var CQuizCategory $category */
1920
                foreach ($categories as $category) {
1921
                    $options[$category->getId()] = $category->getTitle();
1922
                }
1923
            }
1924
1925
            $form->addSelect(
1926
                'quiz_category_id',
1927
                get_lang('Category'),
1928
                $options,
1929
                ['placeholder' => get_lang('Please select an option')]
1930
            );
1931
        }
1932
1933
        $editor_config = [
1934
            'ToolbarSet' => 'TestQuestionDescription',
1935
            'Width' => '100%',
1936
            'Height' => '150',
1937
        ];
1938
1939
        if (is_array($type)) {
1940
            $editor_config = array_merge($editor_config, $type);
1941
        }
1942
1943
        $form->addHtmlEditor(
1944
            'exerciseDescription',
1945
            get_lang('Give a context to the test'),
1946
            false,
1947
            false,
1948
            $editor_config
1949
        );
1950
1951
        $skillList = [];
1952
        if ('full' === $type) {
1953
            // Can't modify a DirectFeedback question.
1954
            if (!in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1955
                $this->setResultFeedbackGroup($form);
1956
1957
                // Type of results display on the final page
1958
                $this->setResultDisabledGroup($form);
1959
1960
                // Type of questions disposition on page
1961
                $radios = [];
1962
                $radios[] = $form->createElement(
1963
                    'radio',
1964
                    'exerciseType',
1965
                    null,
1966
                    get_lang('All questions on one page'),
1967
                    '1',
1968
                    [
1969
                        'onclick' => 'check_per_page_all()',
1970
                        'id' => 'option_page_all',
1971
                    ]
1972
                );
1973
                $radios[] = $form->createElement(
1974
                    'radio',
1975
                    'exerciseType',
1976
                    null,
1977
                    get_lang('One question by page'),
1978
                    '2',
1979
                    [
1980
                        'onclick' => 'check_per_page_one()',
1981
                        'id' => 'option_page_one',
1982
                    ]
1983
                );
1984
1985
                $form->addGroup($radios, null, get_lang('Questions per page'));
1986
            } else {
1987
                // if is Direct feedback but has not questions we can allow to modify the question type
1988
                if (empty($this->iId) || 0 === $this->getQuestionCount()) {
1989
                    $this->setResultFeedbackGroup($form);
1990
                    $this->setResultDisabledGroup($form);
1991
1992
                    // Type of questions disposition on page
1993
                    $radios = [];
1994
                    $radios[] = $form->createElement(
1995
                        'radio',
1996
                        'exerciseType',
1997
                        null,
1998
                        get_lang('All questions on one page'),
1999
                        '1'
2000
                    );
2001
                    $radios[] = $form->createElement(
2002
                        'radio',
2003
                        'exerciseType',
2004
                        null,
2005
                        get_lang('One question by page'),
2006
                        '2'
2007
                    );
2008
                    $form->addGroup($radios, null, get_lang('Sequential'));
2009
                } else {
2010
                    $this->setResultFeedbackGroup($form, true);
2011
                    $group = $this->setResultDisabledGroup($form);
2012
                    $group->freeze();
2013
2014
                    // we force the options to the DirectFeedback exercisetype
2015
                    //$form->addElement('hidden', 'exerciseFeedbackType', $this->getFeedbackType());
2016
                    //$form->addElement('hidden', 'exerciseType', ONE_PER_PAGE);
2017
2018
                    // Type of questions disposition on page
2019
                    $radios[] = $form->createElement(
2020
                        'radio',
2021
                        'exerciseType',
2022
                        null,
2023
                        get_lang('All questions on one page'),
2024
                        '1',
2025
                        [
2026
                            'onclick' => 'check_per_page_all()',
2027
                            'id' => 'option_page_all',
2028
                        ]
2029
                    );
2030
                    $radios[] = $form->createElement(
2031
                        'radio',
2032
                        'exerciseType',
2033
                        null,
2034
                        get_lang('One question by page'),
2035
                        '2',
2036
                        [
2037
                            'onclick' => 'check_per_page_one()',
2038
                            'id' => 'option_page_one',
2039
                        ]
2040
                    );
2041
2042
                    $type_group = $form->addGroup($radios, null, get_lang('Questions per page'));
2043
                    $type_group->freeze();
2044
                }
2045
            }
2046
2047
            $option = [
2048
                EX_Q_SELECTION_ORDERED => get_lang('Ordered by user'),
2049
                //  Defined by user
2050
                EX_Q_SELECTION_RANDOM => get_lang('Random'),
2051
                // 1-10, All
2052
                'per_categories' => '--------'.get_lang('Using categories').'----------',
2053
                // Base (A 123 {3} B 456 {3} C 789{2} D 0{0}) --> Matrix {3, 3, 2, 0}
2054
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED => get_lang(
2055
                    'Ordered categories alphabetically with questions ordered'
2056
                ),
2057
                // A 123 B 456 C 78 (0, 1, all)
2058
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED => get_lang(
2059
                    'Random categories with questions ordered'
2060
                ),
2061
                // C 78 B 456 A 123
2062
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM => get_lang(
2063
                    'Ordered categories alphabetically with random questions'
2064
                ),
2065
                // A 321 B 654 C 87
2066
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM => get_lang(
2067
                    'Random categories with random questions'
2068
                ),
2069
                // C 87 B 654 A 321
2070
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED => get_lang('Random categories with questions ordered (questions not grouped)'),
2071
                /*    B 456 C 78 A 123
2072
                        456 78 123
2073
                        123 456 78
2074
                */
2075
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED => get_lang('Random categories with random questions (questions not grouped)'),
2076
                /*
2077
                    A 123 B 456 C 78
2078
                    B 456 C 78 A 123
2079
                    B 654 C 87 A 321
2080
                    654 87 321
2081
                    165 842 73
2082
                */
2083
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED => get_lang('Ordered categories by parent with questions ordered'),
2084
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM => get_lang('Ordered categories by parent with random questions'),
2085
            ];
2086
2087
            $form->addSelect(
2088
                'question_selection_type',
2089
                [get_lang('Question selection type')],
2090
                $option,
2091
                [
2092
                    'id' => 'questionSelection',
2093
                    'onchange' => 'checkQuestionSelection()',
2094
                ]
2095
            );
2096
2097
            $group = [
2098
                $form->createElement(
2099
                    'checkbox',
2100
                    'hide_expected_answer',
2101
                    null,
2102
                    get_lang('Hide expected answers column')
2103
                ),
2104
                $form->createElement(
2105
                    'checkbox',
2106
                    'hide_total_score',
2107
                    null,
2108
                    get_lang('Hide total score')
2109
                ),
2110
                $form->createElement(
2111
                    'checkbox',
2112
                    'hide_question_score',
2113
                    null,
2114
                    get_lang('Hide question score')
2115
                ),
2116
                $form->createElement(
2117
                    'checkbox',
2118
                    'hide_category_table',
2119
                    null,
2120
                    get_lang('Hide category table')
2121
                ),
2122
                $form->createElement(
2123
                    'checkbox',
2124
                    'hide_correct_answered_questions',
2125
                    null,
2126
                    get_lang('Hide correct answered questions')
2127
                ),
2128
            ];
2129
            $form->addGroup($group, null, get_lang('Results page configuration'));
2130
2131
            $group = [
2132
                $form->createElement('radio', 'hide_question_number', null, get_lang('Yes'), '1'),
2133
                $form->createElement('radio', 'hide_question_number', null, get_lang('No'), '0'),
2134
            ];
2135
            $form->addGroup($group, null, get_lang('Hide question numbering'));
2136
2137
            $displayMatrix = 'none';
2138
            $displayRandom = 'none';
2139
            $selectionType = $this->getQuestionSelectionType();
2140
            switch ($selectionType) {
2141
                case EX_Q_SELECTION_RANDOM:
2142
                    $displayRandom = 'block';
2143
2144
                    break;
2145
                case $selectionType >= EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED:
2146
                    $displayMatrix = 'block';
2147
2148
                    break;
2149
            }
2150
2151
            $form->addHtml('<div id="hidden_random" style="display:'.$displayRandom.'">');
2152
            // Number of random question.
2153
            $max = $this->getId() > 0 ? $this->getQuestionCount() : 10;
2154
            $option = range(0, $max);
2155
            $option[0] = get_lang('No');
2156
            $option[-1] = get_lang('All');
2157
            $form->addSelect(
2158
                'randomQuestions',
2159
                [
2160
                    get_lang('Random questions'),
2161
                    get_lang("To randomize all questions choose 10. To disable randomization, choose \"Do not randomize\"."),
2162
                ],
2163
                $option,
2164
                ['id' => 'randomQuestions']
2165
            );
2166
            $form->addHtml('</div>');
2167
            $form->addHtml('<div id="hidden_matrix" style="display:'.$displayMatrix.'">');
2168
2169
            // Category selection.
2170
            $cat = new TestCategory();
2171
            $cat_form = $cat->returnCategoryForm($this);
2172
            if (empty($cat_form)) {
2173
                $cat_form = '<span class="label label-warning">'.get_lang('No categories defined').'</span>';
2174
            }
2175
            $form->addElement('label', null, $cat_form);
2176
            $form->addHtml('</div>');
2177
2178
            // Random answers.
2179
            $radios_random_answers = [
2180
                $form->createElement('radio', 'randomAnswers', null, get_lang('Yes'), '1'),
2181
                $form->createElement('radio', 'randomAnswers', null, get_lang('No'), '0'),
2182
            ];
2183
            $form->addGroup($radios_random_answers, null, get_lang('Shuffle answers'));
2184
2185
            // Category name.
2186
            $radio_display_cat_name = [
2187
                $form->createElement('radio', 'display_category_name', null, get_lang('Yes'), '1'),
2188
                $form->createElement('radio', 'display_category_name', null, get_lang('No'), '0'),
2189
            ];
2190
            $form->addGroup($radio_display_cat_name, null, get_lang('Display questions category'));
2191
2192
            // Hide question title.
2193
            $group = [
2194
                $form->createElement('radio', 'hide_question_title', null, get_lang('Yes'), '1'),
2195
                $form->createElement('radio', 'hide_question_title', null, get_lang('No'), '0'),
2196
            ];
2197
            $form->addGroup($group, null, get_lang('Hide question title'));
2198
2199
            $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
2200
2201
            if (true === $allow) {
2202
                // Hide question title.
2203
                $group = [
2204
                    $form->createElement(
2205
                        'radio',
2206
                        'show_previous_button',
2207
                        null,
2208
                        get_lang('Yes'),
2209
                        '1'
2210
                    ),
2211
                    $form->createElement(
2212
                        'radio',
2213
                        'show_previous_button',
2214
                        null,
2215
                        get_lang('No'),
2216
                        '0'
2217
                    ),
2218
                ];
2219
                $form->addGroup($group, null, get_lang('Show previous button'));
2220
            }
2221
2222
            $form->addElement(
2223
                'number',
2224
                'exerciseAttempts',
2225
                get_lang('Max number of attempts'),
2226
                null,
2227
                ['id' => 'exerciseAttempts']
2228
            );
2229
2230
            // Exercise time limit
2231
            $form->addElement(
2232
                'checkbox',
2233
                'activate_start_date_check',
2234
                null,
2235
                get_lang('Enable start time'),
2236
                ['onclick' => 'activate_start_date()']
2237
            );
2238
2239
            if (!empty($this->start_time)) {
2240
                $form->addElement('html', '<div id="start_date_div" style="display:block;">');
2241
            } else {
2242
                $form->addElement('html', '<div id="start_date_div" style="display:none;">');
2243
            }
2244
2245
            $form->addElement('date_time_picker', 'start_time');
2246
            $form->addElement('html', '</div>');
2247
            $form->addElement(
2248
                'checkbox',
2249
                'activate_end_date_check',
2250
                null,
2251
                get_lang('Enable end time'),
2252
                ['onclick' => 'activate_end_date()']
2253
            );
2254
2255
            if (!empty($this->end_time)) {
2256
                $form->addHtml('<div id="end_date_div" style="display:block;">');
2257
            } else {
2258
                $form->addHtml('<div id="end_date_div" style="display:none;">');
2259
            }
2260
2261
            $form->addElement('date_time_picker', 'end_time');
2262
            $form->addElement('html', '</div>');
2263
2264
            $display = 'block';
2265
            $form->addElement(
2266
                'checkbox',
2267
                'propagate_neg',
2268
                null,
2269
                get_lang('Propagate negative results between questions')
2270
            );
2271
2272
            $options = [
2273
                '' => get_lang('Please select an option'),
2274
                1 => get_lang('Save the correct answer for the next attempt'),
2275
                2 => get_lang('Pre-fill with answers from previous attempt'),
2276
            ];
2277
            $form->addSelect(
2278
                'save_correct_answers',
2279
                get_lang('Save answers'),
2280
                $options
2281
            );
2282
2283
            $form->addElement('html', '<div class="clear">&nbsp;</div>');
2284
            $form->addCheckBox('review_answers', null, get_lang('Review my answers'));
2285
            $form->addElement('html', '<div id="divtimecontrol"  style="display:'.$display.';">');
2286
2287
            // Timer control
2288
            $form->addElement(
2289
                'checkbox',
2290
                'enabletimercontrol',
2291
                null,
2292
                get_lang('Enable time control'),
2293
                [
2294
                    'onclick' => 'option_time_expired()',
2295
                    'id' => 'enabletimercontrol',
2296
                    'onload' => 'check_load_time()',
2297
                ]
2298
            );
2299
2300
            $expired_date = (int) $this->selectExpiredTime();
2301
2302
            if (('0' != $expired_date)) {
2303
                $form->addElement('html', '<div id="timercontrol" style="display:block;">');
2304
            } else {
2305
                $form->addElement('html', '<div id="timercontrol" style="display:none;">');
2306
            }
2307
            $form->addText(
2308
                'enabletimercontroltotalminutes',
2309
                get_lang('Total duration in minutes of the test'),
2310
                false,
2311
                [
2312
                    'id' => 'enabletimercontroltotalminutes',
2313
                    'cols-size' => [2, 2, 8],
2314
                ]
2315
            );
2316
            $form->addElement('html', '</div>');
2317
            $form->addCheckBox('prevent_backwards', null, get_lang('Prevent moving backwards between questions'));
2318
            $form->addElement(
2319
                'text',
2320
                'pass_percentage',
2321
                [get_lang('Pass percentage'), null, '%'],
2322
                ['id' => 'pass_percentage']
2323
            );
2324
2325
            $form->addRule('pass_percentage', get_lang('Numerical'), 'numeric');
2326
            $form->addRule('pass_percentage', get_lang('Value is too small.'), 'min_numeric_length', 0);
2327
            $form->addRule('pass_percentage', get_lang('Value is too big.'), 'max_numeric_length', 100);
2328
2329
            // add the text_when_finished textbox
2330
            $form->addHtmlEditor(
2331
                'text_when_finished',
2332
                get_lang('Text appearing at the end of the test when the user has succeeded or if no pass percentage was set.'),
2333
                false,
2334
                false,
2335
                $editor_config
2336
            );
2337
            $form->addHtmlEditor(
2338
                'text_when_finished_failure',
2339
                get_lang('Text appearing at the end of the test when the user has failed.'),
2340
                false,
2341
                false,
2342
                $editor_config
2343
            );
2344
2345
            $allow = ('true' === api_get_setting('exercise.allow_notification_setting_per_exercise'));
2346
            if (true === $allow) {
2347
                $settings = ExerciseLib::getNotificationSettings();
2348
                $group = [];
2349
                foreach ($settings as $itemId => $label) {
2350
                    $group[] = $form->createElement(
2351
                        'checkbox',
2352
                        'notifications[]',
2353
                        null,
2354
                        $label,
2355
                        ['value' => $itemId]
2356
                    );
2357
                }
2358
                $form->addGroup($group, '', [get_lang('E-mail notifications')]);
2359
            }
2360
2361
            $form->addCheckBox('update_title_in_lps', null, get_lang('Update this title in learning paths'));
2362
2363
            $defaults = [];
2364
            if ('true' === api_get_setting('search_enabled')) {
2365
                $form->addCheckBox('index_document', '', get_lang('Index document text?'));
2366
                $form->addSelectLanguage('language', get_lang('Document language for indexation'));
2367
                $specific_fields = get_specific_field_list();
2368
2369
                foreach ($specific_fields as $specific_field) {
2370
                    $form->addElement('text', $specific_field['code'], $specific_field['name']);
2371
                    $filter = [
2372
                        'c_id' => api_get_course_int_id(),
2373
                        'field_id' => $specific_field['id'],
2374
                        'ref_id' => $this->getId(),
2375
                        'tool_id' => "'".TOOL_QUIZ."'",
2376
                    ];
2377
                    $values = get_specific_field_values_list($filter, ['value']);
2378
                    if (!empty($values)) {
2379
                        $arr_str_values = [];
2380
                        foreach ($values as $value) {
2381
                            $arr_str_values[] = $value['value'];
2382
                        }
2383
                        $defaults[$specific_field['code']] = implode(', ', $arr_str_values);
2384
                    }
2385
                }
2386
            }
2387
2388
            $skillList = SkillModel::addSkillsToForm($form, ITEM_TYPE_EXERCISE, $this->iId);
2389
2390
            $extraField = new ExtraField('exercise');
2391
            $extraField->addElements(
2392
                $form,
2393
                $this->iId,
2394
                ['notifications'], //exclude
2395
                false, // filter
2396
                false, // tag as select
2397
                [], //show only fields
2398
                [], // order fields
2399
                [] // extra data
2400
            );
2401
            $settings = api_get_configuration_value('exercise_finished_notification_settings');
2402
            if (!empty($settings)) {
2403
                $options = [];
2404
                foreach ($settings as $name => $data) {
2405
                    $options[$name] = $name;
2406
                }
2407
                $form->addSelect(
2408
                    'extra_notifications',
2409
                    get_lang('Notifications'),
2410
                    $options,
2411
                    ['placeholder' => get_lang('Please select an option')]
2412
                );
2413
            }
2414
            $form->addElement('html', '</div>'); //End advanced setting
2415
            $form->addElement('html', '</div>');
2416
        }
2417
2418
        // submit
2419
        if (isset($_GET['id'])) {
2420
            $form->addButtonSave(get_lang('Edit test name and settings'), 'submitExercise');
2421
        } else {
2422
            $form->addButtonUpdate(get_lang('Proceed to questions'), 'submitExercise');
2423
        }
2424
2425
        $form->addRule('exerciseTitle', get_lang('Name'), 'required');
2426
2427
        // defaults
2428
        if ('full' == $type) {
2429
            // rules
2430
            $form->addRule('exerciseAttempts', get_lang('Numerical'), 'numeric');
2431
            $form->addRule('start_time', get_lang('Invalid date'), 'datetime');
2432
            $form->addRule('end_time', get_lang('Invalid date'), 'datetime');
2433
2434
            if ($this->getId() > 0) {
2435
                $defaults['randomQuestions'] = $this->random;
2436
                $defaults['randomAnswers'] = $this->getRandomAnswers();
2437
                $defaults['exerciseType'] = $this->selectType();
2438
                $defaults['exerciseTitle'] = $this->get_formated_title();
2439
                $defaults['exerciseDescription'] = $this->selectDescription();
2440
                $defaults['exerciseAttempts'] = $this->selectAttempts();
2441
                $defaults['exerciseFeedbackType'] = $this->getFeedbackType();
2442
                $defaults['results_disabled'] = $this->selectResultsDisabled();
2443
                $defaults['propagate_neg'] = $this->selectPropagateNeg();
2444
                $defaults['save_correct_answers'] = $this->getSaveCorrectAnswers();
2445
                $defaults['review_answers'] = $this->review_answers;
2446
                $defaults['randomByCat'] = $this->getRandomByCategory();
2447
                $defaults['text_when_finished'] = $this->getTextWhenFinished();
2448
                $defaults['text_when_finished_failure'] = $this->getTextWhenFinishedFailure();
2449
                $defaults['display_category_name'] = $this->selectDisplayCategoryName();
2450
                $defaults['pass_percentage'] = $this->selectPassPercentage();
2451
                $defaults['question_selection_type'] = $this->getQuestionSelectionType();
2452
                $defaults['hide_question_title'] = $this->getHideQuestionTitle();
2453
                $defaults['show_previous_button'] = $this->showPreviousButton();
2454
                $defaults['quiz_category_id'] = $this->getQuizCategoryId();
2455
                $defaults['prevent_backwards'] = $this->getPreventBackwards();
2456
                $defaults['hide_question_number'] = $this->getHideQuestionNumber();
2457
2458
                if (!empty($this->start_time)) {
2459
                    $defaults['activate_start_date_check'] = 1;
2460
                }
2461
                if (!empty($this->end_time)) {
2462
                    $defaults['activate_end_date_check'] = 1;
2463
                }
2464
2465
                $defaults['start_time'] = !empty($this->start_time) ? api_get_local_time($this->start_time) : date(
2466
                    'Y-m-d 12:00:00'
2467
                );
2468
                $defaults['end_time'] = !empty($this->end_time) ? api_get_local_time($this->end_time) : date(
2469
                    'Y-m-d 12:00:00',
2470
                    time() + 84600
2471
                );
2472
2473
                // Get expired time
2474
                if ('0' != $this->expired_time) {
2475
                    $defaults['enabletimercontrol'] = 1;
2476
                    $defaults['enabletimercontroltotalminutes'] = $this->expired_time;
2477
                } else {
2478
                    $defaults['enabletimercontroltotalminutes'] = 0;
2479
                }
2480
                $defaults['skills'] = array_keys($skillList);
2481
                $defaults['notifications'] = $this->getNotifications();
2482
            } else {
2483
                $defaults['exerciseType'] = 2;
2484
                $defaults['exerciseAttempts'] = 0;
2485
                $defaults['randomQuestions'] = 0;
2486
                $defaults['randomAnswers'] = 0;
2487
                $defaults['exerciseDescription'] = '';
2488
                $defaults['exerciseFeedbackType'] = 0;
2489
                $defaults['results_disabled'] = 0;
2490
                $defaults['randomByCat'] = 0;
2491
                $defaults['text_when_finished'] = '';
2492
                $defaults['text_when_finished_failure'] = '';
2493
                $defaults['start_time'] = date('Y-m-d 12:00:00');
2494
                $defaults['display_category_name'] = 1;
2495
                $defaults['end_time'] = date('Y-m-d 12:00:00', time() + 84600);
2496
                $defaults['pass_percentage'] = '';
2497
                $defaults['end_button'] = $this->selectEndButton();
2498
                $defaults['question_selection_type'] = 1;
2499
                $defaults['hide_question_title'] = 0;
2500
                $defaults['show_previous_button'] = 1;
2501
                $defaults['on_success_message'] = null;
2502
                $defaults['on_failed_message'] = null;
2503
            }
2504
        } else {
2505
            $defaults['exerciseTitle'] = $this->selectTitle();
2506
            $defaults['exerciseDescription'] = $this->selectDescription();
2507
        }
2508
2509
        if ('true' === api_get_setting('search_enabled')) {
2510
            $defaults['index_document'] = 'checked="checked"';
2511
        }
2512
2513
        $this->setPageResultConfigurationDefaults($defaults);
2514
        $form->setDefaults($defaults);
2515
2516
        // Freeze some elements.
2517
        if (0 != $this->getId() && false == $this->edit_exercise_in_lp) {
2518
            $elementsToFreeze = [
2519
                'randomQuestions',
2520
                //'randomByCat',
2521
                'exerciseAttempts',
2522
                'propagate_neg',
2523
                'enabletimercontrol',
2524
                'review_answers',
2525
            ];
2526
2527
            foreach ($elementsToFreeze as $elementName) {
2528
                /** @var HTML_QuickForm_element $element */
2529
                $element = $form->getElement($elementName);
2530
                $element->freeze();
2531
            }
2532
        }
2533
    }
2534
2535
    public function setResultFeedbackGroup(FormValidator $form, $checkFreeze = true)
2536
    {
2537
        // Feedback type.
2538
        $feedback = [];
2539
        $warning = sprintf(
2540
            get_lang("The setting \"%s\" will change to \"%s\""),
2541
            get_lang('Show score to learner'),
2542
            get_lang('Auto-evaluation mode: show score and expected answers')
2543
        );
2544
        $endTest = $form->createElement(
2545
            'radio',
2546
            'exerciseFeedbackType',
2547
            null,
2548
            get_lang('At end of test'),
2549
            EXERCISE_FEEDBACK_TYPE_END,
2550
            [
2551
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_END,
2552
                //'onclick' => 'if confirm() check_feedback()',
2553
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_feedback(); } else { return false;} ',
2554
            ]
2555
        );
2556
2557
        $noFeedBack = $form->createElement(
2558
            'radio',
2559
            'exerciseFeedbackType',
2560
            null,
2561
            get_lang('Exam (no feedback)'),
2562
            EXERCISE_FEEDBACK_TYPE_EXAM,
2563
            [
2564
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_EXAM,
2565
            ]
2566
        );
2567
2568
        $feedback[] = $endTest;
2569
        $feedback[] = $noFeedBack;
2570
2571
        $scenarioEnabled = 'true' === api_get_setting('enable_quiz_scenario');
2572
        $freeze = true;
2573
        if ($scenarioEnabled) {
2574
            if ($this->getQuestionCount() > 0) {
2575
                $hasDifferentQuestion = $this->hasQuestionWithTypeNotInList([UNIQUE_ANSWER, HOT_SPOT_DELINEATION]);
2576
                if (false === $hasDifferentQuestion) {
2577
                    $freeze = false;
2578
                }
2579
            } else {
2580
                $freeze = false;
2581
            }
2582
            // Can't convert a question from one feedback to another
2583
            $direct = $form->createElement(
2584
                'radio',
2585
                'exerciseFeedbackType',
2586
                null,
2587
                get_lang('Adaptative test with immediate feedback'),
2588
                EXERCISE_FEEDBACK_TYPE_DIRECT,
2589
                [
2590
                    'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_DIRECT,
2591
                    'onclick' => 'check_direct_feedback()',
2592
                ]
2593
            );
2594
2595
            $directPopUp = $form->createElement(
2596
                'radio',
2597
                'exerciseFeedbackType',
2598
                null,
2599
                get_lang('Direct pop-up mode'),
2600
                EXERCISE_FEEDBACK_TYPE_POPUP,
2601
                ['id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_POPUP, 'onclick' => 'check_direct_feedback()']
2602
            );
2603
            if ($freeze) {
2604
                $direct->freeze();
2605
                $directPopUp->freeze();
2606
            }
2607
2608
            // If has delineation freeze all.
2609
            $hasDelineation = $this->hasQuestionWithType(HOT_SPOT_DELINEATION);
2610
            if ($hasDelineation) {
2611
                $endTest->freeze();
2612
                $noFeedBack->freeze();
2613
                $direct->freeze();
2614
                $directPopUp->freeze();
2615
            }
2616
2617
            $feedback[] = $direct;
2618
            $feedback[] = $directPopUp;
2619
        }
2620
2621
        $form->addGroup(
2622
            $feedback,
2623
            null,
2624
            [
2625
                get_lang('Feedback'),
2626
                get_lang(
2627
                    'How should we show the feedback/comment for each question? This option defines how it will be shown to the learner when taking the test. We recommend you try different options by editing your test options before having learners take it.'
2628
                ),
2629
            ]
2630
        );
2631
    }
2632
2633
    /**
2634
     * function which process the creation of exercises.
2635
     *
2636
     * @param FormValidator $form
2637
     *
2638
     * @return int c_quiz.iid
2639
     */
2640
    public function processCreation($form)
2641
    {
2642
        $this->updateTitle(self::format_title_variable($form->getSubmitValue('exerciseTitle')));
2643
        $this->updateDescription($form->getSubmitValue('exerciseDescription'));
2644
        $this->updateAttempts($form->getSubmitValue('exerciseAttempts'));
2645
        $this->updateFeedbackType($form->getSubmitValue('exerciseFeedbackType'));
2646
        $this->updateType($form->getSubmitValue('exerciseType'));
2647
2648
        // If direct feedback then force to One per page
2649
        if (EXERCISE_FEEDBACK_TYPE_DIRECT == $form->getSubmitValue('exerciseFeedbackType')) {
2650
            $this->updateType(ONE_PER_PAGE);
2651
        }
2652
2653
        $this->setRandom($form->getSubmitValue('randomQuestions'));
2654
        $this->updateRandomAnswers($form->getSubmitValue('randomAnswers'));
2655
        $this->updateResultsDisabled($form->getSubmitValue('results_disabled'));
2656
        $this->updateExpiredTime($form->getSubmitValue('enabletimercontroltotalminutes'));
2657
        $this->updatePropagateNegative($form->getSubmitValue('propagate_neg'));
2658
        $this->updateSaveCorrectAnswers($form->getSubmitValue('save_correct_answers'));
2659
        $this->updateRandomByCat($form->getSubmitValue('randomByCat'));
2660
        $this->setTextWhenFinished($form->getSubmitValue('text_when_finished'));
2661
        $this->setTextWhenFinishedFailure($form->getSubmitValue('text_when_finished_failure'));
2662
        $this->updateDisplayCategoryName($form->getSubmitValue('display_category_name'));
2663
        $this->updateReviewAnswers($form->getSubmitValue('review_answers'));
2664
        $this->updatePassPercentage($form->getSubmitValue('pass_percentage'));
2665
        $this->updateCategories($form->getSubmitValue('category'));
2666
        $this->updateEndButton($form->getSubmitValue('end_button'));
2667
        $this->setOnSuccessMessage($form->getSubmitValue('on_success_message'));
2668
        $this->setOnFailedMessage($form->getSubmitValue('on_failed_message'));
2669
        $this->updateEmailNotificationTemplate($form->getSubmitValue('email_notification_template'));
2670
        $this->setEmailNotificationTemplateToUser($form->getSubmitValue('email_notification_template_to_user'));
2671
        $this->setNotifyUserByEmail($form->getSubmitValue('notify_user_by_email'));
2672
        $this->setModelType($form->getSubmitValue('model_type'));
2673
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2674
        $this->setHideQuestionTitle($form->getSubmitValue('hide_question_title'));
2675
        $this->sessionId = api_get_session_id();
2676
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2677
        $this->setScoreTypeModel($form->getSubmitValue('score_type_model'));
2678
        $this->setGlobalCategoryId($form->getSubmitValue('global_category_id'));
2679
        $this->setShowPreviousButton($form->getSubmitValue('show_previous_button'));
2680
        $this->setNotifications($form->getSubmitValue('notifications'));
2681
        $this->setQuizCategoryId($form->getSubmitValue('quiz_category_id'));
2682
        $this->setPageResultConfiguration($form->getSubmitValues());
2683
        $this->setHideQuestionNumber($form->getSubmitValue('hide_question_number'));
2684
        $this->preventBackwards = (int) $form->getSubmitValue('prevent_backwards');
2685
2686
        $this->start_time = null;
2687
        if (1 == $form->getSubmitValue('activate_start_date_check')) {
2688
            $start_time = $form->getSubmitValue('start_time');
2689
            $this->start_time = api_get_utc_datetime($start_time);
2690
        }
2691
2692
        $this->end_time = null;
2693
        if (1 == $form->getSubmitValue('activate_end_date_check')) {
2694
            $end_time = $form->getSubmitValue('end_time');
2695
            $this->end_time = api_get_utc_datetime($end_time);
2696
        }
2697
2698
        $this->expired_time = 0;
2699
        if (1 == $form->getSubmitValue('enabletimercontrol')) {
2700
            $expired_total_time = $form->getSubmitValue('enabletimercontroltotalminutes');
2701
            if (0 == $this->expired_time) {
2702
                $this->expired_time = $expired_total_time;
2703
            }
2704
        }
2705
2706
        $this->random_answers = 0;
2707
        if (1 == $form->getSubmitValue('randomAnswers')) {
2708
            $this->random_answers = 1;
2709
        }
2710
2711
        // Update title in all LPs that have this quiz added
2712
        if (1 == $form->getSubmitValue('update_title_in_lps')) {
2713
            $table = Database::get_course_table(TABLE_LP_ITEM);
2714
            $sql = "SELECT iid FROM $table
2715
                    WHERE
2716
                        item_type = 'quiz' AND
2717
                        path = '".$this->getId()."'
2718
                    ";
2719
            $result = Database::query($sql);
2720
            $items = Database::store_result($result);
2721
            if (!empty($items)) {
2722
                foreach ($items as $item) {
2723
                    $itemId = $item['iid'];
2724
                    $sql = "UPDATE $table
2725
                            SET title = '".$this->title."'
2726
                            WHERE iid = $itemId ";
2727
                    Database::query($sql);
2728
                }
2729
            }
2730
        }
2731
2732
        $iId = $this->save();
2733
        if (!empty($iId)) {
2734
            $values = $form->getSubmitValues();
2735
            $values['item_id'] = $iId;
2736
            $extraFieldValue = new ExtraFieldValue('exercise');
2737
            $extraFieldValue->saveFieldValues($values);
2738
2739
            SkillModel::saveSkills($form, ITEM_TYPE_EXERCISE, $iId);
2740
        }
2741
    }
2742
2743
    public function search_engine_save()
2744
    {
2745
        if (1 != $_POST['index_document']) {
2746
            return;
2747
        }
2748
        $course_id = api_get_course_id();
2749
        $specific_fields = get_specific_field_list();
2750
        $ic_slide = new IndexableChunk();
2751
2752
        $all_specific_terms = '';
2753
        foreach ($specific_fields as $specific_field) {
2754
            if (isset($_REQUEST[$specific_field['code']])) {
2755
                $sterms = trim($_REQUEST[$specific_field['code']]);
2756
                if (!empty($sterms)) {
2757
                    $all_specific_terms .= ' '.$sterms;
2758
                    $sterms = explode(',', $sterms);
2759
                    foreach ($sterms as $sterm) {
2760
                        $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2761
                        add_specific_field_value($specific_field['id'], $course_id, TOOL_QUIZ, $this->getId(), $sterm);
2762
                    }
2763
                }
2764
            }
2765
        }
2766
2767
        // build the chunk to index
2768
        $ic_slide->addValue('title', $this->exercise);
2769
        $ic_slide->addCourseId($course_id);
2770
        $ic_slide->addToolId(TOOL_QUIZ);
2771
        $xapian_data = [
2772
            SE_COURSE_ID => $course_id,
2773
            SE_TOOL_ID => TOOL_QUIZ,
2774
            SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->getId()],
2775
            SE_USER => (int) api_get_user_id(),
2776
        ];
2777
        $ic_slide->xapian_data = serialize($xapian_data);
2778
        $exercise_description = $all_specific_terms.' '.$this->description;
2779
        $ic_slide->addValue('content', $exercise_description);
2780
2781
        $di = new ChamiloIndexer();
2782
        isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
2783
        $di->connectDb(null, null, $lang);
2784
        $di->addChunk($ic_slide);
2785
2786
        //index and return search engine document id
2787
        $did = $di->index();
2788
        if ($did) {
2789
            // save it to db
2790
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2791
            $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2792
			    VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2793
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId(), $did);
2794
            Database::query($sql);
2795
        }
2796
    }
2797
2798
    public function search_engine_edit()
2799
    {
2800
        // update search enchine and its values table if enabled
2801
        if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
2802
            $course_id = api_get_course_id();
2803
2804
            // actually, it consists on delete terms from db,
2805
            // insert new ones, create a new search engine document, and remove the old one
2806
            // get search_did
2807
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2808
            $sql = 'SELECT * FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s LIMIT 1';
2809
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2810
            $res = Database::query($sql);
2811
2812
            if (Database::num_rows($res) > 0) {
2813
                $se_ref = Database::fetch_array($res);
2814
                $specific_fields = get_specific_field_list();
2815
                $ic_slide = new IndexableChunk();
2816
2817
                $all_specific_terms = '';
2818
                foreach ($specific_fields as $specific_field) {
2819
                    delete_all_specific_field_value($course_id, $specific_field['id'], TOOL_QUIZ, $this->getId());
2820
                    if (isset($_REQUEST[$specific_field['code']])) {
2821
                        $sterms = trim($_REQUEST[$specific_field['code']]);
2822
                        $all_specific_terms .= ' '.$sterms;
2823
                        $sterms = explode(',', $sterms);
2824
                        foreach ($sterms as $sterm) {
2825
                            $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2826
                            add_specific_field_value(
2827
                                $specific_field['id'],
2828
                                $course_id,
2829
                                TOOL_QUIZ,
2830
                                $this->getId(),
2831
                                $sterm
2832
                            );
2833
                        }
2834
                    }
2835
                }
2836
2837
                // build the chunk to index
2838
                $ic_slide->addValue('title', $this->exercise);
2839
                $ic_slide->addCourseId($course_id);
2840
                $ic_slide->addToolId(TOOL_QUIZ);
2841
                $xapian_data = [
2842
                    SE_COURSE_ID => $course_id,
2843
                    SE_TOOL_ID => TOOL_QUIZ,
2844
                    SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->getId()],
2845
                    SE_USER => (int) api_get_user_id(),
2846
                ];
2847
                $ic_slide->xapian_data = serialize($xapian_data);
2848
                $exercise_description = $all_specific_terms.' '.$this->description;
2849
                $ic_slide->addValue('content', $exercise_description);
2850
2851
                $di = new ChamiloIndexer();
2852
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
2853
                $di->connectDb(null, null, $lang);
2854
                $di->remove_document($se_ref['search_did']);
2855
                $di->addChunk($ic_slide);
2856
2857
                //index and return search engine document id
2858
                $did = $di->index();
2859
                if ($did) {
2860
                    // save it to db
2861
                    $sql = 'DELETE FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=\'%s\'';
2862
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2863
                    Database::query($sql);
2864
                    $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2865
                        VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2866
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId(), $did);
2867
                    Database::query($sql);
2868
                }
2869
            } else {
2870
                $this->search_engine_save();
2871
            }
2872
        }
2873
    }
2874
2875
    public function search_engine_delete()
2876
    {
2877
        // remove from search engine if enabled
2878
        if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
2879
            $course_id = api_get_course_id();
2880
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2881
            $sql = 'SELECT * FROM %s
2882
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
2883
                    LIMIT 1';
2884
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2885
            $res = Database::query($sql);
2886
            if (Database::num_rows($res) > 0) {
2887
                $row = Database::fetch_array($res);
2888
                $di = new ChamiloIndexer();
2889
                $di->remove_document($row['search_did']);
2890
                unset($di);
2891
                $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2892
                foreach ($this->questionList as $question_i) {
2893
                    $sql = 'SELECT type FROM %s WHERE id=%s';
2894
                    $sql = sprintf($sql, $tbl_quiz_question, $question_i);
2895
                    $qres = Database::query($sql);
2896
                    if (Database::num_rows($qres) > 0) {
2897
                        $qrow = Database::fetch_array($qres);
2898
                        $objQuestion = Question::getInstance($qrow['type']);
2899
                        $objQuestion = Question::read((int) $question_i);
2900
                        $objQuestion->search_engine_edit($this->getId(), false, true);
2901
                        unset($objQuestion);
2902
                    }
2903
                }
2904
            }
2905
            $sql = 'DELETE FROM %s
2906
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
2907
                    LIMIT 1';
2908
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2909
            Database::query($sql);
2910
2911
            // remove terms from db
2912
            delete_all_values_for_item($course_id, TOOL_QUIZ, $this->getId());
2913
        }
2914
    }
2915
2916
    public function selectExpiredTime()
2917
    {
2918
        return $this->expired_time;
2919
    }
2920
2921
    /**
2922
     * Cleans the student's results only for the Exercise tool (Not from the LP)
2923
     * The LP results are NOT deleted by default, otherwise put $cleanLpTests = true
2924
     * Works with exercises in sessions.
2925
     *
2926
     * @param bool   $cleanLpTests
2927
     * @param string $cleanResultBeforeDate
2928
     *
2929
     * @return int quantity of user's exercises deleted
2930
     */
2931
    public function cleanResults($cleanLpTests = false, $cleanResultBeforeDate = null)
2932
    {
2933
        $sessionId = api_get_session_id();
2934
        $table_track_e_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2935
        $table_track_e_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
2936
2937
        $sql_where = '  AND
2938
                        orig_lp_id = 0 AND
2939
                        orig_lp_item_id = 0';
2940
2941
        // if we want to delete results from LP too
2942
        if ($cleanLpTests) {
2943
            $sql_where = '';
2944
        }
2945
2946
        // if we want to delete attempts before date $cleanResultBeforeDate
2947
        // $cleanResultBeforeDate must be a valid UTC-0 date yyyy-mm-dd
2948
        if (!empty($cleanResultBeforeDate)) {
2949
            $cleanResultBeforeDate = Database::escape_string($cleanResultBeforeDate);
2950
            if (api_is_valid_date($cleanResultBeforeDate)) {
2951
                $sql_where .= "  AND exe_date <= '$cleanResultBeforeDate' ";
2952
            } else {
2953
                return 0;
2954
            }
2955
        }
2956
2957
        $sessionCondition = api_get_session_condition($sessionId);
2958
        $sql = "SELECT exe_id
2959
                FROM $table_track_e_exercises
2960
                WHERE
2961
                    c_id = ".api_get_course_int_id().' AND
2962
                    exe_exo_id = '.$this->getId()."
2963
                    $sessionCondition
2964
                    $sql_where";
2965
2966
        $result = Database::query($sql);
2967
        $exe_list = Database::store_result($result);
2968
2969
        // deleting TRACK_E_ATTEMPT table
2970
        // check if exe in learning path or not
2971
        $i = 0;
2972
        if (is_array($exe_list) && count($exe_list) > 0) {
2973
            foreach ($exe_list as $item) {
2974
                $sql = "DELETE FROM $table_track_e_attempt
2975
                        WHERE exe_id = '".$item['exe_id']."'";
2976
                Database::query($sql);
2977
                $i++;
2978
            }
2979
        }
2980
2981
        // delete TRACK_E_EXERCISES table
2982
        $sql = "DELETE FROM $table_track_e_exercises
2983
                WHERE
2984
                  c_id = ".api_get_course_int_id().' AND
2985
                  exe_exo_id = '.$this->getId()." $sql_where $sessionCondition";
2986
        Database::query($sql);
2987
2988
        $this->generateStats($this->getId(), api_get_course_info(), $sessionId);
2989
2990
        Event::addEvent(
2991
            LOG_EXERCISE_RESULT_DELETE,
2992
            LOG_EXERCISE_ID,
2993
            $this->getId(),
2994
            null,
2995
            null,
2996
            api_get_course_int_id(),
2997
            $sessionId
2998
        );
2999
3000
        return $i;
3001
    }
3002
3003
    /**
3004
     * Copies an exercise (duplicate all questions and answers).
3005
     */
3006
    public function copyExercise()
3007
    {
3008
        $exerciseObject = $this;
3009
        $categories = $exerciseObject->getCategoriesInExercise(true);
3010
        // Get all questions no matter the order/category settings
3011
        $questionList = $exerciseObject->getQuestionOrderedList();
3012
        $sourceId = $exerciseObject->iId;
3013
        // Force the creation of a new exercise
3014
        $exerciseObject->updateTitle($exerciseObject->selectTitle().' - '.get_lang('Copy'));
3015
        $exerciseObject->iId = 0;
3016
        $exerciseObject->sessionId = api_get_session_id();
3017
        $courseId = api_get_course_int_id();
3018
        $exerciseObject->save();
3019
        $newId = $exerciseObject->getId();
3020
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
3021
3022
        $count = 1;
3023
        $batchSize = 20;
3024
        $em = Database::getManager();
3025
        if ($newId && !empty($questionList)) {
3026
            $extraField = new ExtraFieldValue('exercise');
3027
            $extraField->copy($sourceId, $newId);
3028
            // Question creation
3029
            foreach ($questionList as $oldQuestionId) {
3030
                $oldQuestionObj = Question::read($oldQuestionId, null, false);
3031
                $newQuestionId = $oldQuestionObj->duplicate();
3032
                if ($newQuestionId) {
3033
                    $newQuestionObj = Question::read($newQuestionId, null, false);
3034
                    if (isset($newQuestionObj) && $newQuestionObj) {
3035
                        $sql = "INSERT INTO $exerciseRelQuestionTable (question_id, quiz_id, question_order)
3036
                                VALUES (".$newQuestionId.", ".$newId.", '$count')";
3037
                        Database::query($sql);
3038
                        $count++;
3039
                        if (!empty($oldQuestionObj->category)) {
3040
                            $newQuestionObj->saveCategory($oldQuestionObj->category);
3041
                        }
3042
3043
                        // This should be moved to the duplicate function
3044
                        $newAnswerObj = new Answer($oldQuestionId, $courseId, $exerciseObject);
3045
                        $newAnswerObj->read();
3046
                        $newAnswerObj->duplicate($newQuestionObj);
3047
                        if (($count % $batchSize) === 0) {
3048
                            $em->clear(); // Detaches all objects from Doctrine!
3049
                        }
3050
                    }
3051
                }
3052
            }
3053
            if (!empty($categories)) {
3054
                $newCategoryList = [];
3055
                foreach ($categories as $category) {
3056
                    $newCategoryList[$category['category_id']] = $category['count_questions'];
3057
                }
3058
                $exerciseObject->saveCategoriesInExercise($newCategoryList);
3059
            }
3060
        }
3061
    }
3062
3063
    /**
3064
     * Changes the exercise status.
3065
     *
3066
     * @param string $status - exercise status
3067
     */
3068
    public function updateStatus($status)
3069
    {
3070
        $this->active = $status;
3071
    }
3072
3073
    /**
3074
     * @param int    $lp_id
3075
     * @param int    $lp_item_id
3076
     * @param int    $lp_item_view_id
3077
     * @param string $status
3078
     *
3079
     * @return array
3080
     */
3081
    public function get_stat_track_exercise_info(
3082
        $lp_id = 0,
3083
        $lp_item_id = 0,
3084
        $lp_item_view_id = 0,
3085
        $status = 'incomplete'
3086
    ) {
3087
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3088
        $lp_id = (int) $lp_id;
3089
        $lp_item_id = (int) $lp_item_id;
3090
        $lp_item_view_id = (int) $lp_item_view_id;
3091
3092
        $sessionCondition = api_get_session_condition(api_get_session_id());
3093
        $condition = " WHERE exe_exo_id 	= ".$this->getId()." AND
3094
					   exe_user_id 			= '".api_get_user_id()."' AND
3095
					   c_id                 = ".api_get_course_int_id()." AND
3096
					   status 				= '".Database::escape_string($status)."' AND
3097
					   orig_lp_id 			= $lp_id AND
3098
					   orig_lp_item_id 		= $lp_item_id AND
3099
                       orig_lp_item_view_id =  $lp_item_view_id
3100
					   ";
3101
3102
        $sql_track = " SELECT * FROM  $track_exercises $condition $sessionCondition LIMIT 1 ";
3103
3104
        $result = Database::query($sql_track);
3105
        $new_array = [];
3106
        if (Database::num_rows($result) > 0) {
3107
            $new_array = Database::fetch_assoc($result);
3108
            $new_array['num_exe'] = Database::num_rows($result);
3109
        }
3110
3111
        return $new_array;
3112
    }
3113
3114
    /**
3115
     * Saves a test attempt.
3116
     *
3117
     * @param int   $clock_expired_time   clock_expired_time
3118
     * @param int   $safe_lp_id           lp id
3119
     * @param int   $safe_lp_item_id      lp item id
3120
     * @param int   $safe_lp_item_view_id lp item_view id
3121
     * @param array $questionList
3122
     * @param float $weight
3123
     *
3124
     * @throws \Doctrine\ORM\ORMException
3125
     * @throws \Doctrine\ORM\OptimisticLockException
3126
     * @throws \Doctrine\ORM\TransactionRequiredException
3127
     *
3128
     * @return int
3129
     */
3130
    public function save_stat_track_exercise_info(
3131
        $clock_expired_time,
3132
        $safe_lp_id = 0,
3133
        $safe_lp_item_id = 0,
3134
        $safe_lp_item_view_id = 0,
3135
        $questionList = [],
3136
        $weight = 0
3137
    ) {
3138
        $safe_lp_id = (int) $safe_lp_id;
3139
        $safe_lp_item_id = (int) $safe_lp_item_id;
3140
        $safe_lp_item_view_id = (int) $safe_lp_item_view_id;
3141
3142
        if (empty($clock_expired_time)) {
3143
            $clock_expired_time = null;
3144
        }
3145
3146
        $questionList = array_filter(
3147
            $questionList,
3148
            function (int $qid) {
3149
                $q = Question::read($qid);
3150
                return $q
3151
                    && !in_array(
3152
                        $q->type,
3153
                        [PAGE_BREAK, MEDIA_QUESTION],
3154
                        true
3155
                    );
3156
            }
3157
        );
3158
3159
        $questionList = array_map('intval', $questionList);
3160
        $em = Database::getManager();
3161
3162
        $quiz = $em->find(CQuiz::class, $this->getId());
3163
3164
        $trackExercise = (new TrackEExercise())
3165
            ->setSession(api_get_session_entity())
3166
            ->setCourse(api_get_course_entity())
3167
            ->setMaxScore($weight)
3168
            ->setDataTracking(implode(',', $questionList))
3169
            ->setUser(api_get_user_entity())
3170
            ->setUserIp(api_get_real_ip())
3171
            ->setOrigLpId($safe_lp_id)
3172
            ->setOrigLpItemId($safe_lp_item_id)
3173
            ->setOrigLpItemViewId($safe_lp_item_view_id)
3174
            ->setExpiredTimeControl($clock_expired_time)
3175
            ->setQuiz($quiz)
3176
        ;
3177
        $em->persist($trackExercise);
3178
        $em->flush();
3179
3180
        return $trackExercise->getExeId();
3181
    }
3182
3183
    /**
3184
     * @param int    $question_id
3185
     * @param int    $questionNum
3186
     * @param array  $questions_in_media
3187
     * @param string $currentAnswer
3188
     * @param array  $myRemindList
3189
     * @param bool   $showPreviousButton
3190
     *
3191
     * @return string
3192
     */
3193
    public function show_button(
3194
        $question_id,
3195
        $questionNum,
3196
        $questions_in_media = [],
3197
        $currentAnswer = '',
3198
        $myRemindList = [],
3199
        $showPreviousButton = true
3200
    ) {
3201
        global $safe_lp_id, $safe_lp_item_id, $safe_lp_item_view_id;
3202
        $nbrQuestions = $this->countQuestionsInExercise();
3203
        $buttonList = [];
3204
        $html = $label = '';
3205
        $hotspotGet = isset($_POST['hotspot']) ? Security::remove_XSS($_POST['hotspot']) : null;
3206
3207
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]) &&
3208
            ONE_PER_PAGE == $this->type
3209
        ) {
3210
            $urlTitle = get_lang('Proceed with the test');
3211
            if ($questionNum == count($this->questionList)) {
3212
                $urlTitle = get_lang('End test');
3213
            }
3214
3215
            $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit_modal.php?'.api_get_cidreq();
3216
            $url .= '&'.http_build_query(
3217
                    [
3218
                        'learnpath_id' => $safe_lp_id,
3219
                        'learnpath_item_id' => $safe_lp_item_id,
3220
                        'learnpath_item_view_id' => $safe_lp_item_view_id,
3221
                        'hotspot' => $hotspotGet,
3222
                        'nbrQuestions' => $nbrQuestions,
3223
                        'num' => $questionNum,
3224
                        'exerciseType' => $this->type,
3225
                        'exerciseId' => $this->getId(),
3226
                        'reminder' => empty($myRemindList) ? null : 2,
3227
                        'tryagain' => isset($_REQUEST['tryagain']) && 1 === (int) $_REQUEST['tryagain'] ? 1 : 0,
3228
                    ]
3229
                );
3230
3231
            $params = [
3232
                'class' => 'ajax btn btn--plain no-close-button',
3233
                'data-title' => Security::remove_XSS(get_lang('Comment')),
3234
                'data-size' => 'md',
3235
                'id' => "button_$question_id",
3236
            ];
3237
3238
            if (EXERCISE_FEEDBACK_TYPE_POPUP === $this->getFeedbackType()) {
3239
                //$params['data-block-div-after-closing'] = "question_div_$question_id";
3240
                $params['data-block-closing'] = 'true';
3241
                $params['class'] .= ' no-header ';
3242
            }
3243
3244
            $html .= Display::url($urlTitle, $url, $params);
3245
            $html .= '<br />';
3246
3247
            return $html;
3248
        }
3249
3250
        if (!api_is_allowed_to_session_edit()) {
3251
            return '';
3252
        }
3253
3254
        $isReviewingAnswers = isset($_REQUEST['reminder']) && 2 == $_REQUEST['reminder'];
3255
3256
        // User
3257
        $endReminderValue = false;
3258
        if (!empty($myRemindList) && $isReviewingAnswers) {
3259
            $endValue = end($myRemindList);
3260
            if ($endValue == $question_id) {
3261
                $endReminderValue = true;
3262
            }
3263
        }
3264
        $endTest = false;
3265
        if (ALL_ON_ONE_PAGE == $this->type || $nbrQuestions == $questionNum || $endReminderValue) {
3266
            if ($this->review_answers) {
3267
                $label = get_lang('Review selected questions');
3268
                $class = 'btn btn--success';
3269
            } else {
3270
                $endTest = true;
3271
                $label = get_lang('End test');
3272
                $class = 'btn btn--warning';
3273
            }
3274
        } else {
3275
            $label = get_lang('Next question');
3276
            $class = 'btn btn--primary';
3277
        }
3278
        // used to select it with jquery
3279
        $class .= ' question-validate-btn';
3280
        if (ONE_PER_PAGE == $this->type) {
3281
            if (1 != $questionNum && $this->showPreviousButton()) {
3282
                $prev_question = $questionNum - 2;
3283
                $showPreview = true;
3284
                if (!empty($myRemindList) && $isReviewingAnswers) {
3285
                    $beforeId = null;
3286
                    for ($i = 0; $i < count($myRemindList); $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...
3287
                        if (isset($myRemindList[$i]) && $myRemindList[$i] == $question_id) {
3288
                            $beforeId = isset($myRemindList[$i - 1]) ? $myRemindList[$i - 1] : null;
3289
3290
                            break;
3291
                        }
3292
                    }
3293
3294
                    if (empty($beforeId)) {
3295
                        $showPreview = false;
3296
                    } else {
3297
                        $num = 0;
3298
                        foreach ($this->questionList as $originalQuestionId) {
3299
                            if ($originalQuestionId == $beforeId) {
3300
                                break;
3301
                            }
3302
                            $num++;
3303
                        }
3304
                        $prev_question = $num;
3305
                    }
3306
                }
3307
3308
                if ($showPreviousButton && $showPreview && 0 === $this->getPreventBackwards()) {
3309
                    $buttonList[] = Display::button(
3310
                        'previous_question_and_save',
3311
                        get_lang('Previous question'),
3312
                        [
3313
                            'type' => 'button',
3314
                            'class' => 'btn btn--plain',
3315
                            'data-prev' => $prev_question,
3316
                            'data-question' => $question_id,
3317
                        ]
3318
                    );
3319
                }
3320
            }
3321
3322
            // Next question
3323
            if (!empty($questions_in_media)) {
3324
                $buttonList[] = Display::button(
3325
                    'save_question_list',
3326
                    $label,
3327
                    [
3328
                        'type' => 'button',
3329
                        'class' => $class,
3330
                        'data-list' => implode(',', $questions_in_media),
3331
                    ]
3332
                );
3333
            } else {
3334
                $attributes = ['type' => 'button', 'class' => $class, 'data-question' => $question_id];
3335
                $name = 'save_now';
3336
                if ($endTest && api_get_configuration_value('quiz_check_all_answers_before_end_test')) {
3337
                    $name = 'check_answers';
3338
                }
3339
                $buttonList[] = Display::button(
3340
                    $name,
3341
                    $label,
3342
                    $attributes
3343
                );
3344
            }
3345
            $buttonList[] = '<span id="save_for_now_'.$question_id.'" class="exercise_save_mini_message"></span>';
3346
3347
            $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3348
3349
            return $html;
3350
        }
3351
3352
        if ($this->review_answers) {
3353
            $all_label = get_lang('Review selected questions');
3354
            $class = 'btn btn--success';
3355
        } else {
3356
            $all_label = get_lang('End test');
3357
            $class = 'btn btn--warning';
3358
        }
3359
        // used to select it with jquery
3360
        $class .= ' question-validate-btn';
3361
        $buttonList[] = Display::button(
3362
            'validate_all',
3363
            $all_label,
3364
            ['type' => 'button', 'class' => $class]
3365
        );
3366
        $buttonList[] = Display::span(null, ['id' => 'save_all_response']);
3367
        $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3368
3369
        return $html;
3370
    }
3371
3372
    /**
3373
     * @param int    $timeLeft in seconds
3374
     * @param string $url
3375
     *
3376
     * @return string
3377
     */
3378
    public function showSimpleTimeControl($timeLeft, $url = '')
3379
    {
3380
        $timeLeft = (int) $timeLeft;
3381
3382
        return "<script>
3383
            function openClockWarning() {
3384
                $('#clock_warning').dialog({
3385
                    modal:true,
3386
                    height:320,
3387
                    width:550,
3388
                    closeOnEscape: false,
3389
                    resizable: false,
3390
                    buttons: {
3391
                        '".addslashes(get_lang('Close'))."': function() {
3392
                            $('#clock_warning').dialog('close');
3393
                        }
3394
                    },
3395
                    close: function() {
3396
                        window.location.href = '$url';
3397
                    }
3398
                });
3399
                $('#clock_warning').dialog('open');
3400
                $('#counter_to_redirect').epiclock({
3401
                    mode: $.epiclock.modes.countdown,
3402
                    offset: {seconds: 5},
3403
                    format: 's'
3404
                }).bind('timer', function () {
3405
                    window.location.href = '$url';
3406
                });
3407
            }
3408
3409
            function onExpiredTimeExercise() {
3410
                $('#wrapper-clock').hide();
3411
                $('#expired-message-id').show();
3412
                // Fixes bug #5263
3413
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3414
                openClockWarning();
3415
            }
3416
3417
			$(function() {
3418
				// time in seconds when using minutes there are some seconds lost
3419
                var time_left = parseInt(".$timeLeft.");
3420
                $('#exercise_clock_warning').epiclock({
3421
                    mode: $.epiclock.modes.countdown,
3422
                    offset: {seconds: time_left},
3423
                    format: 'x:i:s',
3424
                    renderer: 'minute'
3425
                }).bind('timer', function () {
3426
                    onExpiredTimeExercise();
3427
                });
3428
	       		$('#submit_save').click(function () {});
3429
	        });
3430
	    </script>";
3431
    }
3432
3433
    /**
3434
     * So the time control will work.
3435
     *
3436
     * @param int    $timeLeft
3437
     * @param string $redirectToUrl
3438
     *
3439
     * @return string
3440
     */
3441
    public function showTimeControlJS($timeLeft, $redirectToUrl = '')
3442
    {
3443
        $timeLeft = (int) $timeLeft;
3444
        $script = 'redirectExerciseToResult();';
3445
        if (ALL_ON_ONE_PAGE == $this->type) {
3446
            $script = "save_now_all('validate');";
3447
        } elseif (ONE_PER_PAGE == $this->type) {
3448
            $script = 'window.quizTimeEnding = true;
3449
                $(\'[name="save_now"]\').trigger(\'click\');';
3450
        }
3451
3452
        $exerciseSubmitRedirect = '';
3453
        if (!empty($redirectToUrl)) {
3454
            $exerciseSubmitRedirect = "window.location = '$redirectToUrl'";
3455
        }
3456
3457
        return "<script>
3458
            function openClockWarning() {
3459
                $('#clock_warning').dialog({
3460
                    modal:true,
3461
                    height:320,
3462
                    width:550,
3463
                    closeOnEscape: false,
3464
                    resizable: false,
3465
                    buttons: {
3466
                        '".addslashes(get_lang('End test'))."': function() {
3467
                            $('#clock_warning').dialog('close');
3468
                        }
3469
                    },
3470
                    close: function() {
3471
                        send_form();
3472
                    }
3473
                });
3474
3475
                $('#clock_warning').dialog('open');
3476
                $('#counter_to_redirect').epiclock({
3477
                    mode: $.epiclock.modes.countdown,
3478
                    offset: {seconds: 5},
3479
                    format: 's'
3480
                }).bind('timer', function () {
3481
                    send_form();
3482
                });
3483
            }
3484
3485
            function send_form() {
3486
                if ($('#exercise_form').length) {
3487
                    $script
3488
                } else {
3489
                    $exerciseSubmitRedirect
3490
                    // In exercise_reminder.php
3491
                    final_submit();
3492
                }
3493
            }
3494
3495
            function onExpiredTimeExercise() {
3496
                $('#wrapper-clock').hide();
3497
                $('#expired-message-id').show();
3498
                // Fixes bug #5263
3499
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3500
                openClockWarning();
3501
            }
3502
3503
			$(function() {
3504
				// time in seconds when using minutes there are some seconds lost
3505
                var time_left = parseInt(".$timeLeft.");
3506
                $('#exercise_clock_warning').epiclock({
3507
                    mode: $.epiclock.modes.countdown,
3508
                    offset: {seconds: time_left},
3509
                    format: 'x:C:s',
3510
                    renderer: 'minute'
3511
                }).bind('timer', function () {
3512
                    onExpiredTimeExercise();
3513
                });
3514
	       		$('#submit_save').click(function () {});
3515
	        });
3516
	    </script>";
3517
    }
3518
3519
    /**
3520
     * This function was originally found in the exercise_show.php.
3521
     *
3522
     * @param int    $exeId
3523
     * @param int    $questionId
3524
     * @param mixed  $choice                                    the user-selected option
3525
     * @param string $from                                      function is called from 'exercise_show' or
3526
     *                                                          'exercise_result'
3527
     * @param array  $exerciseResultCoordinates                 the hotspot coordinates $hotspot[$question_id] =
3528
     *                                                          coordinates
3529
     * @param bool   $save_results                              save results in the DB or just show the response
3530
     * @param bool   $from_database                             gets information from DB or from the current selection
3531
     * @param bool   $show_result                               show results or not
3532
     * @param int    $propagate_neg
3533
     * @param array  $hotspot_delineation_result
3534
     * @param bool   $showTotalScoreAndUserChoicesInLastAttempt
3535
     * @param bool   $updateResults
3536
     * @param bool   $showHotSpotDelineationTable
3537
     * @param int    $questionDuration                          seconds
3538
     *
3539
     * @return string html code
3540
     *
3541
     * @todo    reduce parameters of this function
3542
     */
3543
    public function manage_answer(
3544
        $exeId,
3545
        $questionId,
3546
        $choice,
3547
        $from = 'exercise_show',
3548
        $exerciseResultCoordinates = [],
3549
        $save_results = true,
3550
        $from_database = false,
3551
        $show_result = true,
3552
        $propagate_neg = 0,
3553
        $hotspot_delineation_result = [],
3554
        $showTotalScoreAndUserChoicesInLastAttempt = true,
3555
        $updateResults = false,
3556
        $showHotSpotDelineationTable = false,
3557
        $questionDuration = 0
3558
    ) {
3559
        $debug = false;
3560
        //needed in order to use in the exercise_attempt() for the time
3561
        global $learnpath_id, $learnpath_item_id;
3562
        require_once api_get_path(LIBRARY_PATH).'geometry.lib.php';
3563
        $em = Database::getManager();
3564
        $feedback_type = $this->getFeedbackType();
3565
        $results_disabled = $this->selectResultsDisabled();
3566
        $questionDuration = (int) $questionDuration;
3567
3568
        if ($debug) {
3569
            error_log('<------ manage_answer ------> ');
3570
            error_log('exe_id: '.$exeId);
3571
            error_log('$from:  '.$from);
3572
            error_log('$save_results: '.(int) $save_results);
3573
            error_log('$from_database: '.(int) $from_database);
3574
            error_log('$show_result: '.(int) $show_result);
3575
            error_log('$propagate_neg: '.$propagate_neg);
3576
            error_log('$exerciseResultCoordinates: '.print_r($exerciseResultCoordinates, 1));
3577
            error_log('$hotspot_delineation_result: '.print_r($hotspot_delineation_result, 1));
3578
            error_log('$learnpath_id: '.$learnpath_id);
3579
            error_log('$learnpath_item_id: '.$learnpath_item_id);
3580
            error_log('$choice: '.print_r($choice, 1));
3581
            error_log('-----------------------------');
3582
        }
3583
3584
        $final_overlap = 0;
3585
        $final_missing = 0;
3586
        $final_excess = 0;
3587
        $overlap_color = 0;
3588
        $missing_color = 0;
3589
        $excess_color = 0;
3590
        $threadhold1 = 0;
3591
        $threadhold2 = 0;
3592
        $threadhold3 = 0;
3593
        $arrques = null;
3594
        $arrans = null;
3595
        $studentChoice = null;
3596
        $expectedAnswer = '';
3597
        $calculatedChoice = '';
3598
        $calculatedStatus = '';
3599
        $questionId = (int) $questionId;
3600
        $exeId = (int) $exeId;
3601
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3602
        $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
3603
        $studentChoiceDegree = null;
3604
3605
        // Creates a temporary Question object
3606
        $course_id = $this->course_id;
3607
        $objQuestionTmp = Question::read($questionId, $this->course);
3608
3609
        if (false === $objQuestionTmp) {
3610
            return false;
3611
        }
3612
3613
        $questionName = $objQuestionTmp->selectTitle();
3614
        $questionWeighting = $objQuestionTmp->selectWeighting();
3615
        $answerType = $objQuestionTmp->selectType();
3616
        $quesId = $objQuestionTmp->getId();
3617
        $extra = $objQuestionTmp->extra;
3618
        $next = 1; //not for now
3619
        $totalWeighting = 0;
3620
        $totalScore = 0;
3621
3622
        // Extra information of the question
3623
        if ((
3624
                MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
3625
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
3626
            )
3627
            && !empty($extra)
3628
        ) {
3629
            $extra = explode(':', $extra);
3630
            // Fixes problems with negatives values using intval
3631
            $true_score = (float) trim($extra[0]);
3632
            $false_score = (float) trim($extra[1]);
3633
            $doubt_score = (float) trim($extra[2]);
3634
        }
3635
3636
        // Construction of the Answer object
3637
        $objAnswerTmp = new Answer($questionId, $course_id);
3638
        $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
3639
3640
        if ($debug) {
3641
            error_log('Count of possible answers: '.$nbrAnswers);
3642
            error_log('$answerType: '.$answerType);
3643
        }
3644
3645
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
3646
            $choiceTmp = $choice;
3647
            $choice = isset($choiceTmp['choice']) ? $choiceTmp['choice'] : '';
3648
            $choiceDegreeCertainty = isset($choiceTmp['choiceDegreeCertainty']) ? $choiceTmp['choiceDegreeCertainty'] : '';
3649
        }
3650
3651
        if (FREE_ANSWER == $answerType ||
3652
            ORAL_EXPRESSION == $answerType ||
3653
            CALCULATED_ANSWER == $answerType ||
3654
            ANNOTATION == $answerType
3655
        ) {
3656
            $nbrAnswers = 1;
3657
        }
3658
3659
        $generatedFilesHtml = '';
3660
        if ($answerType == ORAL_EXPRESSION) {
3661
            $generatedFilesHtml = ExerciseLib::getOralFileAudio($exeId, $questionId);
3662
        }
3663
3664
        $user_answer = '';
3665
        // Get answer list for matching
3666
        $sql = "SELECT iid, answer
3667
                FROM $table_ans
3668
                WHERE question_id = $questionId";
3669
        $res_answer = Database::query($sql);
3670
3671
        $answerMatching = [];
3672
        while ($real_answer = Database::fetch_array($res_answer)) {
3673
            $answerMatching[$real_answer['iid']] = $real_answer['answer'];
3674
        }
3675
3676
        // Get first answer needed for global question, no matter the answer shuffle option;
3677
        $firstAnswer = [];
3678
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
3679
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
3680
        ) {
3681
            $sql = "SELECT *
3682
                    FROM $table_ans
3683
                    WHERE question_id = $questionId
3684
                    ORDER BY position
3685
                    LIMIT 1";
3686
            $result = Database::query($sql);
3687
            if (Database::num_rows($result)) {
3688
                $firstAnswer = Database::fetch_array($result);
3689
            }
3690
        }
3691
3692
        $real_answers = [];
3693
        $quiz_question_options = Question::readQuestionOption($questionId, $course_id);
3694
3695
        $organs_at_risk_hit = 0;
3696
        $questionScore = 0;
3697
        $orderedHotSpots = [];
3698
        if (HOT_SPOT == $answerType || ANNOTATION == $answerType) {
3699
            $orderedHotSpots = $em->getRepository(TrackEHotspot::class)->findBy(
3700
                [
3701
                    'hotspotQuestionId' => $questionId,
3702
                    'course' => $course_id,
3703
                    'hotspotExeId' => $exeId,
3704
                ],
3705
                ['hotspotAnswerId' => 'ASC']
3706
            );
3707
        }
3708
3709
        if ($debug) {
3710
            error_log('-- Start answer loop --');
3711
        }
3712
3713
        $answerDestination = null;
3714
        $userAnsweredQuestion = false;
3715
        $correctAnswerId = [];
3716
        for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
3717
            $answer = $objAnswerTmp->selectAnswer($answerId);
3718
            $answerComment = $objAnswerTmp->selectComment($answerId);
3719
            $answerCorrect = $objAnswerTmp->isCorrect($answerId);
3720
            $answerWeighting = (float) $objAnswerTmp->selectWeighting($answerId);
3721
            $answerAutoId = $objAnswerTmp->selectAutoId($answerId);
3722
            $answerIid = isset($objAnswerTmp->iid[$answerId]) ? (int) $objAnswerTmp->iid[$answerId] : 0;
3723
3724
            if ($debug) {
3725
                error_log("c_quiz_answer.id_auto: $answerAutoId ");
3726
                error_log("Answer marked as correct in db (0/1)?: $answerCorrect ");
3727
                error_log("answerWeighting: $answerWeighting");
3728
            }
3729
3730
            // Delineation
3731
            $delineation_cord = $objAnswerTmp->selectHotspotCoordinates(1);
3732
            $answer_delineation_destination = $objAnswerTmp->selectDestination(1);
3733
3734
            switch ($answerType) {
3735
                case UNIQUE_ANSWER:
3736
                case UNIQUE_ANSWER_IMAGE:
3737
                case UNIQUE_ANSWER_NO_OPTION:
3738
                case READING_COMPREHENSION:
3739
                    if ($from_database) {
3740
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3741
                                WHERE
3742
                                    exe_id = $exeId AND
3743
                                    question_id = $questionId";
3744
                        $result = Database::query($sql);
3745
                        $choice = Database::result($result, 0, 'answer');
3746
3747
                        if (false === $userAnsweredQuestion) {
3748
                            $userAnsweredQuestion = !empty($choice);
3749
                        }
3750
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3751
                        if ($studentChoice) {
3752
                            $questionScore += $answerWeighting;
3753
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3754
                            $correctAnswerId[] = $answerId;
3755
                        }
3756
                    } else {
3757
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3758
                        if ($studentChoice) {
3759
                            $questionScore += $answerWeighting;
3760
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3761
                            $correctAnswerId[] = $answerId;
3762
                        }
3763
                    }
3764
3765
                    break;
3766
                case MULTIPLE_ANSWER_TRUE_FALSE:
3767
                    if ($from_database) {
3768
                        $choice = [];
3769
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3770
                                WHERE
3771
                                    exe_id = $exeId AND
3772
                                    question_id = ".$questionId;
3773
3774
                        $result = Database::query($sql);
3775
                        while ($row = Database::fetch_array($result)) {
3776
                            $values = explode(':', $row['answer']);
3777
                            $my_answer_id = isset($values[0]) ? $values[0] : '';
3778
                            $option = isset($values[1]) ? $values[1] : '';
3779
                            $choice[$my_answer_id] = $option;
3780
                        }
3781
                        $userAnsweredQuestion = !empty($choice);
3782
                    }
3783
3784
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3785
                    if (!empty($studentChoice)) {
3786
                        $correctAnswerId[] = $answerAutoId;
3787
                        if ($studentChoice == $answerCorrect) {
3788
                            $questionScore += $true_score;
3789
                        } else {
3790
                            if (isset($quiz_question_options[$studentChoice])
3791
                                && in_array($quiz_question_options[$studentChoice]['name'], ["Don't know", 'DoubtScore'])
3792
                            ) {
3793
                                $questionScore += $doubt_score;
3794
                            } else {
3795
                                $questionScore += $false_score;
3796
                            }
3797
                        }
3798
                    } else {
3799
                        // If no result then the user just hit don't know
3800
                        $studentChoice = 3;
3801
                        $questionScore += $doubt_score;
3802
                    }
3803
                    $totalScore = $questionScore;
3804
3805
                    break;
3806
                case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
3807
                    if ($from_database) {
3808
                        $choice = [];
3809
                        $choiceDegreeCertainty = [];
3810
                        $sql = "SELECT answer
3811
                                FROM $TBL_TRACK_ATTEMPT
3812
                                WHERE exe_id = $exeId AND question_id = $questionId";
3813
3814
                        $result = Database::query($sql);
3815
                        while ($row = Database::fetch_array($result)) {
3816
                            $ind = $row['answer'];
3817
                            $values = explode(':', $ind);
3818
                            $myAnswerId = $values[0] ?? null;
3819
                            $option = $values[1] ?? null;
3820
                            $percent = $values[2] ?? null;
3821
                            $choice[$myAnswerId] = $option;
3822
                            $choiceDegreeCertainty[$myAnswerId] = $percent;
3823
                        }
3824
                    }
3825
3826
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3827
                    $studentChoiceDegree = isset($choiceDegreeCertainty[$answerAutoId]) ? $choiceDegreeCertainty[$answerAutoId] : null;
3828
3829
                    // student score update
3830
                    if (!empty($studentChoice)) {
3831
                        if ($studentChoice == $answerCorrect) {
3832
                            // correct answer and student is Unsure or PrettySur
3833
                            if (isset($quiz_question_options[$studentChoiceDegree]) &&
3834
                                $quiz_question_options[$studentChoiceDegree]['position'] >= 3 &&
3835
                                $quiz_question_options[$studentChoiceDegree]['position'] < 9
3836
                            ) {
3837
                                $questionScore += $true_score;
3838
                            } else {
3839
                                // student ignore correct answer
3840
                                $questionScore += $doubt_score;
3841
                            }
3842
                        } else {
3843
                            // false answer and student is Unsure or PrettySur
3844
                            if (isset($quiz_question_options[$studentChoiceDegree]) && $quiz_question_options[$studentChoiceDegree]['position'] >= 3
3845
                                && $quiz_question_options[$studentChoiceDegree]['position'] < 9) {
3846
                                $questionScore += $false_score;
3847
                            } else {
3848
                                // student ignore correct answer
3849
                                $questionScore += $doubt_score;
3850
                            }
3851
                        }
3852
                    }
3853
                    $totalScore = $questionScore;
3854
3855
                    break;
3856
                case MULTIPLE_ANSWER:
3857
                    if ($from_database) {
3858
                        $choice = [];
3859
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3860
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3861
                        $resultans = Database::query($sql);
3862
                        while ($row = Database::fetch_array($resultans)) {
3863
                            $choice[$row['answer']] = 1;
3864
                        }
3865
3866
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3867
                        $real_answers[$answerId] = (bool) $studentChoice;
3868
3869
                        if ($studentChoice) {
3870
                            $questionScore += $answerWeighting;
3871
                        }
3872
                    } else {
3873
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3874
                        $real_answers[$answerId] = (bool) $studentChoice;
3875
3876
                        if (isset($studentChoice)) {
3877
                            $correctAnswerId[] = $answerAutoId;
3878
                            $questionScore += $answerWeighting;
3879
                        }
3880
                    }
3881
                    $totalScore += $answerWeighting;
3882
3883
                    break;
3884
                case GLOBAL_MULTIPLE_ANSWER:
3885
                    if ($from_database) {
3886
                        $choice = [];
3887
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3888
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3889
                        $resultans = Database::query($sql);
3890
                        while ($row = Database::fetch_array($resultans)) {
3891
                            $choice[$row['answer']] = 1;
3892
                        }
3893
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3894
                        $real_answers[$answerId] = (bool) $studentChoice;
3895
                        if ($studentChoice) {
3896
                            $questionScore += $answerWeighting;
3897
                        }
3898
                    } else {
3899
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3900
                        if (isset($studentChoice)) {
3901
                            $questionScore += $answerWeighting;
3902
                        }
3903
                        $real_answers[$answerId] = (bool) $studentChoice;
3904
                    }
3905
                    $totalScore += $answerWeighting;
3906
                    if ($debug) {
3907
                        error_log("studentChoice: $studentChoice");
3908
                    }
3909
3910
                    break;
3911
                case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
3912
                    if ($from_database) {
3913
                        $choice = [];
3914
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3915
                                WHERE exe_id = $exeId AND question_id = $questionId";
3916
                        $resultans = Database::query($sql);
3917
                        while ($row = Database::fetch_array($resultans)) {
3918
                            $result = explode(':', $row['answer']);
3919
                            if (isset($result[0])) {
3920
                                $my_answer_id = isset($result[0]) ? $result[0] : '';
3921
                                $option = isset($result[1]) ? $result[1] : '';
3922
                                $choice[$my_answer_id] = $option;
3923
                            }
3924
                        }
3925
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3926
3927
                        $real_answers[$answerId] = false;
3928
                        if ($answerCorrect == $studentChoice) {
3929
                            $real_answers[$answerId] = true;
3930
                        }
3931
                    } else {
3932
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3933
                        $real_answers[$answerId] = false;
3934
                        if ($answerCorrect == $studentChoice) {
3935
                            $real_answers[$answerId] = true;
3936
                        }
3937
                    }
3938
3939
                    break;
3940
                case MULTIPLE_ANSWER_COMBINATION:
3941
                    if ($from_database) {
3942
                        $choice = [];
3943
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3944
                                WHERE exe_id = $exeId AND question_id = $questionId";
3945
                        $resultans = Database::query($sql);
3946
                        while ($row = Database::fetch_array($resultans)) {
3947
                            $choice[$row['answer']] = 1;
3948
                        }
3949
3950
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3951
                        if (1 == $answerCorrect) {
3952
                            $real_answers[$answerId] = false;
3953
                            if ($studentChoice) {
3954
                                $real_answers[$answerId] = true;
3955
                            }
3956
                        } else {
3957
                            $real_answers[$answerId] = true;
3958
                            if ($studentChoice) {
3959
                                $real_answers[$answerId] = false;
3960
                            }
3961
                        }
3962
                    } else {
3963
                        $studentChoice = $choice[$answerAutoId] ?? null;
3964
                        if (1 == $answerCorrect) {
3965
                            $real_answers[$answerId] = false;
3966
                            if ($studentChoice) {
3967
                                $real_answers[$answerId] = true;
3968
                            }
3969
                        } else {
3970
                            $real_answers[$answerId] = true;
3971
                            if ($studentChoice) {
3972
                                $real_answers[$answerId] = false;
3973
                            }
3974
                        }
3975
                    }
3976
3977
                    break;
3978
                case FILL_IN_BLANKS:
3979
                    $str = '';
3980
                    $answerFromDatabase = '';
3981
                    if ($from_database) {
3982
                        $sql = "SELECT answer
3983
                                FROM $TBL_TRACK_ATTEMPT
3984
                                WHERE
3985
                                    exe_id = $exeId AND
3986
                                    question_id= $questionId ";
3987
                        $result = Database::query($sql);
3988
                        $str = $answerFromDatabase = Database::result($result, 0, 'answer');
3989
                    }
3990
3991
                    // if ($saved_results == false && strpos($answerFromDatabase, 'font color') !== false) {
3992
                    if (false) {
3993
                        // the question is encoded like this
3994
                        // [A] B [C] D [E] F::10,10,10@1
3995
                        // number 1 before the "@" means that is a switchable fill in blank question
3996
                        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
3997
                        // means that is a normal fill blank question
3998
                        // first we explode the "::"
3999
                        $pre_array = explode('::', $answer);
4000
4001
                        // is switchable fill blank or not
4002
                        $last = count($pre_array) - 1;
4003
                        $is_set_switchable = explode('@', $pre_array[$last]);
4004
                        $switchable_answer_set = false;
4005
                        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
4006
                            $switchable_answer_set = true;
4007
                        }
4008
                        $answer = '';
4009
                        for ($k = 0; $k < $last; $k++) {
4010
                            $answer .= $pre_array[$k];
4011
                        }
4012
                        // splits weightings that are joined with a comma
4013
                        $answerWeighting = explode(',', $is_set_switchable[0]);
4014
                        // we save the answer because it will be modified
4015
                        $temp = $answer;
4016
                        $answer = '';
4017
                        $j = 0;
4018
                        //initialise answer tags
4019
                        $user_tags = $correct_tags = $real_text = [];
4020
                        // the loop will stop at the end of the text
4021
                        while (1) {
4022
                            // quits the loop if there are no more blanks (detect '[')
4023
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
4024
                                // adds the end of the text
4025
                                $answer = $temp;
4026
                                $real_text[] = $answer;
4027
4028
                                break; //no more "blanks", quit the loop
4029
                            }
4030
                            // adds the piece of text that is before the blank
4031
                            //and ends with '[' into a general storage array
4032
                            $real_text[] = api_substr($temp, 0, $pos + 1);
4033
                            $answer .= api_substr($temp, 0, $pos + 1);
4034
                            //take the string remaining (after the last "[" we found)
4035
                            $temp = api_substr($temp, $pos + 1);
4036
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4037
                            if (false === ($pos = api_strpos($temp, ']'))) {
4038
                                // adds the end of the text
4039
                                $answer .= $temp;
4040
4041
                                break;
4042
                            }
4043
                            if ($from_database) {
4044
                                $str = $answerFromDatabase;
4045
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4046
                                $str = str_replace('\r\n', '', $str);
4047
4048
                                $choice = $arr[1];
4049
                                if (isset($choice[$j])) {
4050
                                    $tmp = api_strrpos($choice[$j], ' / ');
4051
                                    $choice[$j] = api_substr($choice[$j], 0, $tmp);
4052
                                    $choice[$j] = trim($choice[$j]);
4053
                                    // Needed to let characters ' and " to work as part of an answer
4054
                                    $choice[$j] = stripslashes($choice[$j]);
4055
                                } else {
4056
                                    $choice[$j] = null;
4057
                                }
4058
                            } else {
4059
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4060
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4061
                            }
4062
4063
                            $user_tags[] = $choice[$j];
4064
                            // Put the contents of the [] answer tag into correct_tags[]
4065
                            $correct_tags[] = api_substr($temp, 0, $pos);
4066
                            $j++;
4067
                            $temp = api_substr($temp, $pos + 1);
4068
                        }
4069
                        $answer = '';
4070
                        $real_correct_tags = $correct_tags;
4071
                        $chosen_list = [];
4072
4073
                        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...
4074
                            if (0 == $i) {
4075
                                $answer .= $real_text[0];
4076
                            }
4077
                            if (!$switchable_answer_set) {
4078
                                // Needed to parse ' and " characters
4079
                                $user_tags[$i] = stripslashes($user_tags[$i]);
4080
                                if ($correct_tags[$i] == $user_tags[$i]) {
4081
                                    // gives the related weighting to the student
4082
                                    $questionScore += $answerWeighting[$i];
4083
                                    // increments total score
4084
                                    $totalScore += $answerWeighting[$i];
4085
                                    // adds the word in green at the end of the string
4086
                                    $answer .= $correct_tags[$i];
4087
                                } elseif (!empty($user_tags[$i])) {
4088
                                    // else if the word entered by the student IS NOT the same as
4089
                                    // the one defined by the professor
4090
                                    // adds the word in red at the end of the string, and strikes it
4091
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4092
                                } else {
4093
                                    // adds a tabulation if no word has been typed by the student
4094
                                    $answer .= ''; // remove &nbsp; that causes issue
4095
                                }
4096
                            } else {
4097
                                // switchable fill in the blanks
4098
                                if (in_array($user_tags[$i], $correct_tags)) {
4099
                                    $chosen_list[] = $user_tags[$i];
4100
                                    $correct_tags = array_diff($correct_tags, $chosen_list);
4101
                                    // gives the related weighting to the student
4102
                                    $questionScore += $answerWeighting[$i];
4103
                                    // increments total score
4104
                                    $totalScore += $answerWeighting[$i];
4105
                                    // adds the word in green at the end of the string
4106
                                    $answer .= $user_tags[$i];
4107
                                } elseif (!empty($user_tags[$i])) {
4108
                                    // else if the word entered by the student IS NOT the same
4109
                                    // as the one defined by the professor
4110
                                    // adds the word in red at the end of the string, and strikes it
4111
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4112
                                } else {
4113
                                    // adds a tabulation if no word has been typed by the student
4114
                                    $answer .= ''; // remove &nbsp; that causes issue
4115
                                }
4116
                            }
4117
4118
                            // adds the correct word, followed by ] to close the blank
4119
                            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4120
                            if (isset($real_text[$i + 1])) {
4121
                                $answer .= $real_text[$i + 1];
4122
                            }
4123
                        }
4124
                    } else {
4125
                        // insert the student result in the track_e_attempt table, field answer
4126
                        // $answer is the answer like in the c_quiz_answer table for the question
4127
                        // student data are choice[]
4128
                        $listCorrectAnswers = FillBlanks::getAnswerInfo($answer);
4129
                        $switchableAnswerSet = $listCorrectAnswers['switchable'];
4130
                        $answerWeighting = $listCorrectAnswers['weighting'];
4131
                        // user choices is an array $choice
4132
4133
                        // get existing user data in n the BDD
4134
                        if ($from_database) {
4135
                            $listStudentResults = FillBlanks::getAnswerInfo(
4136
                                $answerFromDatabase,
4137
                                true
4138
                            );
4139
                            $choice = $listStudentResults['student_answer'];
4140
                        }
4141
4142
                        // loop other all blanks words
4143
                        if (!$switchableAnswerSet) {
4144
                            // not switchable answer, must be in the same place than teacher order
4145
                            for ($i = 0; $i < count($listCorrectAnswers['words']); $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...
4146
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
4147
                                $correctAnswer = $listCorrectAnswers['words'][$i];
4148
4149
                                if ($debug) {
4150
                                    error_log("Student answer: $i");
4151
                                    error_log($studentAnswer);
4152
                                }
4153
4154
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4155
                                // Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
4156
                                // ENT_QUOTES is used in order to transform ' to &#039;
4157
                                if (!$from_database) {
4158
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4159
                                    if ($debug) {
4160
                                        error_log('Student answer cleaned:');
4161
                                        error_log($studentAnswer);
4162
                                    }
4163
                                }
4164
4165
                                $isAnswerCorrect = 0;
4166
                                if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
4167
                                    // gives the related weighting to the student
4168
                                    $questionScore += $answerWeighting[$i];
4169
                                    // increments total score
4170
                                    $totalScore += $answerWeighting[$i];
4171
                                    $isAnswerCorrect = 1;
4172
                                }
4173
                                if ($debug) {
4174
                                    error_log("isAnswerCorrect $i: $isAnswerCorrect");
4175
                                }
4176
4177
                                $studentAnswerToShow = $studentAnswer;
4178
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4179
                                if ($debug) {
4180
                                    error_log("Fill in blank type: $type");
4181
                                }
4182
                                if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4183
                                    $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4184
                                    if ('' != $studentAnswer) {
4185
                                        foreach ($listMenu as $item) {
4186
                                            if (sha1($item) == $studentAnswer) {
4187
                                                $studentAnswerToShow = $item;
4188
                                            }
4189
                                        }
4190
                                    }
4191
                                }
4192
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4193
                                $listCorrectAnswers['student_score'][$i] = $isAnswerCorrect;
4194
                            }
4195
                        } else {
4196
                            // switchable answer
4197
                            $listStudentAnswerTemp = $choice;
4198
                            $listTeacherAnswerTemp = $listCorrectAnswers['words'];
4199
4200
                            // for every teacher answer, check if there is a student answer
4201
                            for ($i = 0; $i < count($listStudentAnswerTemp); $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...
4202
                                $studentAnswer = trim($listStudentAnswerTemp[$i]);
4203
                                $studentAnswerToShow = $studentAnswer;
4204
4205
                                if (empty($studentAnswer)) {
4206
                                    break;
4207
                                }
4208
4209
                                if ($debug) {
4210
                                    error_log("Student answer: $i");
4211
                                    error_log($studentAnswer);
4212
                                }
4213
4214
                                if (!$from_database) {
4215
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4216
                                    if ($debug) {
4217
                                        error_log("Student answer cleaned:");
4218
                                        error_log($studentAnswer);
4219
                                    }
4220
                                }
4221
4222
                                $found = false;
4223
                                for ($j = 0; $j < count($listTeacherAnswerTemp); $j++) {
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...
4224
                                    $correctAnswer = $listTeacherAnswerTemp[$j];
4225
4226
                                    if (!$found) {
4227
                                        if (FillBlanks::isStudentAnswerGood(
4228
                                            $studentAnswer,
4229
                                            $correctAnswer,
4230
                                            $from_database
4231
                                        )) {
4232
                                            $questionScore += $answerWeighting[$i];
4233
                                            $totalScore += $answerWeighting[$i];
4234
                                            $listTeacherAnswerTemp[$j] = '';
4235
                                            $found = true;
4236
                                        }
4237
                                    }
4238
4239
                                    $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4240
                                    if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4241
                                        $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4242
                                        if (!empty($studentAnswer)) {
4243
                                            foreach ($listMenu as $key => $item) {
4244
                                                if (sha1($item) === $studentAnswer) {
4245
                                                    $studentAnswerToShow = $item;
4246
                                                    break;
4247
                                                }
4248
                                            }
4249
                                        }
4250
                                    }
4251
                                }
4252
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4253
                                $listCorrectAnswers['student_score'][$i] = $found ? 1 : 0;
4254
                            }
4255
                        }
4256
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
4257
                    }
4258
4259
                    break;
4260
                case CALCULATED_ANSWER:
4261
                    $calculatedAnswerList = Session::read('calculatedAnswerId');
4262
                    if (!empty($calculatedAnswerList)) {
4263
                        $answer = $objAnswerTmp->selectAnswer($calculatedAnswerList[$questionId]);
4264
                        $preArray = explode('@@', $answer);
4265
                        $last = count($preArray) - 1;
4266
                        $answer = '';
4267
                        for ($k = 0; $k < $last; $k++) {
4268
                            $answer .= $preArray[$k];
4269
                        }
4270
                        $answerWeighting = [$answerWeighting];
4271
                        // we save the answer because it will be modified
4272
                        $temp = $answer;
4273
                        $answer = '';
4274
                        $j = 0;
4275
                        // initialise answer tags
4276
                        $userTags = $correctTags = $realText = [];
4277
                        // the loop will stop at the end of the text
4278
                        while (1) {
4279
                            // quits the loop if there are no more blanks (detect '[')
4280
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
4281
                                // adds the end of the text
4282
                                $answer = $temp;
4283
                                $realText[] = $answer;
4284
4285
                                break; //no more "blanks", quit the loop
4286
                            }
4287
                            // adds the piece of text that is before the blank
4288
                            // and ends with '[' into a general storage array
4289
                            $realText[] = api_substr($temp, 0, $pos + 1);
4290
                            $answer .= api_substr($temp, 0, $pos + 1);
4291
                            // take the string remaining (after the last "[" we found)
4292
                            $temp = api_substr($temp, $pos + 1);
4293
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4294
                            if (false === ($pos = api_strpos($temp, ']'))) {
4295
                                // adds the end of the text
4296
                                $answer .= $temp;
4297
4298
                                break;
4299
                            }
4300
4301
                            if ($from_database) {
4302
                                $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4303
                                        WHERE
4304
                                            exe_id = $exeId AND
4305
                                            question_id = $questionId ";
4306
                                $result = Database::query($sql);
4307
                                $str = Database::result($result, 0, 'answer');
4308
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4309
                                $str = str_replace('\r\n', '', $str);
4310
                                $choice = $arr[1];
4311
                                if (isset($choice[$j])) {
4312
                                    $tmp = api_strrpos($choice[$j], ' / ');
4313
                                    if ($tmp) {
4314
                                        $choice[$j] = api_substr($choice[$j], 0, $tmp);
4315
                                    } else {
4316
                                        $tmp = ltrim($tmp, '[');
4317
                                        $tmp = rtrim($tmp, ']');
4318
                                    }
4319
                                    $choice[$j] = trim($choice[$j]);
4320
                                    // Needed to let characters ' and " to work as part of an answer
4321
                                    $choice[$j] = stripslashes($choice[$j]);
4322
                                } else {
4323
                                    $choice[$j] = null;
4324
                                }
4325
                            } else {
4326
                                // This value is the user input not escaped while correct answer is escaped by ckeditor
4327
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4328
                            }
4329
                            $userTags[] = $choice[$j];
4330
                            // put the contents of the [] answer tag into correct_tags[]
4331
                            $correctTags[] = api_substr($temp, 0, $pos);
4332
                            $j++;
4333
                            $temp = api_substr($temp, $pos + 1);
4334
                        }
4335
                        $answer = '';
4336
                        $realCorrectTags = $correctTags;
4337
                        $calculatedStatus = Display::label(get_lang('Incorrect'), 'danger');
4338
                        $expectedAnswer = '';
4339
                        $calculatedChoice = '';
4340
4341
                        for ($i = 0; $i < count($realCorrectTags); $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...
4342
                            if (0 == $i) {
4343
                                $answer .= $realText[0];
4344
                            }
4345
                            // Needed to parse ' and " characters
4346
                            $userTags[$i] = stripslashes($userTags[$i]);
4347
                            if ($correctTags[$i] == $userTags[$i]) {
4348
                                // gives the related weighting to the student
4349
                                $questionScore += $answerWeighting[$i];
4350
                                // increments total score
4351
                                $totalScore += $answerWeighting[$i];
4352
                                // adds the word in green at the end of the string
4353
                                $answer .= $correctTags[$i];
4354
                                $calculatedChoice = $correctTags[$i];
4355
                            } elseif (!empty($userTags[$i])) {
4356
                                // else if the word entered by the student IS NOT the same as
4357
                                // the one defined by the professor
4358
                                // adds the word in red at the end of the string, and strikes it
4359
                                $answer .= '<font color="red"><s>'.$userTags[$i].'</s></font>';
4360
                                $calculatedChoice = $userTags[$i];
4361
                            } else {
4362
                                // adds a tabulation if no word has been typed by the student
4363
                                $answer .= ''; // remove &nbsp; that causes issue
4364
                            }
4365
                            // adds the correct word, followed by ] to close the blank
4366
                            if (EXERCISE_FEEDBACK_TYPE_EXAM != $this->results_disabled) {
4367
                                $answer .= ' / <font color="green"><b>'.$realCorrectTags[$i].'</b></font>';
4368
                                $calculatedStatus = Display::label(get_lang('Correct'), 'success');
4369
                                $expectedAnswer = $realCorrectTags[$i];
4370
                            }
4371
                            $answer .= ']';
4372
                            if (isset($realText[$i + 1])) {
4373
                                $answer .= $realText[$i + 1];
4374
                            }
4375
                        }
4376
                    } else {
4377
                        if ($from_database) {
4378
                            $sql = "SELECT *
4379
                                    FROM $TBL_TRACK_ATTEMPT
4380
                                    WHERE
4381
                                        exe_id = $exeId AND
4382
                                        question_id = $questionId ";
4383
                            $result = Database::query($sql);
4384
                            $resultData = Database::fetch_assoc($result);
4385
                            $answer = $resultData['answer'];
4386
                            $questionScore = $resultData['marks'];
4387
                        }
4388
                    }
4389
4390
                    break;
4391
                case FREE_ANSWER:
4392
                    if ($from_database) {
4393
                        $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
4394
                                 WHERE
4395
                                    exe_id = $exeId AND
4396
                                    question_id= ".$questionId;
4397
                        $result = Database::query($sql);
4398
                        $data = Database::fetch_array($result);
4399
                        $choice = '';
4400
                        $questionScore = 0;
4401
                        if ($data) {
4402
                            $choice = $data['answer'];
4403
                            $questionScore = $data['marks'];
4404
                        }
4405
4406
                        $choice = str_replace('\r\n', '', $choice);
4407
                        $choice = stripslashes($choice);
4408
4409
                        if (-1 == $questionScore) {
4410
                            $totalScore += 0;
4411
                        } else {
4412
                            $totalScore += $questionScore;
4413
                        }
4414
                        if ('' == $questionScore) {
4415
                            $questionScore = 0;
4416
                        }
4417
                        $arrques = $questionName;
4418
                        $arrans = $choice;
4419
                    } else {
4420
                        $studentChoice = $choice;
4421
                        if ($studentChoice) {
4422
                            //Fixing negative puntation see #2193
4423
                            $questionScore = 0;
4424
                            $totalScore += 0;
4425
                        }
4426
                    }
4427
4428
                    break;
4429
                case ORAL_EXPRESSION:
4430
                    if ($from_database) {
4431
                        $query = "SELECT answer, marks
4432
                                  FROM $TBL_TRACK_ATTEMPT
4433
                                  WHERE
4434
                                        exe_id = $exeId AND
4435
                                        question_id = $questionId
4436
                                 ";
4437
                        $resq = Database::query($query);
4438
                        $row = Database::fetch_assoc($resq);
4439
                        $choice = '';
4440
                        $questionScore = 0;
4441
4442
                        if (is_array($row)) {
4443
                            $choice = $row['answer'];
4444
                            $choice = str_replace('\r\n', '', $choice);
4445
                            $choice = stripslashes($choice);
4446
                            $questionScore = $row['marks'];
4447
                        }
4448
4449
                        if (-1 == $questionScore) {
4450
                            $totalScore += 0;
4451
                        } else {
4452
                            $totalScore += $questionScore;
4453
                        }
4454
                        $arrques = $questionName;
4455
                        $arrans = $choice;
4456
                    } else {
4457
                        $studentChoice = $choice;
4458
                        if ($studentChoice) {
4459
                            //Fixing negative puntation see #2193
4460
                            $questionScore = 0;
4461
                            $totalScore += 0;
4462
                        }
4463
                    }
4464
4465
                    break;
4466
                case DRAGGABLE:
4467
                case MATCHING_DRAGGABLE:
4468
                case MATCHING:
4469
                    if ($from_database) {
4470
                        $sql = "SELECT iid, answer
4471
                                FROM $table_ans
4472
                                WHERE
4473
                                    question_id = $questionId AND
4474
                                    correct = 0
4475
                                ";
4476
                        $result = Database::query($sql);
4477
                        // Getting the real answer
4478
                        $real_list = [];
4479
                        while ($realAnswer = Database::fetch_array($result)) {
4480
                            $real_list[$realAnswer['iid']] = $realAnswer['answer'];
4481
                        }
4482
4483
                        $orderBy = ' ORDER BY iid ';
4484
                        if (DRAGGABLE == $answerType) {
4485
                            $orderBy = ' ORDER BY correct ';
4486
                        }
4487
4488
                        $sql = "SELECT iid, answer, correct, ponderation
4489
                                FROM $table_ans
4490
                                WHERE
4491
                                    question_id = $questionId AND
4492
                                    correct <> 0
4493
                                $orderBy";
4494
                        $result = Database::query($sql);
4495
                        $options = [];
4496
                        $correctAnswers = [];
4497
                        while ($row = Database::fetch_assoc($result)) {
4498
                            $options[] = $row;
4499
                            $correctAnswers[$row['correct']] = $row['answer'];
4500
                        }
4501
4502
                        $questionScore = 0;
4503
                        $counterAnswer = 1;
4504
                        foreach ($options as $a_answers) {
4505
                            $i_answer_id = $a_answers['iid']; //3
4506
                            $s_answer_label = $a_answers['answer']; // your daddy - your mother
4507
                            $i_answer_correct_answer = $a_answers['correct']; //1 - 2
4508
                            $i_answer_id_auto = $a_answers['iid']; // 3 - 4
4509
4510
                            $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4511
                                    WHERE
4512
                                        exe_id = '$exeId' AND
4513
                                        question_id = '$questionId' AND
4514
                                        position = '$i_answer_id_auto'";
4515
                            $result = Database::query($sql);
4516
                            $s_user_answer = 0;
4517
                            if (Database::num_rows($result) > 0) {
4518
                                //  rich - good looking
4519
                                $s_user_answer = Database::result($result, 0, 0);
4520
                            }
4521
                            $i_answerWeighting = $a_answers['ponderation'];
4522
                            $user_answer = '';
4523
                            $status = Display::label(get_lang('Incorrect'), 'danger');
4524
4525
                            if (!empty($s_user_answer)) {
4526
                                if (DRAGGABLE == $answerType) {
4527
                                    if ($s_user_answer == $i_answer_correct_answer) {
4528
                                        $questionScore += $i_answerWeighting;
4529
                                        $totalScore += $i_answerWeighting;
4530
                                        $user_answer = Display::label(get_lang('Correct'), 'success');
4531
                                        if ($this->showExpectedChoice() && !empty($i_answer_id_auto)) {
4532
                                            $user_answer = $answerMatching[$i_answer_id_auto];
4533
                                        }
4534
                                        $status = Display::label(get_lang('Correct'), 'success');
4535
                                    } else {
4536
                                        $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4537
                                        if ($this->showExpectedChoice() && !empty($s_user_answer)) {
4538
                                            /*$data = $options[$real_list[$s_user_answer] - 1];
4539
                                            $user_answer = $data['answer'];*/
4540
                                            $user_answer = $correctAnswers[$s_user_answer] ?? '';
4541
                                        }
4542
                                    }
4543
                                } else {
4544
                                    if ($s_user_answer == $i_answer_correct_answer) {
4545
                                        $questionScore += $i_answerWeighting;
4546
                                        $totalScore += $i_answerWeighting;
4547
                                        $status = Display::label(get_lang('Correct'), 'success');
4548
4549
                                        // Try with id
4550
                                        if (isset($real_list[$i_answer_id])) {
4551
                                            $user_answer = Display::span(
4552
                                                $real_list[$i_answer_id],
4553
                                                ['style' => 'color: #008000; font-weight: bold;']
4554
                                            );
4555
                                        }
4556
4557
                                        // Try with $i_answer_id_auto
4558
                                        if (empty($user_answer)) {
4559
                                            if (isset($real_list[$i_answer_id_auto])) {
4560
                                                $user_answer = Display::span(
4561
                                                    $real_list[$i_answer_id_auto],
4562
                                                    ['style' => 'color: #008000; font-weight: bold;']
4563
                                                );
4564
                                            }
4565
                                        }
4566
4567
                                        if (isset($real_list[$i_answer_correct_answer])) {
4568
                                            $user_answer = Display::span(
4569
                                                $real_list[$i_answer_correct_answer],
4570
                                                ['style' => 'color: #008000; font-weight: bold;']
4571
                                            );
4572
                                        }
4573
                                    } else {
4574
                                        $user_answer = Display::span(
4575
                                            $real_list[$s_user_answer],
4576
                                            ['style' => 'color: #FF0000; text-decoration: line-through;']
4577
                                        );
4578
                                        if ($this->showExpectedChoice()) {
4579
                                            if (isset($real_list[$s_user_answer])) {
4580
                                                $user_answer = Display::span($real_list[$s_user_answer]);
4581
                                            }
4582
                                        }
4583
                                    }
4584
                                }
4585
                            } elseif (DRAGGABLE == $answerType) {
4586
                                $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4587
                                if ($this->showExpectedChoice()) {
4588
                                    $user_answer = '';
4589
                                }
4590
                            } else {
4591
                                $user_answer = Display::span(
4592
                                    get_lang('Incorrect').' &nbsp;',
4593
                                    ['style' => 'color: #FF0000; text-decoration: line-through;']
4594
                                );
4595
                                if ($this->showExpectedChoice()) {
4596
                                    $user_answer = '';
4597
                                }
4598
                            }
4599
4600
                            if ($show_result) {
4601
                                if (false === $this->showExpectedChoice() &&
4602
                                    false === $showTotalScoreAndUserChoicesInLastAttempt
4603
                                ) {
4604
                                    $user_answer = '';
4605
                                }
4606
                                switch ($answerType) {
4607
                                    case MATCHING:
4608
                                    case MATCHING_DRAGGABLE:
4609
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4610
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4611
                                                break;
4612
                                            }
4613
                                        }
4614
                                        echo '<tr>';
4615
                                        if (!in_array(
4616
                                            $this->results_disabled,
4617
                                            [
4618
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4619
                                            ]
4620
                                        )
4621
                                        ) {
4622
                                            echo '<td>'.$s_answer_label.'</td>';
4623
                                            echo '<td>'.$user_answer.'</td>';
4624
                                        } else {
4625
                                            echo '<td>'.$s_answer_label.'</td>';
4626
                                            $status = Display::label(get_lang('Correct'), 'success');
4627
                                        }
4628
4629
                                        if ($this->showExpectedChoice()) {
4630
                                            if ($this->showExpectedChoiceColumn()) {
4631
                                                echo '<td>';
4632
                                                if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4633
                                                    if (isset($real_list[$i_answer_correct_answer]) &&
4634
                                                        true == $showTotalScoreAndUserChoicesInLastAttempt
4635
                                                    ) {
4636
                                                        echo Display::span(
4637
                                                            $real_list[$i_answer_correct_answer]
4638
                                                        );
4639
                                                    }
4640
                                                }
4641
                                                echo '</td>';
4642
                                            }
4643
                                            echo '<td class="text-center">'.$status.'</td>';
4644
                                        } else {
4645
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4646
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4647
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4648
                                                ) {
4649
                                                    if ($this->showExpectedChoiceColumn()) {
4650
                                                        echo '<td>';
4651
                                                        echo Display::span(
4652
                                                            $real_list[$i_answer_correct_answer],
4653
                                                            ['style' => 'color: #008000; font-weight: bold;']
4654
                                                        );
4655
                                                        echo '</td>';
4656
                                                    }
4657
                                                }
4658
                                            }
4659
                                        }
4660
                                        echo '</tr>';
4661
4662
                                        break;
4663
                                    case DRAGGABLE:
4664
                                        if (false == $showTotalScoreAndUserChoicesInLastAttempt) {
4665
                                            $s_answer_label = '';
4666
                                        }
4667
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4668
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4669
                                                break;
4670
                                            }
4671
                                        }
4672
                                        echo '<tr>';
4673
                                        if ($this->showExpectedChoice()) {
4674
                                            if (!in_array(
4675
                                                $this->results_disabled,
4676
                                                [
4677
                                                    RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4678
                                                    //RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4679
                                                ]
4680
                                            )
4681
                                            ) {
4682
                                                echo '<td>'.$user_answer.'</td>';
4683
                                            } else {
4684
                                                $status = Display::label(get_lang('Correct'), 'success');
4685
                                            }
4686
                                            echo '<td>'.$s_answer_label.'</td>';
4687
                                            echo '<td class="text-center">'.$status.'</td>';
4688
                                        } else {
4689
                                            echo '<td>'.$s_answer_label.'</td>';
4690
                                            echo '<td>'.$user_answer.'</td>';
4691
                                            echo '<td>';
4692
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4693
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4694
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4695
                                                ) {
4696
                                                    echo Display::span(
4697
                                                        $real_list[$i_answer_correct_answer],
4698
                                                        ['style' => 'color: #008000; font-weight: bold;']
4699
                                                    );
4700
                                                }
4701
                                            }
4702
                                            echo '</td>';
4703
                                        }
4704
                                        echo '</tr>';
4705
4706
                                        break;
4707
                                }
4708
                            }
4709
                            $counterAnswer++;
4710
                        }
4711
4712
                        break 2; // break the switch and the "for" condition
4713
                    } else {
4714
                        if ($answerCorrect) {
4715
                            if (isset($choice[$answerAutoId]) &&
4716
                                $answerCorrect == $choice[$answerAutoId]
4717
                            ) {
4718
                                $correctAnswerId[] = $answerAutoId;
4719
                                $questionScore += $answerWeighting;
4720
                                $totalScore += $answerWeighting;
4721
                                $user_answer = Display::span($answerMatching[$choice[$answerAutoId]]);
4722
                            } else {
4723
                                if (isset($answerMatching[$choice[$answerAutoId]])) {
4724
                                    $user_answer = Display::span(
4725
                                        $answerMatching[$choice[$answerAutoId]],
4726
                                        ['style' => 'color: #FF0000; text-decoration: line-through;']
4727
                                    );
4728
                                }
4729
                            }
4730
                            $matching[$answerAutoId] = $choice[$answerAutoId];
4731
                        }
4732
                    }
4733
4734
                    break;
4735
                case HOT_SPOT:
4736
                    if ($from_database) {
4737
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4738
                        // Check auto id
4739
                        $foundAnswerId = $answerAutoId;
4740
                        $sql = "SELECT hotspot_correct
4741
                                FROM $TBL_TRACK_HOTSPOT
4742
                                WHERE
4743
                                    hotspot_exe_id = $exeId AND
4744
                                    hotspot_question_id= $questionId AND
4745
                                    hotspot_answer_id = $answerAutoId
4746
                                ORDER BY hotspot_id ASC";
4747
                        $result = Database::query($sql);
4748
                        if (Database::num_rows($result)) {
4749
                            $studentChoice = Database::result(
4750
                                $result,
4751
                                0,
4752
                                'hotspot_correct'
4753
                            );
4754
4755
                            if ($studentChoice) {
4756
                                $questionScore += $answerWeighting;
4757
                                $totalScore += $answerWeighting;
4758
                            }
4759
                        } else {
4760
                            // If answer.id is different:
4761
                            $sql = "SELECT hotspot_correct
4762
                                FROM $TBL_TRACK_HOTSPOT
4763
                                WHERE
4764
                                    hotspot_exe_id = $exeId AND
4765
                                    hotspot_question_id= $questionId AND
4766
                                    hotspot_answer_id = ".(int) $answerId.'
4767
                                ORDER BY hotspot_id ASC';
4768
                            $result = Database::query($sql);
4769
4770
                            $foundAnswerId = $answerId;
4771
                            if (Database::num_rows($result)) {
4772
                                $studentChoice = Database::result(
4773
                                    $result,
4774
                                    0,
4775
                                    'hotspot_correct'
4776
                                );
4777
4778
                                if ($studentChoice) {
4779
                                    $questionScore += $answerWeighting;
4780
                                    $totalScore += $answerWeighting;
4781
                                }
4782
                            } else {
4783
                                // check answer.iid
4784
                                if (!empty($answerIid)) {
4785
                                    $sql = "SELECT hotspot_correct
4786
                                            FROM $TBL_TRACK_HOTSPOT
4787
                                            WHERE
4788
                                                hotspot_exe_id = $exeId AND
4789
                                                hotspot_question_id= $questionId AND
4790
                                                hotspot_answer_id = $answerIid
4791
                                            ORDER BY hotspot_id ASC";
4792
                                    $result = Database::query($sql);
4793
4794
                                    $foundAnswerId = $answerIid;
4795
                                    $studentChoice = Database::result(
4796
                                        $result,
4797
                                        0,
4798
                                        'hotspot_correct'
4799
                                    );
4800
4801
                                    if ($studentChoice) {
4802
                                        $questionScore += $answerWeighting;
4803
                                        $totalScore += $answerWeighting;
4804
                                    }
4805
                                }
4806
                            }
4807
                        }
4808
                    } else {
4809
                        if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
4810
                            $choice[$answerAutoId] = 0;
4811
                            $choice[$answerIid] = 0;
4812
                        } else {
4813
                            $studentChoice = $choice[$answerAutoId];
4814
                            if (empty($studentChoice)) {
4815
                                $studentChoice = $choice[$answerIid];
4816
                            }
4817
                            $choiceIsValid = false;
4818
                            if (!empty($studentChoice)) {
4819
                                $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
4820
                                $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
4821
                                $choicePoint = Geometry::decodePoint($studentChoice);
4822
4823
                                switch ($hotspotType) {
4824
                                    case 'square':
4825
                                        $hotspotProperties = Geometry::decodeSquare($hotspotCoordinates);
4826
                                        $choiceIsValid = Geometry::pointIsInSquare($hotspotProperties, $choicePoint);
4827
4828
                                        break;
4829
                                    case 'circle':
4830
                                        $hotspotProperties = Geometry::decodeEllipse($hotspotCoordinates);
4831
                                        $choiceIsValid = Geometry::pointIsInEllipse($hotspotProperties, $choicePoint);
4832
4833
                                        break;
4834
                                    case 'poly':
4835
                                        $hotspotProperties = Geometry::decodePolygon($hotspotCoordinates);
4836
                                        $choiceIsValid = Geometry::pointIsInPolygon($hotspotProperties, $choicePoint);
4837
4838
                                        break;
4839
                                }
4840
                            }
4841
4842
                            $choice[$answerAutoId] = 0;
4843
                            if ($choiceIsValid) {
4844
                                $questionScore += $answerWeighting;
4845
                                $totalScore += $answerWeighting;
4846
                                $choice[$answerAutoId] = 1;
4847
                                $choice[$answerIid] = 1;
4848
                            }
4849
                        }
4850
                    }
4851
4852
                    break;
4853
                case HOT_SPOT_ORDER:
4854
                    // @todo never added to chamilo
4855
                    // for hotspot with fixed order
4856
                    $studentChoice = $choice['order'][$answerId];
4857
                    if ($studentChoice == $answerId) {
4858
                        $questionScore += $answerWeighting;
4859
                        $totalScore += $answerWeighting;
4860
                        $studentChoice = true;
4861
                    } else {
4862
                        $studentChoice = false;
4863
                    }
4864
4865
                    break;
4866
                case HOT_SPOT_DELINEATION:
4867
                    // for hotspot with delineation
4868
                    if ($from_database) {
4869
                        // getting the user answer
4870
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4871
                        $query = "SELECT hotspot_correct, hotspot_coordinate
4872
                                    FROM $TBL_TRACK_HOTSPOT
4873
                                    WHERE
4874
                                        hotspot_exe_id = $exeId AND
4875
                                        hotspot_question_id= $questionId AND
4876
                                        hotspot_answer_id = '1'";
4877
                        // By default we take 1 because it's a delineation
4878
                        $resq = Database::query($query);
4879
                        $row = Database::fetch_assoc($resq);
4880
4881
                        if ($row && isset($row['hotspot_correct'], $row['hotspot_coordinate'])) {
4882
                            $choice = $row['hotspot_correct'];
4883
                            $user_answer = $row['hotspot_coordinate'];
4884
                            $coords = explode('/', $user_answer);
4885
                        } else {
4886
                            $choice = '';
4887
                            $user_answer = '';
4888
                            $coords = [];
4889
                        }
4890
4891
                        $user_array = '';
4892
                        foreach ($coords as $coord) {
4893
                            [$x, $y] = explode(';', $coord);
4894
                            $user_array .= round($x).';'.round($y).'/';
4895
                        }
4896
                        $user_array = substr($user_array, 0, -1) ?: '';
4897
                    } else {
4898
                        if (!empty($studentChoice)) {
4899
                            $correctAnswerId[] = $answerAutoId;
4900
                            $newquestionList[] = $questionId;
4901
                        }
4902
4903
                        if (1 === $answerId && isset($choice[$answerId])) {
4904
                            $studentChoice = $choice[$answerId];
4905
                            $questionScore += $answerWeighting;
4906
                        }
4907
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
4908
                            $user_array = $_SESSION['exerciseResultCoordinates'][$questionId];
4909
                        }
4910
                    }
4911
                    $_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
4912
                    $_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
4913
4914
                    break;
4915
                case ANNOTATION:
4916
                    if ($from_database) {
4917
                        $sql = "SELECT answer, marks
4918
                                FROM $TBL_TRACK_ATTEMPT
4919
                                WHERE
4920
                                  exe_id = $exeId AND
4921
                                  question_id = $questionId ";
4922
                        $resq = Database::query($sql);
4923
                        $data = Database::fetch_array($resq);
4924
4925
                        $questionScore = empty($data['marks']) ? 0 : $data['marks'];
4926
                        $arrques = $questionName;
4927
4928
                        break;
4929
                    }
4930
                    $studentChoice = $choice;
4931
                    if ($studentChoice) {
4932
                        $questionScore = 0;
4933
                    }
4934
4935
                    break;
4936
            }
4937
4938
            if ($show_result) {
4939
                if ('exercise_result' === $from) {
4940
                    // Display answers (if not matching type, or if the answer is correct)
4941
                    if (!in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE]) ||
4942
                        $answerCorrect
4943
                    ) {
4944
                        if (in_array(
4945
                            $answerType,
4946
                            [
4947
                                UNIQUE_ANSWER,
4948
                                UNIQUE_ANSWER_IMAGE,
4949
                                UNIQUE_ANSWER_NO_OPTION,
4950
                                MULTIPLE_ANSWER,
4951
                                MULTIPLE_ANSWER_COMBINATION,
4952
                                GLOBAL_MULTIPLE_ANSWER,
4953
                                READING_COMPREHENSION,
4954
                            ]
4955
                        )) {
4956
                            ExerciseShowFunctions::display_unique_or_multiple_answer(
4957
                                $this,
4958
                                $feedback_type,
4959
                                $answerType,
4960
                                $studentChoice,
4961
                                $answer,
4962
                                $answerComment,
4963
                                $answerCorrect,
4964
                                0,
4965
                                0,
4966
                                0,
4967
                                $results_disabled,
4968
                                $showTotalScoreAndUserChoicesInLastAttempt,
4969
                                $this->export
4970
                            );
4971
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
4972
                            ExerciseShowFunctions::display_multiple_answer_true_false(
4973
                                $this,
4974
                                $feedback_type,
4975
                                $answerType,
4976
                                $studentChoice,
4977
                                $answer,
4978
                                $answerComment,
4979
                                $answerCorrect,
4980
                                0,
4981
                                $questionId,
4982
                                0,
4983
                                $results_disabled,
4984
                                $showTotalScoreAndUserChoicesInLastAttempt
4985
                            );
4986
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
4987
                            ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
4988
                                $this,
4989
                                $feedback_type,
4990
                                $studentChoice,
4991
                                $studentChoiceDegree,
4992
                                $answer,
4993
                                $answerComment,
4994
                                $answerCorrect,
4995
                                $questionId,
4996
                                $results_disabled
4997
                            );
4998
                        } elseif (MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType) {
4999
                            ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5000
                                $this,
5001
                                $feedback_type,
5002
                                $answerType,
5003
                                $studentChoice,
5004
                                $answer,
5005
                                $answerComment,
5006
                                $answerCorrect,
5007
                                0,
5008
                                0,
5009
                                0,
5010
                                $results_disabled,
5011
                                $showTotalScoreAndUserChoicesInLastAttempt
5012
                            );
5013
                        } elseif (FILL_IN_BLANKS == $answerType) {
5014
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5015
                                $this,
5016
                                $feedback_type,
5017
                                $answer,
5018
                                0,
5019
                                0,
5020
                                $results_disabled,
5021
                                $showTotalScoreAndUserChoicesInLastAttempt,
5022
                                ''
5023
                            );
5024
                        } elseif (CALCULATED_ANSWER == $answerType) {
5025
                            ExerciseShowFunctions::display_calculated_answer(
5026
                                $this,
5027
                                $feedback_type,
5028
                                $answer,
5029
                                0,
5030
                                0,
5031
                                $results_disabled,
5032
                                $showTotalScoreAndUserChoicesInLastAttempt,
5033
                                $expectedAnswer,
5034
                                $calculatedChoice,
5035
                                $calculatedStatus
5036
                            );
5037
                        } elseif (FREE_ANSWER == $answerType) {
5038
                            ExerciseShowFunctions::display_free_answer(
5039
                                $feedback_type,
5040
                                $choice,
5041
                                $exeId,
5042
                                $questionId,
5043
                                $questionScore,
5044
                                $results_disabled
5045
                            );
5046
                        } elseif (ORAL_EXPRESSION == $answerType) {
5047
                            // to store the details of open questions in an array to be used in mail
5048
                            /** @var OralExpression $objQuestionTmp */
5049
                            ExerciseShowFunctions::display_oral_expression_answer(
5050
                                $feedback_type,
5051
                                $choice,
5052
                                $exeId,
5053
                                $questionId,
5054
                                $results_disabled,
5055
                                $questionScore,
5056
                                true
5057
                            );
5058
                        } elseif (HOT_SPOT == $answerType) {
5059
                            $correctAnswerId = 0;
5060
                            /** @var TrackEHotspot $hotspot */
5061
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5062
                                if ($hotspot->getHotspotAnswerId() == $answerAutoId) {
5063
                                    break;
5064
                                }
5065
                            }
5066
5067
                            // force to show whether the choice is correct or not
5068
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
5069
                            ExerciseShowFunctions::display_hotspot_answer(
5070
                                $this,
5071
                                $feedback_type,
5072
                                $answerId,
5073
                                $answer,
5074
                                $studentChoice,
5075
                                $answerComment,
5076
                                $results_disabled,
5077
                                $answerId,
5078
                                $showTotalScoreAndUserChoicesInLastAttempt
5079
                            );
5080
                        } elseif (HOT_SPOT_ORDER == $answerType) {
5081
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5082
                                $feedback_type,
5083
                                $answerId,
5084
                                $answer,
5085
                                $studentChoice,
5086
                                $answerComment
5087
                            );*/
5088
                        } elseif (HOT_SPOT_DELINEATION == $answerType && isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
5089
                            $user_answer = $_SESSION['exerciseResultCoordinates'][$questionId];
5090
5091
                            // Round-up the coordinates
5092
                            $coords = explode('/', $user_answer);
5093
                            $coords = array_filter($coords);
5094
                            $user_array = '';
5095
                            foreach ($coords as $coord) {
5096
                                if (!empty($coord)) {
5097
                                    $parts = explode(';', $coord);
5098
                                    if (!empty($parts)) {
5099
                                        $user_array .= round($parts[0]).';'.round($parts[1]).'/';
5100
                                    }
5101
                                }
5102
                            }
5103
                            $user_array = substr($user_array, 0, -1) ?: '';
5104
                            if ($next) {
5105
                                $user_answer = $user_array;
5106
                                // We compare only the delineation not the other points
5107
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5108
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5109
5110
                                // Calculating the area
5111
                                $poly_user = convert_coordinates($user_answer, '/');
5112
                                $poly_answer = convert_coordinates($answer_question, '|');
5113
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5114
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5115
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5116
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5117
5118
                                $overlap = $poly_results['both'];
5119
                                $poly_answer_area = $poly_results['s1'];
5120
                                $poly_user_area = $poly_results['s2'];
5121
                                $missing = $poly_results['s1Only'];
5122
                                $excess = $poly_results['s2Only'];
5123
5124
                                // //this is an area in pixels
5125
                                if ($debug > 0) {
5126
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5127
                                }
5128
5129
                                if ($overlap < 1) {
5130
                                    // Shortcut to avoid complicated calculations
5131
                                    $final_overlap = 0;
5132
                                    $final_missing = 100;
5133
                                    $final_excess = 100;
5134
                                } else {
5135
                                    // the final overlap is the percentage of the initial polygon
5136
                                    // that is overlapped by the user's polygon
5137
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5138
                                    if ($debug > 1) {
5139
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap, 0);
5140
                                    }
5141
                                    // the final missing area is the percentage of the initial polygon
5142
                                    // that is not overlapped by the user's polygon
5143
                                    $final_missing = 100 - $final_overlap;
5144
                                    if ($debug > 1) {
5145
                                        error_log(__LINE__.' - Final missing is '.$final_missing, 0);
5146
                                    }
5147
                                    // the final excess area is the percentage of the initial polygon's size
5148
                                    // that is covered by the user's polygon outside of the initial polygon
5149
                                    $final_excess = round(
5150
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5151
                                    );
5152
                                    if ($debug > 1) {
5153
                                        error_log(__LINE__.' - Final excess is '.$final_excess, 0);
5154
                                    }
5155
                                }
5156
5157
                                // Checking the destination parameters parsing the "@@"
5158
                                $destination_items = explode('@@', $answerDestination);
5159
                                $threadhold_total = $destination_items[0];
5160
                                $threadhold_items = explode(';', $threadhold_total);
5161
                                $threadhold1 = $threadhold_items[0]; // overlap
5162
                                $threadhold2 = $threadhold_items[1]; // excess
5163
                                $threadhold3 = $threadhold_items[2]; // missing
5164
5165
                                // if is delineation
5166
                                if (1 === $answerId) {
5167
                                    //setting colors
5168
                                    if ($final_overlap >= $threadhold1) {
5169
                                        $overlap_color = true;
5170
                                    }
5171
                                    if ($final_excess <= $threadhold2) {
5172
                                        $excess_color = true;
5173
                                    }
5174
                                    if ($final_missing <= $threadhold3) {
5175
                                        $missing_color = true;
5176
                                    }
5177
5178
                                    // if pass
5179
                                    if ($final_overlap >= $threadhold1 &&
5180
                                        $final_missing <= $threadhold3 &&
5181
                                        $final_excess <= $threadhold2
5182
                                    ) {
5183
                                        $next = 1; //go to the oars
5184
                                        $result_comment = get_lang('Acceptable');
5185
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5186
                                    } else {
5187
                                        $next = 0;
5188
                                        $result_comment = get_lang('Unacceptable');
5189
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5190
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5191
                                        // checking the destination parameters parsing the "@@"
5192
                                        $destination_items = explode('@@', $answerDestination);
5193
                                    }
5194
                                } elseif ($answerId > 1) {
5195
                                    if ('noerror' == $objAnswerTmp->selectHotspotType($answerId)) {
5196
                                        if ($debug > 0) {
5197
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5198
                                        }
5199
                                        //type no error shouldn't be treated
5200
                                        $next = 1;
5201
5202
                                        continue;
5203
                                    }
5204
                                    if ($debug > 0) {
5205
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5206
                                    }
5207
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5208
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5209
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5210
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5211
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5212
5213
                                    if (false == $overlap) {
5214
                                        //all good, no overlap
5215
                                        $next = 1;
5216
5217
                                        continue;
5218
                                    } else {
5219
                                        if ($debug > 0) {
5220
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5221
                                        }
5222
                                        $organs_at_risk_hit++;
5223
                                        //show the feedback
5224
                                        $next = 0;
5225
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5226
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5227
5228
                                        $destination_items = explode('@@', $answerDestination);
5229
                                        $try_hotspot = $destination_items[1];
5230
                                        $lp_hotspot = $destination_items[2];
5231
                                        $select_question_hotspot = $destination_items[3];
5232
                                        $url_hotspot = $destination_items[4];
5233
                                    }
5234
                                }
5235
                            } else {
5236
                                // the first delineation feedback
5237
                                if ($debug > 0) {
5238
                                    error_log(__LINE__.' first', 0);
5239
                                }
5240
                            }
5241
                        } elseif (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
5242
                            echo '<tr>';
5243
                            echo Display::tag('td', $answerMatching[$answerId]);
5244
                            echo Display::tag(
5245
                                'td',
5246
                                "$user_answer / ".Display::tag(
5247
                                    'strong',
5248
                                    $answerMatching[$answerCorrect],
5249
                                    ['style' => 'color: #008000; font-weight: bold;']
5250
                                )
5251
                            );
5252
                            echo '</tr>';
5253
                        } elseif (ANNOTATION == $answerType) {
5254
                            ExerciseShowFunctions::displayAnnotationAnswer(
5255
                                $feedback_type,
5256
                                $exeId,
5257
                                $questionId,
5258
                                $questionScore,
5259
                                $results_disabled
5260
                            );
5261
                        }
5262
                    }
5263
                } else {
5264
                    if ($debug) {
5265
                        error_log('Showing questions $from '.$from);
5266
                    }
5267
5268
                    switch ($answerType) {
5269
                        case UNIQUE_ANSWER:
5270
                        case UNIQUE_ANSWER_IMAGE:
5271
                        case UNIQUE_ANSWER_NO_OPTION:
5272
                        case MULTIPLE_ANSWER:
5273
                        case GLOBAL_MULTIPLE_ANSWER:
5274
                        case MULTIPLE_ANSWER_COMBINATION:
5275
                        case READING_COMPREHENSION:
5276
                            if (1 == $answerId) {
5277
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5278
                                    $this,
5279
                                    $feedback_type,
5280
                                    $answerType,
5281
                                    $studentChoice,
5282
                                    $answer,
5283
                                    $answerComment,
5284
                                    $answerCorrect,
5285
                                    $exeId,
5286
                                    $questionId,
5287
                                    $answerId,
5288
                                    $results_disabled,
5289
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5290
                                    $this->export
5291
                                );
5292
                            } else {
5293
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5294
                                    $this,
5295
                                    $feedback_type,
5296
                                    $answerType,
5297
                                    $studentChoice,
5298
                                    $answer,
5299
                                    $answerComment,
5300
                                    $answerCorrect,
5301
                                    $exeId,
5302
                                    $questionId,
5303
                                    '',
5304
                                    $results_disabled,
5305
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5306
                                    $this->export
5307
                                );
5308
                            }
5309
5310
                            break;
5311
                        case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
5312
                            if (1 == $answerId) {
5313
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5314
                                    $this,
5315
                                    $feedback_type,
5316
                                    $answerType,
5317
                                    $studentChoice,
5318
                                    $answer,
5319
                                    $answerComment,
5320
                                    $answerCorrect,
5321
                                    $exeId,
5322
                                    $questionId,
5323
                                    $answerId,
5324
                                    $results_disabled,
5325
                                    $showTotalScoreAndUserChoicesInLastAttempt
5326
                                );
5327
                            } else {
5328
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5329
                                    $this,
5330
                                    $feedback_type,
5331
                                    $answerType,
5332
                                    $studentChoice,
5333
                                    $answer,
5334
                                    $answerComment,
5335
                                    $answerCorrect,
5336
                                    $exeId,
5337
                                    $questionId,
5338
                                    '',
5339
                                    $results_disabled,
5340
                                    $showTotalScoreAndUserChoicesInLastAttempt
5341
                                );
5342
                            }
5343
5344
                            break;
5345
                        case MULTIPLE_ANSWER_TRUE_FALSE:
5346
                            if (1 == $answerId) {
5347
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5348
                                    $this,
5349
                                    $feedback_type,
5350
                                    $answerType,
5351
                                    $studentChoice,
5352
                                    $answer,
5353
                                    $answerComment,
5354
                                    $answerCorrect,
5355
                                    $exeId,
5356
                                    $questionId,
5357
                                    $answerId,
5358
                                    $results_disabled,
5359
                                    $showTotalScoreAndUserChoicesInLastAttempt
5360
                                );
5361
                            } else {
5362
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5363
                                    $this,
5364
                                    $feedback_type,
5365
                                    $answerType,
5366
                                    $studentChoice,
5367
                                    $answer,
5368
                                    $answerComment,
5369
                                    $answerCorrect,
5370
                                    $exeId,
5371
                                    $questionId,
5372
                                    '',
5373
                                    $results_disabled,
5374
                                    $showTotalScoreAndUserChoicesInLastAttempt
5375
                                );
5376
                            }
5377
5378
                            break;
5379
                        case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
5380
                            if (1 == $answerId) {
5381
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5382
                                    $this,
5383
                                    $feedback_type,
5384
                                    $studentChoice,
5385
                                    $studentChoiceDegree,
5386
                                    $answer,
5387
                                    $answerComment,
5388
                                    $answerCorrect,
5389
                                    $questionId,
5390
                                    $results_disabled
5391
                                );
5392
                            } else {
5393
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5394
                                    $this,
5395
                                    $feedback_type,
5396
                                    $studentChoice,
5397
                                    $studentChoiceDegree,
5398
                                    $answer,
5399
                                    $answerComment,
5400
                                    $answerCorrect,
5401
                                    $questionId,
5402
                                    $results_disabled
5403
                                );
5404
                            }
5405
5406
                            break;
5407
                        case FILL_IN_BLANKS:
5408
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5409
                                $this,
5410
                                $feedback_type,
5411
                                $answer,
5412
                                $exeId,
5413
                                $questionId,
5414
                                $results_disabled,
5415
                                $showTotalScoreAndUserChoicesInLastAttempt,
5416
                                $str
5417
                            );
5418
5419
                            break;
5420
                        case CALCULATED_ANSWER:
5421
                            ExerciseShowFunctions::display_calculated_answer(
5422
                                $this,
5423
                                $feedback_type,
5424
                                $answer,
5425
                                $exeId,
5426
                                $questionId,
5427
                                $results_disabled,
5428
                                '',
5429
                                $showTotalScoreAndUserChoicesInLastAttempt
5430
                            );
5431
5432
                            break;
5433
                        case FREE_ANSWER:
5434
                            echo ExerciseShowFunctions::display_free_answer(
5435
                                $feedback_type,
5436
                                $choice,
5437
                                $exeId,
5438
                                $questionId,
5439
                                $questionScore,
5440
                                $results_disabled
5441
                            );
5442
5443
                            break;
5444
                        case ORAL_EXPRESSION:
5445
                            /** @var OralExpression $objQuestionTmp */
5446
                            echo '<tr>
5447
                                <td valign="top">'.
5448
                                ExerciseShowFunctions::display_oral_expression_answer(
5449
                                    $feedback_type,
5450
                                    $choice,
5451
                                    $exeId,
5452
                                    $questionId,
5453
                                    $results_disabled,
5454
                                    $questionScore
5455
                                ).'</td>
5456
                                </tr>
5457
                                </table>';
5458
                            break;
5459
                        case HOT_SPOT:
5460
                            $correctAnswerId = 0;
5461
5462
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5463
                                if ($hotspot->getHotspotAnswerId() == $foundAnswerId) {
5464
                                    break;
5465
                                }
5466
                            }
5467
                            ExerciseShowFunctions::display_hotspot_answer(
5468
                                $this,
5469
                                $feedback_type,
5470
                                $answerId,
5471
                                $answer,
5472
                                $studentChoice,
5473
                                $answerComment,
5474
                                $results_disabled,
5475
                                $answerId,
5476
                                $showTotalScoreAndUserChoicesInLastAttempt
5477
                            );
5478
5479
                            break;
5480
                        case HOT_SPOT_DELINEATION:
5481
                            $user_answer = $user_array;
5482
                            if ($next) {
5483
                                $user_answer = $user_array;
5484
                                // we compare only the delineation not the other points
5485
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5486
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5487
5488
                                // calculating the area
5489
                                $poly_user = convert_coordinates($user_answer, '/');
5490
                                $poly_answer = convert_coordinates($answer_question, '|');
5491
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5492
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5493
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5494
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5495
5496
                                $overlap = $poly_results['both'];
5497
                                $poly_answer_area = $poly_results['s1'];
5498
                                $poly_user_area = $poly_results['s2'];
5499
                                $missing = $poly_results['s1Only'];
5500
                                $excess = $poly_results['s2Only'];
5501
                                if ($debug > 0) {
5502
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5503
                                }
5504
                                if ($overlap < 1) {
5505
                                    //shortcut to avoid complicated calculations
5506
                                    $final_overlap = 0;
5507
                                    $final_missing = 100;
5508
                                    $final_excess = 100;
5509
                                } else {
5510
                                    // the final overlap is the percentage of the initial polygon
5511
                                    // that is overlapped by the user's polygon
5512
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5513
5514
                                    // the final missing area is the percentage of the initial polygon that
5515
                                    // is not overlapped by the user's polygon
5516
                                    $final_missing = 100 - $final_overlap;
5517
                                    // the final excess area is the percentage of the initial polygon's size that is
5518
                                    // covered by the user's polygon outside of the initial polygon
5519
                                    $final_excess = round(
5520
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5521
                                    );
5522
5523
                                    if ($debug > 1) {
5524
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap);
5525
                                        error_log(__LINE__.' - Final excess is '.$final_excess);
5526
                                        error_log(__LINE__.' - Final missing is '.$final_missing);
5527
                                    }
5528
                                }
5529
5530
                                // Checking the destination parameters parsing the "@@"
5531
                                $destination_items = explode('@@', $answerDestination);
5532
                                $threadhold_total = $destination_items[0];
5533
                                $threadhold_items = explode(';', $threadhold_total);
5534
                                $threadhold1 = $threadhold_items[0]; // overlap
5535
                                $threadhold2 = $threadhold_items[1]; // excess
5536
                                $threadhold3 = $threadhold_items[2]; //missing
5537
                                // if is delineation
5538
                                if (1 === $answerId) {
5539
                                    //setting colors
5540
                                    if ($final_overlap >= $threadhold1) {
5541
                                        $overlap_color = true;
5542
                                    }
5543
                                    if ($final_excess <= $threadhold2) {
5544
                                        $excess_color = true;
5545
                                    }
5546
                                    if ($final_missing <= $threadhold3) {
5547
                                        $missing_color = true;
5548
                                    }
5549
5550
                                    // if pass
5551
                                    if ($final_overlap >= $threadhold1 &&
5552
                                        $final_missing <= $threadhold3 &&
5553
                                        $final_excess <= $threadhold2
5554
                                    ) {
5555
                                        $next = 1; //go to the oars
5556
                                        $result_comment = get_lang('Acceptable');
5557
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5558
                                    } else {
5559
                                        $next = 0;
5560
                                        $result_comment = get_lang('Unacceptable');
5561
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5562
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5563
                                        //checking the destination parameters parsing the "@@"
5564
                                        $destination_items = explode('@@', $answerDestination);
5565
                                    }
5566
                                } elseif ($answerId > 1) {
5567
                                    if ('noerror' === $objAnswerTmp->selectHotspotType($answerId)) {
5568
                                        if ($debug > 0) {
5569
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5570
                                        }
5571
                                        //type no error shouldn't be treated
5572
                                        $next = 1;
5573
5574
                                        break;
5575
                                    }
5576
                                    if ($debug > 0) {
5577
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5578
                                    }
5579
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5580
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5581
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5582
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5583
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5584
5585
                                    if (false == $overlap) {
5586
                                        //all good, no overlap
5587
                                        $next = 1;
5588
5589
                                        break;
5590
                                    } else {
5591
                                        if ($debug > 0) {
5592
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5593
                                        }
5594
                                        $organs_at_risk_hit++;
5595
                                        //show the feedback
5596
                                        $next = 0;
5597
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5598
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5599
5600
                                        $destination_items = explode('@@', $answerDestination);
5601
                                        $try_hotspot = $destination_items[1];
5602
                                        $lp_hotspot = $destination_items[2];
5603
                                        $select_question_hotspot = $destination_items[3];
5604
                                        $url_hotspot = $destination_items[4];
5605
                                    }
5606
                                }
5607
                            }
5608
5609
                            break;
5610
                        case HOT_SPOT_ORDER:
5611
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5612
                                $feedback_type,
5613
                                $answerId,
5614
                                $answer,
5615
                                $studentChoice,
5616
                                $answerComment
5617
                            );*/
5618
5619
                            break;
5620
                        case DRAGGABLE:
5621
                        case MATCHING_DRAGGABLE:
5622
                        case MATCHING:
5623
                            echo '<tr>';
5624
                            echo Display::tag('td', $answerMatching[$answerId]);
5625
                            echo Display::tag(
5626
                                'td',
5627
                                "$user_answer / ".Display::tag(
5628
                                    'strong',
5629
                                    $answerMatching[$answerCorrect],
5630
                                    ['style' => 'color: #008000; font-weight: bold;']
5631
                                )
5632
                            );
5633
                            echo '</tr>';
5634
5635
                            break;
5636
                        case ANNOTATION:
5637
                            ExerciseShowFunctions::displayAnnotationAnswer(
5638
                                $feedback_type,
5639
                                $exeId,
5640
                                $questionId,
5641
                                $questionScore,
5642
                                $results_disabled
5643
                            );
5644
5645
                            break;
5646
                    }
5647
                }
5648
            }
5649
        } // end for that loops over all answers of the current question
5650
5651
        if ($debug) {
5652
            error_log('-- End answer loop --');
5653
        }
5654
5655
        $final_answer = true;
5656
5657
        foreach ($real_answers as $my_answer) {
5658
            if (!$my_answer) {
5659
                $final_answer = false;
5660
            }
5661
        }
5662
5663
        //we add the total score after dealing with the answers
5664
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
5665
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
5666
        ) {
5667
            if ($final_answer) {
5668
                //getting only the first score where we save the weight of all the question
5669
                $answerWeighting = $objAnswerTmp->selectWeighting(1);
5670
                if (empty($answerWeighting) && !empty($firstAnswer) && isset($firstAnswer['ponderation'])) {
5671
                    $answerWeighting = $firstAnswer['ponderation'];
5672
                }
5673
                $questionScore += $answerWeighting;
5674
            }
5675
        }
5676
5677
        $extra_data = [
5678
            'final_overlap' => $final_overlap,
5679
            'final_missing' => $final_missing,
5680
            'final_excess' => $final_excess,
5681
            'overlap_color' => $overlap_color,
5682
            'missing_color' => $missing_color,
5683
            'excess_color' => $excess_color,
5684
            'threadhold1' => $threadhold1,
5685
            'threadhold2' => $threadhold2,
5686
            'threadhold3' => $threadhold3,
5687
        ];
5688
5689
        if ('exercise_result' === $from) {
5690
            // if answer is hotspot. To the difference of exercise_show.php,
5691
            //  we use the results from the session (from_db=0)
5692
            // TODO Change this, because it is wrong to show the user
5693
            //  some results that haven't been stored in the database yet
5694
            if (HOT_SPOT == $answerType || HOT_SPOT_ORDER == $answerType || HOT_SPOT_DELINEATION == $answerType) {
5695
                if ($debug) {
5696
                    error_log('$from AND this is a hotspot kind of question ');
5697
                }
5698
                if (HOT_SPOT_DELINEATION === $answerType) {
5699
                    if ($showHotSpotDelineationTable) {
5700
                        if (!is_numeric($final_overlap)) {
5701
                            $final_overlap = 0;
5702
                        }
5703
                        if (!is_numeric($final_missing)) {
5704
                            $final_missing = 0;
5705
                        }
5706
                        if (!is_numeric($final_excess)) {
5707
                            $final_excess = 0;
5708
                        }
5709
5710
                        if ($final_overlap > 100) {
5711
                            $final_overlap = 100;
5712
                        }
5713
5714
                        $overlap = 0;
5715
                        if ($final_overlap > 0) {
5716
                            $overlap = (int) $final_overlap;
5717
                        }
5718
5719
                        $excess = 0;
5720
                        if ($final_excess > 0) {
5721
                            $excess = (int) $final_excess;
5722
                        }
5723
5724
                        $missing = 0;
5725
                        if ($final_missing > 0) {
5726
                            $missing = (int) $final_missing;
5727
                        }
5728
5729
                        $table_resume = '<table class="table table-hover table-striped data_table">
5730
                                <tr class="row_odd" >
5731
                                    <td></td>
5732
                                    <td ><b>'.get_lang('Requirements').'</b></td>
5733
                                    <td><b>'.get_lang('Your answer').'</b></td>
5734
                                </tr>
5735
                                <tr class="row_even">
5736
                                    <td><b>'.get_lang('Overlapping area').'</b></td>
5737
                                    <td>'.get_lang('Minimum').' '.$threadhold1.'</td>
5738
                                    <td class="text-right '.($overlap_color ? 'text-success' : 'text-error').'">'
5739
                                    .$overlap.'</td>
5740
                                </tr>
5741
                                <tr>
5742
                                    <td><b>'.get_lang('Excessive area').'</b></td>
5743
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold2.'</td>
5744
                                    <td class="text-right '.($excess_color ? 'text-success' : 'text-error').'">'
5745
                                    .$excess.'</td>
5746
                                </tr>
5747
                                <tr class="row_even">
5748
                                    <td><b>'.get_lang('Missing area').'</b></td>
5749
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold3.'</td>
5750
                                    <td class="text-right '.($missing_color ? 'text-success' : 'text-error').'">'
5751
                                    .$missing.'</td>
5752
                                </tr>
5753
                            </table>';
5754
                        if (0 == $next) {
5755
                        } else {
5756
                            $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
5757
                            $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
5758
                        }
5759
5760
                        $message = '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>
5761
                                    <p style="text-align:center">';
5762
                        $message .= '<p>'.get_lang('Your delineation :').'</p>';
5763
                        $message .= $table_resume;
5764
                        $message .= '<br />'.get_lang('Your result is :').' '.$result_comment.'<br />';
5765
                        if ($organs_at_risk_hit > 0) {
5766
                            $message .= '<p><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
5767
                        }
5768
                        $message .= '<p>'.$comment.'</p>';
5769
                        echo $message;
5770
5771
                        $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][0] = $message;
5772
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
5773
                            $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][1] = $_SESSION['exerciseResultCoordinates'][$questionId];
5774
                        }
5775
                    } else {
5776
                        echo $hotspot_delineation_result[0] ?? '';
5777
                    }
5778
5779
                    // Save the score attempts
5780
                    if (1) {
5781
                        //getting the answer 1 or 0 comes from exercise_submit_modal.php
5782
                        $final_answer = $hotspot_delineation_result[1] ?? '';
5783
                        if (0 == $final_answer) {
5784
                            $questionScore = 0;
5785
                        }
5786
                        // we always insert the answer_id 1 = delineation
5787
                        Event::saveQuestionAttempt($this, $questionScore, 1, $quesId, $exeId, 0);
5788
                        //in delineation mode, get the answer from $hotspot_delineation_result[1]
5789
                        $hotspotValue = isset($hotspot_delineation_result[1]) ? 1 === (int) $hotspot_delineation_result[1] ? 1 : 0 : 0;
5790
                        Event::saveExerciseAttemptHotspot(
5791
                            $this,
5792
                            $exeId,
5793
                            $quesId,
5794
                            1,
5795
                            $hotspotValue,
5796
                            $exerciseResultCoordinates[$quesId] ?? '',
5797
                            false,
5798
                            0,
5799
                            $learnpath_id,
5800
                            $learnpath_item_id
5801
                        );
5802
                    } else {
5803
                        if (0 == $final_answer) {
5804
                            $questionScore = 0;
5805
                            $answer = 0;
5806
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
5807
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5808
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5809
                                    Event::saveExerciseAttemptHotspot(
5810
                                        $this,
5811
                                        $exeId,
5812
                                        $quesId,
5813
                                        $idx,
5814
                                        0,
5815
                                        $val,
5816
                                        false,
5817
                                        0,
5818
                                        $learnpath_id,
5819
                                        $learnpath_item_id
5820
                                    );
5821
                                }
5822
                            }
5823
                        } else {
5824
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
5825
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5826
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5827
                                    $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
5828
                                    Event::saveExerciseAttemptHotspot(
5829
                                        $this,
5830
                                        $exeId,
5831
                                        $quesId,
5832
                                        $idx,
5833
                                        $hotspotValue,
5834
                                        $val,
5835
                                        false,
5836
                                        0,
5837
                                        $learnpath_id,
5838
                                        $learnpath_item_id
5839
                                    );
5840
                                }
5841
                            }
5842
                        }
5843
                    }
5844
                }
5845
            }
5846
5847
            $relPath = api_get_path(WEB_CODE_PATH);
5848
5849
            if (HOT_SPOT == $answerType || HOT_SPOT_ORDER == $answerType) {
5850
                // We made an extra table for the answers
5851
                if ($show_result) {
5852
                    echo '</table></td></tr>';
5853
                    echo '
5854
                        <tr>
5855
                            <td colspan="2">
5856
                                <p><em>'.get_lang('Image zones')."</em></p>
5857
                                <div id=\"hotspot-solution-$questionId\"></div>
5858
                                <script>
5859
                                    $(function() {
5860
                                        new HotspotQuestion({
5861
                                            questionId: $questionId,
5862
                                            exerciseId: {$this->getId()},
5863
                                            exeId: $exeId,
5864
                                            selector: '#hotspot-solution-$questionId',
5865
                                            for: 'solution',
5866
                                            relPath: '$relPath'
5867
                                        });
5868
                                    });
5869
                                </script>
5870
                            </td>
5871
                        </tr>
5872
                    ";
5873
                }
5874
            } elseif (ANNOTATION == $answerType) {
5875
                if ($show_result) {
5876
                    echo '
5877
                        <p><em>'.get_lang('Annotation').'</em></p>
5878
                        <div id="annotation-canvas-'.$questionId.'"></div>
5879
                        <script>
5880
                            AnnotationQuestion({
5881
                                questionId: parseInt('.$questionId.'),
5882
                                exerciseId: parseInt('.$exeId.'),
5883
                                relPath: \''.$relPath.'\',
5884
                                courseId: parseInt('.$course_id.')
5885
                            });
5886
                        </script>
5887
                    ';
5888
                }
5889
            }
5890
5891
            if ($show_result && ANNOTATION != $answerType) {
5892
                echo '</table>';
5893
            }
5894
        }
5895
        unset($objAnswerTmp);
5896
5897
        $totalWeighting += $questionWeighting;
5898
        // Store results directly in the database
5899
        // For all in one page exercises, the results will be
5900
        // stored by exercise_results.php (using the session)
5901
        if (in_array(
5902
            $objQuestionTmp->type,
5903
            [PAGE_BREAK, MEDIA_QUESTION],
5904
            true
5905
        )) {
5906
            $save_results = false;
5907
        }
5908
        if ($save_results) {
5909
            if ($debug) {
5910
                error_log("Save question results $save_results");
5911
                error_log("Question score: $questionScore");
5912
                error_log('choice: ');
5913
                error_log(print_r($choice, 1));
5914
            }
5915
5916
            if (empty($choice)) {
5917
                $choice = 0;
5918
            }
5919
            // with certainty degree
5920
            if (empty($choiceDegreeCertainty)) {
5921
                $choiceDegreeCertainty = 0;
5922
            }
5923
            if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
5924
                MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType ||
5925
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
5926
            ) {
5927
                if (0 != $choice) {
5928
                    $reply = array_keys($choice);
5929
                    $countReply = count($reply);
5930
                    for ($i = 0; $i < $countReply; $i++) {
5931
                        $chosenAnswer = $reply[$i];
5932
                        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
5933
                            if (0 != $choiceDegreeCertainty) {
5934
                                $replyDegreeCertainty = array_keys($choiceDegreeCertainty);
5935
                                $answerDegreeCertainty = isset($replyDegreeCertainty[$i]) ? $replyDegreeCertainty[$i] : '';
5936
                                $answerValue = isset($choiceDegreeCertainty[$answerDegreeCertainty]) ? $choiceDegreeCertainty[$answerDegreeCertainty] : '';
5937
                                Event::saveQuestionAttempt(
5938
                                    $this,
5939
                                    $questionScore,
5940
                                    $chosenAnswer.':'.$choice[$chosenAnswer].':'.$answerValue,
5941
                                    $quesId,
5942
                                    $exeId,
5943
                                    $i,
5944
                                    $this->getId(),
5945
                                    $updateResults,
5946
                                    $questionDuration
5947
                                );
5948
                            }
5949
                        } else {
5950
                            Event::saveQuestionAttempt(
5951
                                $this,
5952
                                $questionScore,
5953
                                $chosenAnswer.':'.$choice[$chosenAnswer],
5954
                                $quesId,
5955
                                $exeId,
5956
                                $i,
5957
                                $this->getId(),
5958
                                $updateResults,
5959
                                $questionDuration
5960
                            );
5961
                        }
5962
                        if ($debug) {
5963
                            error_log('result =>'.$questionScore.' '.$chosenAnswer.':'.$choice[$chosenAnswer]);
5964
                        }
5965
                    }
5966
                } else {
5967
                    Event::saveQuestionAttempt(
5968
                        $this,
5969
                        $questionScore,
5970
                        0,
5971
                        $quesId,
5972
                        $exeId,
5973
                        0,
5974
                        $this->getId(),
5975
                        false,
5976
                        $questionDuration
5977
                    );
5978
                }
5979
            } elseif (MULTIPLE_ANSWER == $answerType || GLOBAL_MULTIPLE_ANSWER == $answerType) {
5980
                if (0 != $choice) {
5981
                    $reply = array_keys($choice);
5982
                    for ($i = 0; $i < count($reply); $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...
5983
                        $ans = $reply[$i];
5984
                        Event::saveQuestionAttempt(
5985
                            $this,
5986
                            $questionScore,
5987
                            $ans,
5988
                            $quesId,
5989
                            $exeId,
5990
                            $i,
5991
                            $this->id,
5992
                            false,
5993
                            $questionDuration
5994
                        );
5995
                    }
5996
                } else {
5997
                    Event::saveQuestionAttempt(
5998
                        $this,
5999
                        $questionScore,
6000
                        0,
6001
                        $quesId,
6002
                        $exeId,
6003
                        0,
6004
                        $this->id,
6005
                        false,
6006
                        $questionDuration
6007
                    );
6008
                }
6009
            } elseif (MULTIPLE_ANSWER_COMBINATION == $answerType) {
6010
                if (0 != $choice) {
6011
                    $reply = array_keys($choice);
6012
                    for ($i = 0; $i < count($reply); $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...
6013
                        $ans = $reply[$i];
6014
                        Event::saveQuestionAttempt(
6015
                            $this,
6016
                            $questionScore,
6017
                            $ans,
6018
                            $quesId,
6019
                            $exeId,
6020
                            $i,
6021
                            $this->id,
6022
                            false,
6023
                            $questionDuration
6024
                        );
6025
                    }
6026
                } else {
6027
                    Event::saveQuestionAttempt(
6028
                        $this,
6029
                        $questionScore,
6030
                        0,
6031
                        $quesId,
6032
                        $exeId,
6033
                        0,
6034
                        $this->id,
6035
                        false,
6036
                        $questionDuration
6037
                    );
6038
                }
6039
            } elseif (in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE])) {
6040
                if (isset($matching)) {
6041
                    foreach ($matching as $j => $val) {
6042
                        Event::saveQuestionAttempt(
6043
                            $this,
6044
                            $questionScore,
6045
                            $val,
6046
                            $quesId,
6047
                            $exeId,
6048
                            $j,
6049
                            $this->id,
6050
                            false,
6051
                            $questionDuration
6052
                        );
6053
                    }
6054
                }
6055
            } elseif (FREE_ANSWER == $answerType) {
6056
                $answer = $choice;
6057
                Event::saveQuestionAttempt(
6058
                    $this,
6059
                    $questionScore,
6060
                    $answer,
6061
                    $quesId,
6062
                    $exeId,
6063
                    0,
6064
                    $this->id,
6065
                    false,
6066
                    $questionDuration
6067
                );
6068
            } elseif (ORAL_EXPRESSION == $answerType) {
6069
                $answer = $choice;
6070
                /** @var OralExpression $objQuestionTmp */
6071
                $questionAttemptId = Event::saveQuestionAttempt(
6072
                    $this,
6073
                    $questionScore,
6074
                    $answer,
6075
                    $quesId,
6076
                    $exeId,
6077
                    0,
6078
                    $this->id,
6079
                    false,
6080
                    $questionDuration
6081
                );
6082
6083
                if (false !== $questionAttemptId) {
6084
                    OralExpression::saveAssetInQuestionAttempt($questionAttemptId);
6085
                }
6086
            } elseif (
6087
            in_array(
6088
                $answerType,
6089
                [UNIQUE_ANSWER, UNIQUE_ANSWER_IMAGE, UNIQUE_ANSWER_NO_OPTION, READING_COMPREHENSION]
6090
            )
6091
            ) {
6092
                $answer = $choice;
6093
                Event::saveQuestionAttempt(
6094
                    $this,
6095
                    $questionScore,
6096
                    $answer,
6097
                    $quesId,
6098
                    $exeId,
6099
                    0,
6100
                    $this->id,
6101
                    false,
6102
                    $questionDuration
6103
                );
6104
            } elseif (HOT_SPOT == $answerType || ANNOTATION == $answerType) {
6105
                $answer = [];
6106
                if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) {
6107
                    if ($debug) {
6108
                        error_log('Checking result coordinates');
6109
                    }
6110
                    Database::delete(
6111
                        Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
6112
                        [
6113
                            'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
6114
                                $exeId,
6115
                                $questionId,
6116
                                api_get_course_int_id(),
6117
                            ],
6118
                        ]
6119
                    );
6120
6121
                    foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
6122
                        $answer[] = $val;
6123
                        $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
6124
                        if ($debug) {
6125
                            error_log('Hotspot value: '.$hotspotValue);
6126
                        }
6127
                        Event::saveExerciseAttemptHotspot(
6128
                            $this,
6129
                            $exeId,
6130
                            $quesId,
6131
                            $idx,
6132
                            $hotspotValue,
6133
                            $val,
6134
                            false,
6135
                            $this->id,
6136
                            $learnpath_id,
6137
                            $learnpath_item_id
6138
                        );
6139
                    }
6140
                } else {
6141
                    if ($debug) {
6142
                        error_log('Empty: exerciseResultCoordinates');
6143
                    }
6144
                }
6145
                Event::saveQuestionAttempt(
6146
                    $this,
6147
                    $questionScore,
6148
                    implode('|', $answer),
6149
                    $quesId,
6150
                    $exeId,
6151
                    0,
6152
                    $this->id,
6153
                    false,
6154
                    $questionDuration
6155
                );
6156
            } else {
6157
                Event::saveQuestionAttempt(
6158
                    $this,
6159
                    $questionScore,
6160
                    $answer,
6161
                    $quesId,
6162
                    $exeId,
6163
                    0,
6164
                    $this->id,
6165
                    false,
6166
                    $questionDuration
6167
                );
6168
            }
6169
        }
6170
6171
        if (0 == $propagate_neg && $questionScore < 0) {
6172
            $questionScore = 0;
6173
        }
6174
6175
        if ($save_results) {
6176
            $statsTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6177
            $sql = "UPDATE $statsTable SET
6178
                        score = score + ".(float) $questionScore."
6179
                    WHERE exe_id = $exeId";
6180
            Database::query($sql);
6181
        }
6182
6183
        return [
6184
            'score' => $questionScore,
6185
            'weight' => $questionWeighting,
6186
            'extra' => $extra_data,
6187
            'open_question' => $arrques,
6188
            'open_answer' => $arrans,
6189
            'answer_type' => $answerType,
6190
            'generated_oral_file' => $generatedFilesHtml,
6191
            'user_answered' => $userAnsweredQuestion,
6192
            'correct_answer_id' => $correctAnswerId,
6193
            'answer_destination' => $answerDestination,
6194
        ];
6195
    }
6196
6197
    /**
6198
     * Sends a notification when a user ends an examn.
6199
     *
6200
     * @param string $type                  'start' or 'end' of an exercise
6201
     * @param array  $question_list_answers
6202
     * @param string $origin
6203
     * @param int    $exe_id
6204
     * @param float  $score
6205
     * @param float  $weight
6206
     *
6207
     * @return bool
6208
     */
6209
    public function send_mail_notification_for_exam(
6210
        $type,
6211
        $question_list_answers,
6212
        $origin,
6213
        $exe_id,
6214
        $score = null,
6215
        $weight = null
6216
    ) {
6217
        $setting = api_get_course_setting('email_alert_manager_on_new_quiz');
6218
6219
        if ((empty($setting) || !is_array($setting)) && empty($this->getNotifications())) {
6220
            return false;
6221
        }
6222
6223
        $settingFromExercise = $this->getNotifications();
6224
        if (!empty($settingFromExercise)) {
6225
            $setting = $settingFromExercise;
6226
        }
6227
6228
        // Email configuration settings
6229
        $courseCode = api_get_course_id();
6230
        $courseInfo = api_get_course_info($courseCode);
6231
6232
        if (empty($courseInfo)) {
6233
            return false;
6234
        }
6235
6236
        $sessionId = api_get_session_id();
6237
6238
        $sessionData = '';
6239
        if (!empty($sessionId)) {
6240
            $sessionInfo = api_get_session_info($sessionId);
6241
            if (!empty($sessionInfo)) {
6242
                $sessionData = '<tr>'
6243
                    .'<td>'.get_lang('Session name').'</td>'
6244
                    .'<td>'.$sessionInfo['name'].'</td>'
6245
                    .'</tr>';
6246
            }
6247
        }
6248
6249
        $sendStart = false;
6250
        $sendEnd = false;
6251
        $sendEndOpenQuestion = false;
6252
        $sendEndOralQuestion = false;
6253
6254
        foreach ($setting as $option) {
6255
            switch ($option) {
6256
                case 0:
6257
                    return false;
6258
6259
                    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...
6260
                case 1: // End
6261
                    if ('end' == $type) {
6262
                        $sendEnd = true;
6263
                    }
6264
6265
                    break;
6266
                case 2: // start
6267
                    if ('start' == $type) {
6268
                        $sendStart = true;
6269
                    }
6270
6271
                    break;
6272
                case 3: // end + open
6273
                    if ('end' == $type) {
6274
                        $sendEndOpenQuestion = true;
6275
                    }
6276
6277
                    break;
6278
                case 4: // end + oral
6279
                    if ('end' == $type) {
6280
                        $sendEndOralQuestion = true;
6281
                    }
6282
6283
                    break;
6284
            }
6285
        }
6286
6287
        $user_info = api_get_user_info(api_get_user_id());
6288
        $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_show.php?'.
6289
            api_get_cidreq(true, true, 'qualify').'&id='.$exe_id.'&action=qualify';
6290
6291
        if (!empty($sessionId)) {
6292
            $addGeneralCoach = true;
6293
            $setting = ('true' === api_get_setting('exercise.block_quiz_mail_notification_general_coach'));
6294
            if (true === $setting) {
6295
                $addGeneralCoach = false;
6296
            }
6297
            $teachers = CourseManager::get_coach_list_from_course_code(
6298
                $courseCode,
6299
                $sessionId,
6300
                $addGeneralCoach
6301
            );
6302
        } else {
6303
            $teachers = CourseManager::get_teacher_list_from_course_code($courseCode);
6304
        }
6305
6306
        if ($sendEndOpenQuestion) {
6307
            $this->sendNotificationForOpenQuestions(
6308
                $question_list_answers,
6309
                $origin,
6310
                $user_info,
6311
                $url,
6312
                $teachers
6313
            );
6314
        }
6315
6316
        if ($sendEndOralQuestion) {
6317
            $this->sendNotificationForOralQuestions(
6318
                $question_list_answers,
6319
                $origin,
6320
                $exe_id,
6321
                $user_info,
6322
                $url,
6323
                $teachers
6324
            );
6325
        }
6326
6327
        if (!$sendEnd && !$sendStart) {
6328
            return false;
6329
        }
6330
6331
        $scoreLabel = '';
6332
        if ($sendEnd &&
6333
            ('true' === api_get_setting('exercise.send_score_in_exam_notification_mail_to_manager'))
6334
        ) {
6335
            $notificationPercentage = ('true' === api_get_setting('mail.send_notification_score_in_percentage'));
6336
            $scoreLabel = ExerciseLib::show_score($score, $weight, $notificationPercentage, true);
6337
            $scoreLabel = '<tr>
6338
                            <td>'.get_lang('Score')."</td>
6339
                            <td>&nbsp;$scoreLabel</td>
6340
                        </tr>";
6341
        }
6342
6343
        if ($sendEnd) {
6344
            $msg = get_lang('A learner attempted an exercise').'<br /><br />';
6345
        } else {
6346
            $msg = get_lang('Student just started an exercise').'<br /><br />';
6347
        }
6348
6349
        $msg .= get_lang('Attempt details').' : <br /><br />
6350
                    <table>
6351
                        <tr>
6352
                            <td>'.get_lang('Course name').'</td>
6353
                            <td>#course#</td>
6354
                        </tr>
6355
                        '.$sessionData.'
6356
                        <tr>
6357
                            <td>'.get_lang('Test').'</td>
6358
                            <td>&nbsp;#exercise#</td>
6359
                        </tr>
6360
                        <tr>
6361
                            <td>'.get_lang('Learner name').'</td>
6362
                            <td>&nbsp;#student_complete_name#</td>
6363
                        </tr>
6364
                        <tr>
6365
                            <td>'.get_lang('Learner e-mail').'</td>
6366
                            <td>&nbsp;#email#</td>
6367
                        </tr>
6368
                        '.$scoreLabel.'
6369
                    </table>';
6370
6371
        $variables = [
6372
            '#email#' => $user_info['email'],
6373
            '#exercise#' => $this->exercise,
6374
            '#student_complete_name#' => $user_info['complete_name'],
6375
            '#course#' => Display::url(
6376
                $courseInfo['title'],
6377
                $courseInfo['course_public_url'].'?sid='.$sessionId
6378
            ),
6379
        ];
6380
6381
        if ($sendEnd) {
6382
            $msg .= '<br /><a href="#url#">'.get_lang(
6383
                    'Click this link to check the answer and/or give feedback'
6384
                ).'</a>';
6385
            $variables['#url#'] = $url;
6386
        }
6387
6388
        $content = str_replace(array_keys($variables), array_values($variables), $msg);
6389
6390
        if ($sendEnd) {
6391
            $subject = get_lang('A learner attempted an exercise');
6392
        } else {
6393
            $subject = get_lang('Student just started an exercise');
6394
        }
6395
6396
        if (!empty($teachers)) {
6397
            foreach ($teachers as $user_id => $teacher_data) {
6398
                MessageManager::send_message_simple(
6399
                    $user_id,
6400
                    $subject,
6401
                    $content
6402
                );
6403
            }
6404
        }
6405
    }
6406
6407
    /**
6408
     * @param array $user_data         result of api_get_user_info()
6409
     * @param array $trackExerciseInfo result of get_stat_track_exercise_info
6410
     * @param bool  $saveUserResult
6411
     * @param bool  $allowSignature
6412
     * @param bool  $allowExportPdf
6413
     *
6414
     * @return string
6415
     */
6416
    public function showExerciseResultHeader(
6417
        $user_data,
6418
        $trackExerciseInfo,
6419
        $saveUserResult,
6420
        $allowSignature = false,
6421
        $allowExportPdf = false
6422
    ) {
6423
        if ('true' === api_get_setting('exercise.hide_user_info_in_quiz_result')) {
6424
            return '';
6425
        }
6426
6427
        $start_date = null;
6428
        if (isset($trackExerciseInfo['start_date'])) {
6429
            $start_date = api_convert_and_format_date($trackExerciseInfo['start_date']);
6430
        }
6431
        $duration = isset($trackExerciseInfo['duration_formatted']) ? $trackExerciseInfo['duration_formatted'] : null;
6432
        $ip = isset($trackExerciseInfo['user_ip']) ? $trackExerciseInfo['user_ip'] : null;
6433
6434
        if (!empty($user_data)) {
6435
            $userFullName = $user_data['complete_name'];
6436
            if (api_is_teacher() || api_is_platform_admin(true, true)) {
6437
                $userFullName = '<a href="'.$user_data['profile_url'].'" title="'.get_lang('Go to learner details').'">'.
6438
                    $user_data['complete_name'].'</a>';
6439
            }
6440
6441
            $data = [
6442
                'name_url' => $userFullName,
6443
                'complete_name' => $user_data['complete_name'],
6444
                'username' => $user_data['username'],
6445
                'avatar' => $user_data['avatar_medium'],
6446
                'url' => $user_data['profile_url'],
6447
            ];
6448
6449
            if (!empty($user_data['official_code'])) {
6450
                $data['code'] = $user_data['official_code'];
6451
            }
6452
        }
6453
        // Description can be very long and is generally meant to explain
6454
        //   rules *before* the exam. Leaving here to make display easier if
6455
        //   necessary
6456
        /*
6457
        if (!empty($this->description)) {
6458
            $array[] = array('title' => get_lang("Description"), 'content' => $this->description);
6459
        }
6460
        */
6461
6462
        $data['start_date'] = $start_date;
6463
        $data['duration'] = $duration;
6464
        $data['ip'] = $ip;
6465
6466
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
6467
            $data['title'] = $this->get_formated_title().get_lang('Result');
6468
        } else {
6469
            $data['title'] = PHP_EOL.$this->exercise.' : '.get_lang('Result');
6470
        }
6471
6472
        $questionsCount = count(explode(',', $trackExerciseInfo['data_tracking']));
6473
        $savedAnswersCount = $this->countUserAnswersSavedInExercise($trackExerciseInfo['exe_id']);
6474
6475
        $data['number_of_answers'] = $questionsCount;
6476
        $data['number_of_answers_saved'] = $savedAnswersCount;
6477
        $exeId = $trackExerciseInfo['exe_id'];
6478
6479
        if ('true' === api_get_setting('exercise.quiz_confirm_saved_answers')) {
6480
            $em = Database::getManager();
6481
6482
            if ($saveUserResult) {
6483
                $trackConfirmation = new TrackEExerciseConfirmation();
6484
                $trackConfirmation
6485
                    ->setUser(api_get_user_entity($trackExerciseInfo['exe_user_id']))
6486
                    ->setQuizId($trackExerciseInfo['exe_exo_id'])
6487
                    ->setAttemptId($trackExerciseInfo['exe_id'])
6488
                    ->setQuestionsCount($questionsCount)
6489
                    ->setSavedAnswersCount($savedAnswersCount)
6490
                    ->setCourseId($trackExerciseInfo['c_id'])
6491
                    ->setSessionId($trackExerciseInfo['session_id'])
6492
                    ->setCreatedAt(api_get_utc_datetime(null, false, true));
6493
6494
                $em->persist($trackConfirmation);
6495
                $em->flush();
6496
            } else {
6497
                $trackConfirmation = $em
6498
                    ->getRepository(TrackEExerciseConfirmation::class)
6499
                    ->findOneBy(
6500
                        [
6501
                            'attemptId' => $trackExerciseInfo['exe_id'],
6502
                            'quizId' => $trackExerciseInfo['exe_exo_id'],
6503
                            'courseId' => $trackExerciseInfo['c_id'],
6504
                            'sessionId' => $trackExerciseInfo['session_id'],
6505
                        ]
6506
                    );
6507
            }
6508
6509
            $data['track_confirmation'] = $trackConfirmation;
6510
        }
6511
6512
        $signature = '';
6513
        if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($this)) {
6514
            $signature = ExerciseSignaturePlugin::getSignature($trackExerciseInfo['exe_user_id'], $trackExerciseInfo);
6515
        }
6516
        $tpl = new Template(null, false, false, false, false, false, false);
6517
        $tpl->assign('data', $data);
6518
        $tpl->assign('allow_signature', $allowSignature);
6519
        $tpl->assign('signature', $signature);
6520
        $tpl->assign('allow_export_pdf', $allowExportPdf);
6521
        $tpl->assign(
6522
            'export_url',
6523
            api_get_path(WEB_CODE_PATH).'exercise/result.php?action=export&id='.$exeId.'&'.api_get_cidreq()
6524
        );
6525
        $layoutTemplate = $tpl->get_template('exercise/partials/result_exercise.tpl');
6526
6527
        return $tpl->fetch($layoutTemplate);
6528
    }
6529
6530
    /**
6531
     * Returns the exercise result.
6532
     *
6533
     * @param int        attempt id
6534
     *
6535
     * @return array
6536
     */
6537
    public function get_exercise_result($exe_id)
6538
    {
6539
        $result = [];
6540
        $track_exercise_info = ExerciseLib::get_exercise_track_exercise_info($exe_id);
6541
6542
        if (!empty($track_exercise_info)) {
6543
            $totalScore = 0;
6544
            $objExercise = new self();
6545
            $objExercise->read($track_exercise_info['exe_exo_id']);
6546
            if (!empty($track_exercise_info['data_tracking'])) {
6547
                $question_list = explode(',', $track_exercise_info['data_tracking']);
6548
            }
6549
            foreach ($question_list as $questionId) {
6550
                $question_result = $objExercise->manage_answer(
6551
                    $exe_id,
6552
                    $questionId,
6553
                    '',
6554
                    'exercise_show',
6555
                    [],
6556
                    false,
6557
                    true,
6558
                    false,
6559
                    $objExercise->selectPropagateNeg()
6560
                );
6561
                $totalScore += $question_result['score'];
6562
            }
6563
6564
            if (0 == $objExercise->selectPropagateNeg() && $totalScore < 0) {
6565
                $totalScore = 0;
6566
            }
6567
            $result = [
6568
                'score' => $totalScore,
6569
                'weight' => $track_exercise_info['max_score'],
6570
            ];
6571
        }
6572
6573
        return $result;
6574
    }
6575
6576
    /**
6577
     * Checks if the exercise is visible due a lot of conditions
6578
     * visibility, time limits, student attempts
6579
     * Return associative array
6580
     * value : true if exercise visible
6581
     * message : HTML formatted message
6582
     * rawMessage : text message.
6583
     *
6584
     * @param int  $lpId
6585
     * @param int  $lpItemId
6586
     * @param int  $lpItemViewId
6587
     * @param bool $filterByAdmin
6588
     *
6589
     * @return array
6590
     */
6591
    public function is_visible(
6592
        $lpId = 0,
6593
        $lpItemId = 0,
6594
        $lpItemViewId = 0,
6595
        $filterByAdmin = true
6596
    ) {
6597
        // 1. By default the exercise is visible
6598
        $isVisible = true;
6599
        $message = null;
6600
6601
        // 1.1 Admins and teachers can access to the exercise
6602
        if ($filterByAdmin) {
6603
            if (api_is_platform_admin() || api_is_course_admin() || api_is_course_tutor()) {
6604
                return ['value' => true, 'message' => ''];
6605
            }
6606
        }
6607
6608
        // Deleted exercise.
6609
        if (-1 == $this->active) {
6610
            return [
6611
                'value' => false,
6612
                'message' => Display::return_message(
6613
                    get_lang('Test not found or not visible'),
6614
                    'warning',
6615
                    false
6616
                ),
6617
                'rawMessage' => get_lang('Test not found or not visible'),
6618
            ];
6619
        }
6620
6621
        $repo = Container::getQuizRepository();
6622
        $exercise = $repo->find($this->iId);
6623
6624
        if (null === $exercise) {
6625
            return [];
6626
        }
6627
6628
        $course = api_get_course_entity($this->course_id);
6629
        $link = $exercise->getFirstResourceLinkFromCourseSession($course);
6630
6631
        if ($link && $link->isDraft()) {
6632
            $this->active = 0;
6633
        }
6634
6635
        // 2. If the exercise is not active.
6636
        if (empty($lpId)) {
6637
            // 2.1 LP is OFF
6638
            if (0 == $this->active) {
6639
                return [
6640
                    'value' => false,
6641
                    'message' => Display::return_message(
6642
                        get_lang('Test not found or not visible'),
6643
                        'warning',
6644
                        false
6645
                    ),
6646
                    'rawMessage' => get_lang('Test not found or not visible'),
6647
                ];
6648
            }
6649
        } else {
6650
            $lp = Container::getLpRepository()->find($lpId);
6651
            // 2.1 LP is loaded
6652
            if ($lp && 0 == $this->active &&
6653
                !learnpath::is_lp_visible_for_student($lp, api_get_user_id(), $course)
6654
            ) {
6655
                return [
6656
                    'value' => false,
6657
                    'message' => Display::return_message(
6658
                        get_lang('Test not found or not visible'),
6659
                        'warning',
6660
                        false
6661
                    ),
6662
                    'rawMessage' => get_lang('Test not found or not visible'),
6663
                ];
6664
            }
6665
        }
6666
6667
        // 3. We check if the time limits are on
6668
        $limitTimeExists = false;
6669
        if (!empty($this->start_time) || !empty($this->end_time)) {
6670
            $limitTimeExists = true;
6671
        }
6672
6673
        if ($limitTimeExists) {
6674
            $timeNow = time();
6675
            $existsStartDate = false;
6676
            $nowIsAfterStartDate = true;
6677
            $existsEndDate = false;
6678
            $nowIsBeforeEndDate = true;
6679
6680
            if (!empty($this->start_time)) {
6681
                $existsStartDate = true;
6682
            }
6683
6684
            if (!empty($this->end_time)) {
6685
                $existsEndDate = true;
6686
            }
6687
6688
            // check if we are before-or-after end-or-start date
6689
            if ($existsStartDate && $timeNow < api_strtotime($this->start_time, 'UTC')) {
6690
                $nowIsAfterStartDate = false;
6691
            }
6692
6693
            if ($existsEndDate & $timeNow >= api_strtotime($this->end_time, 'UTC')) {
6694
                $nowIsBeforeEndDate = false;
6695
            }
6696
6697
            // lets check all cases
6698
            if ($existsStartDate && !$existsEndDate) {
6699
                // exists start date and dont exists end date
6700
                if ($nowIsAfterStartDate) {
6701
                    // after start date, no end date
6702
                    $isVisible = true;
6703
                    $message = sprintf(
6704
                        get_lang('Exercise available since %s'),
6705
                        api_convert_and_format_date($this->start_time)
6706
                    );
6707
                } else {
6708
                    // before start date, no end date
6709
                    $isVisible = false;
6710
                    $message = sprintf(
6711
                        get_lang('Exercise available from %s'),
6712
                        api_convert_and_format_date($this->start_time)
6713
                    );
6714
                }
6715
            } elseif (!$existsStartDate && $existsEndDate) {
6716
                // doesnt exist start date, exists end date
6717
                if ($nowIsBeforeEndDate) {
6718
                    // before end date, no start date
6719
                    $isVisible = true;
6720
                    $message = sprintf(
6721
                        get_lang('Exercise available until %s'),
6722
                        api_convert_and_format_date($this->end_time)
6723
                    );
6724
                } else {
6725
                    // after end date, no start date
6726
                    $isVisible = false;
6727
                    $message = sprintf(
6728
                        get_lang('Exercise available until %s'),
6729
                        api_convert_and_format_date($this->end_time)
6730
                    );
6731
                }
6732
            } elseif ($existsStartDate && $existsEndDate) {
6733
                // exists start date and end date
6734
                if ($nowIsAfterStartDate) {
6735
                    if ($nowIsBeforeEndDate) {
6736
                        // after start date and before end date
6737
                        $isVisible = true;
6738
                        $message = sprintf(
6739
                            get_lang('Exercise was activated from %s to %s'),
6740
                            api_convert_and_format_date($this->start_time),
6741
                            api_convert_and_format_date($this->end_time)
6742
                        );
6743
                    } else {
6744
                        // after start date and after end date
6745
                        $isVisible = false;
6746
                        $message = sprintf(
6747
                            get_lang('Exercise was activated from %s to %s'),
6748
                            api_convert_and_format_date($this->start_time),
6749
                            api_convert_and_format_date($this->end_time)
6750
                        );
6751
                    }
6752
                } else {
6753
                    if ($nowIsBeforeEndDate) {
6754
                        // before start date and before end date
6755
                        $isVisible = false;
6756
                        $message = sprintf(
6757
                            get_lang('Exercise will be activated from %s to %s'),
6758
                            api_convert_and_format_date($this->start_time),
6759
                            api_convert_and_format_date($this->end_time)
6760
                        );
6761
                    }
6762
                    // case before start date and after end date is impossible
6763
                }
6764
            } elseif (!$existsStartDate && !$existsEndDate) {
6765
                // doesnt exist start date nor end date
6766
                $isVisible = true;
6767
                $message = '';
6768
            }
6769
        }
6770
6771
        // 4. We check if the student have attempts
6772
        if ($isVisible) {
6773
            $exerciseAttempts = $this->selectAttempts();
6774
6775
            if ($exerciseAttempts > 0) {
6776
                $attemptCount = Event::get_attempt_count(
6777
                    api_get_user_id(),
6778
                    $this->getId(),
6779
                    (int) $lpId,
6780
                    (int) $lpItemId,
6781
                    (int) $lpItemViewId
6782
                );
6783
6784
                if ($attemptCount >= $exerciseAttempts) {
6785
                    $message = sprintf(
6786
                        get_lang('You cannot take test <b>%s</b> because you have already reached the maximum of %s attempts.'),
6787
                        $this->name,
6788
                        $exerciseAttempts
6789
                    );
6790
                    $isVisible = false;
6791
                } else {
6792
                    // Check blocking exercise.
6793
                    $extraFieldValue = new ExtraFieldValue('exercise');
6794
                    $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
6795
                        $this->iId,
6796
                        'blocking_percentage'
6797
                    );
6798
                    if ($blockExercise && isset($blockExercise['value']) && !empty($blockExercise['value'])) {
6799
                        $blockPercentage = (int) $blockExercise['value'];
6800
                        $userAttempts = Event::getExerciseResultsByUser(
6801
                            api_get_user_id(),
6802
                            $this->iId,
6803
                            $this->course_id,
6804
                            $this->sessionId,
6805
                            $lpId,
6806
                            $lpItemId
6807
                        );
6808
6809
                        if (!empty($userAttempts)) {
6810
                            $currentAttempt = current($userAttempts);
6811
                            if ($currentAttempt['total_percentage'] <= $blockPercentage) {
6812
                                $message = sprintf(
6813
                                    get_lang('All attempts blocked because you did not reach the minimum score of %s % at one of your attempts.'),
6814
                                    $blockPercentage
6815
                                );
6816
                                $isVisible = false;
6817
                            }
6818
                        }
6819
                    }
6820
                }
6821
            }
6822
        }
6823
6824
        $rawMessage = '';
6825
        if (!empty($message)) {
6826
            $rawMessage = $message;
6827
            $message = Display::return_message($message, 'warning', false);
6828
        }
6829
6830
        return [
6831
            'value' => $isVisible,
6832
            'message' => $message,
6833
            'rawMessage' => $rawMessage,
6834
        ];
6835
    }
6836
6837
    /**
6838
     * @return bool
6839
     */
6840
    public function added_in_lp()
6841
    {
6842
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
6843
        $sql = "SELECT max_score FROM $TBL_LP_ITEM
6844
                WHERE
6845
                    item_type = '".TOOL_QUIZ."' AND
6846
                    path = '{$this->getId()}'";
6847
        $result = Database::query($sql);
6848
        if (Database::num_rows($result) > 0) {
6849
            return true;
6850
        }
6851
6852
        return false;
6853
    }
6854
6855
    /**
6856
     * Returns an array with this form.
6857
     *
6858
     * @return array
6859
     *
6860
     * @example
6861
     * <code>
6862
     * array (size=3)
6863
     * 999 =>
6864
     * array (size=3)
6865
     * 0 => int 3422
6866
     * 1 => int 3423
6867
     * 2 => int 3424
6868
     * 100 =>
6869
     * array (size=2)
6870
     * 0 => int 3469
6871
     * 1 => int 3470
6872
     * 101 =>
6873
     * array (size=1)
6874
     * 0 => int 3482
6875
     * </code>
6876
     * The array inside the key 999 means the question list that belongs to the media id = 999,
6877
     * this case is special because 999 means "no media".
6878
     */
6879
    public function getMediaList()
6880
    {
6881
        return $this->mediaList;
6882
    }
6883
6884
    /**
6885
     * Is media question activated?
6886
     *
6887
     * @return bool
6888
     */
6889
    public function mediaIsActivated()
6890
    {
6891
        $mediaQuestions = $this->getMediaList();
6892
        $active = false;
6893
        if (isset($mediaQuestions) && !empty($mediaQuestions)) {
6894
            $media_count = count($mediaQuestions);
6895
            if ($media_count > 1) {
6896
                return true;
6897
            } elseif (1 == $media_count) {
6898
                if (isset($mediaQuestions[999])) {
6899
                    return false;
6900
                } else {
6901
                    return true;
6902
                }
6903
            }
6904
        }
6905
6906
        return $active;
6907
    }
6908
6909
    /**
6910
     * Gets question list from the exercise.
6911
     *
6912
     * @return array
6913
     */
6914
    public function getQuestionList()
6915
    {
6916
        return $this->questionList;
6917
    }
6918
6919
    /**
6920
     * Question list with medias compressed like this.
6921
     *
6922
     * @return array
6923
     *
6924
     * @example
6925
     *      <code>
6926
     *      array(
6927
     *      question_id_1,
6928
     *      question_id_2,
6929
     *      media_id, <- this media id contains question ids
6930
     *      question_id_3,
6931
     *      )
6932
     *      </code>
6933
     */
6934
    public function getQuestionListWithMediasCompressed()
6935
    {
6936
        return $this->questionList;
6937
    }
6938
6939
    /**
6940
     * Question list with medias uncompressed like this.
6941
     *
6942
     * @return array
6943
     *
6944
     * @example
6945
     *      <code>
6946
     *      array(
6947
     *      question_id,
6948
     *      question_id,
6949
     *      question_id, <- belongs to a media id
6950
     *      question_id, <- belongs to a media id
6951
     *      question_id,
6952
     *      )
6953
     *      </code>
6954
     */
6955
    public function getQuestionListWithMediasUncompressed()
6956
    {
6957
        return $this->questionListUncompressed;
6958
    }
6959
6960
    /**
6961
     * Sets the question list when the exercise->read() is executed.
6962
     *
6963
     * @param bool $adminView Whether to view the set the list of *all* questions or just the normal student view
6964
     */
6965
    public function setQuestionList($adminView = false)
6966
    {
6967
        // Getting question list.
6968
        $questionList = $this->selectQuestionList(true, $adminView);
6969
        $this->setMediaList($questionList);
6970
        $this->questionList = $this->transformQuestionListWithMedias($questionList, false);
6971
        $this->questionListUncompressed = $this->transformQuestionListWithMedias(
6972
            $questionList,
6973
            true
6974
        );
6975
    }
6976
6977
    /**
6978
     * @params array question list
6979
     * @params bool expand or not question list (true show all questions,
6980
     * false show media question id instead of the question ids)
6981
     */
6982
    public function transformQuestionListWithMedias(
6983
        $question_list,
6984
        $expand_media_questions = false
6985
    ) {
6986
        $new_question_list = [];
6987
        if (!empty($question_list)) {
6988
            $media_questions = $this->getMediaList();
6989
            $media_active = $this->mediaIsActivated($media_questions);
6990
6991
            if ($media_active) {
6992
                $counter = 1;
6993
                foreach ($question_list as $question_id) {
6994
                    $add_question = true;
6995
                    foreach ($media_questions as $media_id => $question_list_in_media) {
6996
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
6997
                            $add_question = false;
6998
                            if (!in_array($media_id, $new_question_list)) {
6999
                                $new_question_list[$counter] = $media_id;
7000
                                $counter++;
7001
                            }
7002
7003
                            break;
7004
                        }
7005
                    }
7006
                    if ($add_question) {
7007
                        $new_question_list[$counter] = $question_id;
7008
                        $counter++;
7009
                    }
7010
                }
7011
                if ($expand_media_questions) {
7012
                    $media_key_list = array_keys($media_questions);
7013
                    foreach ($new_question_list as &$question_id) {
7014
                        if (in_array($question_id, $media_key_list)) {
7015
                            $question_id = $media_questions[$question_id];
7016
                        }
7017
                    }
7018
                    $new_question_list = array_flatten($new_question_list);
7019
                }
7020
            } else {
7021
                $new_question_list = $question_list;
7022
            }
7023
        }
7024
7025
        return $new_question_list;
7026
    }
7027
7028
    /**
7029
     * Get question list depend on the random settings.
7030
     *
7031
     * @return array
7032
     */
7033
    public function get_validated_question_list()
7034
    {
7035
        $isRandomByCategory = $this->isRandomByCat();
7036
        if (0 == $isRandomByCategory) {
7037
            if ($this->isRandom()) {
7038
                return $this->getRandomList();
7039
            }
7040
7041
            return $this->selectQuestionList();
7042
        }
7043
7044
        if ($this->isRandom()) {
7045
            // USE question categories
7046
            // get questions by category for this exercise
7047
            // we have to choice $objExercise->random question in each array values of $tabCategoryQuestions
7048
            // key of $tabCategoryQuestions are the categopy id (0 for not in a category)
7049
            // value is the array of question id of this category
7050
            $questionList = [];
7051
            $categoryQuestions = TestCategory::getQuestionsByCat($this->id);
7052
            $isRandomByCategory = $this->getRandomByCategory();
7053
            // We sort categories based on the term between [] in the head
7054
            // of the category's description
7055
            /* examples of categories :
7056
             * [biologie] Maitriser les mecanismes de base de la genetique
7057
             * [biologie] Relier les moyens de depenses et les agents infectieux
7058
             * [biologie] Savoir ou est produite l'enrgie dans les cellules et sous quelle forme
7059
             * [chimie] Classer les molles suivant leur pouvoir oxydant ou reacteur
7060
             * [chimie] Connaître la denition de la theoie acide/base selon Brönsted
7061
             * [chimie] Connaître les charges des particules
7062
             * We want that in the order of the groups defined by the term
7063
             * between brackets at the beginning of the category title
7064
            */
7065
            // If test option is Grouped By Categories
7066
            if (2 == $isRandomByCategory) {
7067
                $categoryQuestions = TestCategory::sortTabByBracketLabel($categoryQuestions);
7068
            }
7069
            foreach ($categoryQuestions as $question) {
7070
                $number_of_random_question = $this->random;
7071
                if (-1 == $this->random) {
7072
                    $number_of_random_question = count($this->questionList);
7073
                }
7074
                $questionList = array_merge(
7075
                    $questionList,
7076
                    TestCategory::getNElementsFromArray(
7077
                        $question,
7078
                        $number_of_random_question
7079
                    )
7080
                );
7081
            }
7082
            // shuffle the question list if test is not grouped by categories
7083
            if (1 == $isRandomByCategory) {
7084
                shuffle($questionList); // or not
7085
            }
7086
7087
            return $questionList;
7088
        }
7089
7090
        // Problem, random by category has been selected and
7091
        // we have no $this->isRandom number of question selected
7092
        // Should not happened
7093
7094
        return [];
7095
    }
7096
7097
    public function get_question_list($expand_media_questions = false)
7098
    {
7099
        $question_list = $this->get_validated_question_list();
7100
        $question_list = $this->transform_question_list_with_medias($question_list, $expand_media_questions);
7101
7102
        return $question_list;
7103
    }
7104
7105
    public function transform_question_list_with_medias($question_list, $expand_media_questions = false)
7106
    {
7107
        $new_question_list = [];
7108
        if (!empty($question_list)) {
7109
            $media_questions = $this->getMediaList();
7110
            $media_active = $this->mediaIsActivated($media_questions);
7111
7112
            if ($media_active) {
7113
                $counter = 1;
7114
                foreach ($question_list as $question_id) {
7115
                    $add_question = true;
7116
                    foreach ($media_questions as $media_id => $question_list_in_media) {
7117
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
7118
                            $add_question = false;
7119
                            if (!in_array($media_id, $new_question_list)) {
7120
                                $new_question_list[$counter] = $media_id;
7121
                                $counter++;
7122
                            }
7123
7124
                            break;
7125
                        }
7126
                    }
7127
                    if ($add_question) {
7128
                        $new_question_list[$counter] = $question_id;
7129
                        $counter++;
7130
                    }
7131
                }
7132
                if ($expand_media_questions) {
7133
                    $media_key_list = array_keys($media_questions);
7134
                    foreach ($new_question_list as &$question_id) {
7135
                        if (in_array($question_id, $media_key_list)) {
7136
                            $question_id = $media_questions[$question_id];
7137
                        }
7138
                    }
7139
                    $new_question_list = array_flatten($new_question_list);
7140
                }
7141
            } else {
7142
                $new_question_list = $question_list;
7143
            }
7144
        }
7145
7146
        return $new_question_list;
7147
    }
7148
7149
    /**
7150
     * @param int $exe_id
7151
     *
7152
     * @return array
7153
     */
7154
    public function get_stat_track_exercise_info_by_exe_id($exe_id)
7155
    {
7156
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7157
        $exe_id = (int) $exe_id;
7158
        $sql_track = "SELECT * FROM $table WHERE exe_id = $exe_id ";
7159
        $result = Database::query($sql_track);
7160
        $new_array = [];
7161
        if (Database::num_rows($result) > 0) {
7162
            $new_array = Database::fetch_assoc($result);
7163
            $start_date = api_get_utc_datetime($new_array['start_date'], true);
7164
            $end_date = api_get_utc_datetime($new_array['exe_date'], true);
7165
            $new_array['duration_formatted'] = '';
7166
            if (!empty($new_array['exe_duration']) && !empty($start_date) && !empty($end_date)) {
7167
                $time = api_format_time($new_array['exe_duration'], 'js');
7168
                $new_array['duration_formatted'] = $time;
7169
            }
7170
        }
7171
7172
        return $new_array;
7173
    }
7174
7175
    /**
7176
     * @param int $exeId
7177
     *
7178
     * @return bool
7179
     */
7180
    public function removeAllQuestionToRemind($exeId)
7181
    {
7182
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7183
        $exeId = (int) $exeId;
7184
        if (empty($exeId)) {
7185
            return false;
7186
        }
7187
        $sql = "UPDATE $table
7188
                SET questions_to_check = ''
7189
                WHERE exe_id = $exeId ";
7190
        Database::query($sql);
7191
7192
        return true;
7193
    }
7194
7195
    /**
7196
     * @param int   $exeId
7197
     * @param array $questionList
7198
     *
7199
     * @return bool
7200
     */
7201
    public function addAllQuestionToRemind($exeId, $questionList = [])
7202
    {
7203
        $exeId = (int) $exeId;
7204
        if (empty($questionList)) {
7205
            return false;
7206
        }
7207
7208
        $questionListToString = implode(',', $questionList);
7209
        $questionListToString = Database::escape_string($questionListToString);
7210
7211
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7212
        $sql = "UPDATE $table
7213
                SET questions_to_check = '$questionListToString'
7214
                WHERE exe_id = $exeId";
7215
        Database::query($sql);
7216
7217
        return true;
7218
    }
7219
7220
    /**
7221
     * @param int    $exeId
7222
     * @param int    $questionId
7223
     * @param string $action
7224
     */
7225
    public function editQuestionToRemind($exeId, $questionId, $action = 'add')
7226
    {
7227
        $exercise_info = self::get_stat_track_exercise_info_by_exe_id($exeId);
7228
        $questionId = (int) $questionId;
7229
        $exeId = (int) $exeId;
7230
7231
        if ($exercise_info) {
7232
            $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7233
            if (empty($exercise_info['questions_to_check'])) {
7234
                if ('add' == $action) {
7235
                    $sql = "UPDATE $track_exercises
7236
                            SET questions_to_check = '$questionId'
7237
                            WHERE exe_id = $exeId ";
7238
                    Database::query($sql);
7239
                }
7240
            } else {
7241
                $remind_list = explode(',', $exercise_info['questions_to_check']);
7242
                $remind_list_string = '';
7243
                if ('add' === $action) {
7244
                    if (!in_array($questionId, $remind_list)) {
7245
                        $newRemindList = [];
7246
                        $remind_list[] = $questionId;
7247
                        $questionListInSession = Session::read('questionList');
7248
                        if (!empty($questionListInSession)) {
7249
                            foreach ($questionListInSession as $originalQuestionId) {
7250
                                if (in_array($originalQuestionId, $remind_list)) {
7251
                                    $newRemindList[] = $originalQuestionId;
7252
                                }
7253
                            }
7254
                        }
7255
                        $remind_list_string = implode(',', $newRemindList);
7256
                    }
7257
                } elseif ('delete' == $action) {
7258
                    if (!empty($remind_list)) {
7259
                        if (in_array($questionId, $remind_list)) {
7260
                            $remind_list = array_flip($remind_list);
7261
                            unset($remind_list[$questionId]);
7262
                            $remind_list = array_flip($remind_list);
7263
7264
                            if (!empty($remind_list)) {
7265
                                sort($remind_list);
7266
                                array_filter($remind_list);
7267
                                $remind_list_string = implode(',', $remind_list);
7268
                            }
7269
                        }
7270
                    }
7271
                }
7272
                $value = Database::escape_string($remind_list_string);
7273
                $sql = "UPDATE $track_exercises
7274
                        SET questions_to_check = '$value'
7275
                        WHERE exe_id = $exeId ";
7276
                Database::query($sql);
7277
            }
7278
        }
7279
    }
7280
7281
    /**
7282
     * @param string $answer
7283
     */
7284
    public function fill_in_blank_answer_to_array($answer)
7285
    {
7286
        $list = null;
7287
        api_preg_match_all('/\[[^]]+\]/', $answer, $list);
7288
7289
        if (empty($list)) {
7290
            return '';
7291
        }
7292
7293
        return $list[0];
7294
    }
7295
7296
    /**
7297
     * @param string $answer
7298
     *
7299
     * @return string
7300
     */
7301
    public function fill_in_blank_answer_to_string($answer)
7302
    {
7303
        $teacher_answer_list = $this->fill_in_blank_answer_to_array($answer);
7304
        $result = '';
7305
        if (!empty($teacher_answer_list)) {
7306
            foreach ($teacher_answer_list as $teacher_item) {
7307
                //Cleaning student answer list
7308
                $value = strip_tags($teacher_item);
7309
                $value = api_substr($value, 1, api_strlen($value) - 2);
7310
                $value = explode('/', $value);
7311
                if (!empty($value[0])) {
7312
                    $value = trim($value[0]);
7313
                    $value = str_replace('&nbsp;', '', $value);
7314
                    $result .= $value;
7315
                }
7316
            }
7317
        }
7318
7319
        return $result;
7320
    }
7321
7322
    /**
7323
     * @return string
7324
     */
7325
    public function returnTimeLeftDiv()
7326
    {
7327
        $html = '<div id="clock_warning" style="display:none">';
7328
        $html .= Display::return_message(
7329
            get_lang('Time limit reached'),
7330
            'warning'
7331
        );
7332
        $html .= ' ';
7333
        $html .= sprintf(
7334
            get_lang('Just a moment, please. You will be redirected in %s seconds...'),
7335
            '<span id="counter_to_redirect" class="red_alert"></span>'
7336
        );
7337
        $html .= '</div>';
7338
        $icon = Display::getMdiIcon('clock-outline', 'ch-tool-icon');
7339
        $html .= '<div class="count_down">
7340
                    '.get_lang('Remaining time to finish exercise').'
7341
                    '.$icon.'<span id="exercise_clock_warning"></span>
7342
                </div>';
7343
7344
        return $html;
7345
    }
7346
7347
    /**
7348
     * Get categories added in the exercise--category matrix.
7349
     *
7350
     * @return array
7351
     */
7352
    public function getCategoriesInExercise()
7353
    {
7354
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7355
        if (!empty($this->getId())) {
7356
            $sql = "SELECT * FROM $table
7357
                    WHERE exercise_id = {$this->getId()} ";
7358
            $result = Database::query($sql);
7359
            $list = [];
7360
            if (Database::num_rows($result)) {
7361
                while ($row = Database::fetch_assoc($result)) {
7362
                    $list[$row['category_id']] = $row;
7363
                }
7364
7365
                return $list;
7366
            }
7367
        }
7368
7369
        return [];
7370
    }
7371
7372
    /**
7373
     * Get total number of question that will be parsed when using the category/exercise.
7374
     *
7375
     * @return int
7376
     */
7377
    public function getNumberQuestionExerciseCategory()
7378
    {
7379
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7380
        if (!empty($this->getId())) {
7381
            $sql = "SELECT SUM(count_questions) count_questions
7382
                    FROM $table
7383
                    WHERE exercise_id = {$this->getId()}";
7384
            $result = Database::query($sql);
7385
            if (Database::num_rows($result)) {
7386
                $row = Database::fetch_array($result);
7387
7388
                return (int) $row['count_questions'];
7389
            }
7390
        }
7391
7392
        return 0;
7393
    }
7394
7395
    /**
7396
     * Save categories in the TABLE_QUIZ_REL_CATEGORY table
7397
     *
7398
     * @param array $categories
7399
     */
7400
    public function saveCategoriesInExercise($categories)
7401
    {
7402
        if (!empty($categories) && !empty($this->getId())) {
7403
            $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7404
            $sql = "DELETE FROM $table
7405
                    WHERE exercise_id = {$this->getId()}";
7406
            Database::query($sql);
7407
            foreach ($categories as $categoryId => $countQuestions) {
7408
                if ($categoryId !== 0) {
7409
                    $params = [
7410
                        'exercise_id' => $this->getId(),
7411
                        'category_id' => $categoryId,
7412
                        'count_questions' => $countQuestions,
7413
                    ];
7414
                    Database::insert($table, $params);
7415
                }
7416
            }
7417
        }
7418
    }
7419
7420
    /**
7421
     * @param array  $questionList
7422
     * @param int    $currentQuestion
7423
     * @param array  $conditions
7424
     * @param string $link
7425
     *
7426
     * @return string
7427
     */
7428
    public function progressExercisePaginationBar(
7429
        $questionList,
7430
        $currentQuestion,
7431
        $conditions,
7432
        $link
7433
    ) {
7434
        $mediaQuestions = $this->getMediaList();
7435
7436
        $html = '<div class="exercise_pagination pagination pagination-mini"><ul>';
7437
        $counter = 0;
7438
        $nextValue = 0;
7439
        $wasMedia = false;
7440
        $before = 0;
7441
        $counterNoMedias = 0;
7442
        foreach ($questionList as $questionId) {
7443
            $isCurrent = $currentQuestion == $counterNoMedias + 1 ? true : false;
7444
7445
            if (!empty($nextValue)) {
7446
                if ($wasMedia) {
7447
                    $nextValue = $nextValue - $before + 1;
7448
                }
7449
            }
7450
7451
            if (isset($mediaQuestions) && isset($mediaQuestions[$questionId])) {
7452
                $fixedValue = $counterNoMedias;
7453
7454
                $html .= Display::progressPaginationBar(
7455
                    $nextValue,
7456
                    $mediaQuestions[$questionId],
7457
                    $currentQuestion,
7458
                    $fixedValue,
7459
                    $conditions,
7460
                    $link,
7461
                    true,
7462
                    true
7463
                );
7464
7465
                $counter += count($mediaQuestions[$questionId]) - 1;
7466
                $before = count($questionList);
7467
                $wasMedia = true;
7468
                $nextValue += count($questionList);
7469
            } else {
7470
                $html .= Display::parsePaginationItem(
7471
                    $questionId,
7472
                    $isCurrent,
7473
                    $conditions,
7474
                    $link,
7475
                    $counter
7476
                );
7477
                $counter++;
7478
                $nextValue++;
7479
                $wasMedia = false;
7480
            }
7481
            $counterNoMedias++;
7482
        }
7483
        $html .= '</ul></div>';
7484
7485
        return $html;
7486
    }
7487
7488
    /**
7489
     *  Shows a list of numbers that represents the question to answer in a exercise.
7490
     *
7491
     * @param array  $categories
7492
     * @param int    $current
7493
     * @param array  $conditions
7494
     * @param string $link
7495
     *
7496
     * @return string
7497
     */
7498
    public function progressExercisePaginationBarWithCategories(
7499
        $categories,
7500
        $current,
7501
        $conditions = [],
7502
        $link = null
7503
    ) {
7504
        $html = null;
7505
        $counterNoMedias = 0;
7506
        $nextValue = 0;
7507
        $wasMedia = false;
7508
        $before = 0;
7509
7510
        if (!empty($categories)) {
7511
            $selectionType = $this->getQuestionSelectionType();
7512
            $useRootAsCategoryTitle = false;
7513
7514
            // Grouping questions per parent category see BT#6540
7515
            if (in_array(
7516
                $selectionType,
7517
                [
7518
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED,
7519
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM,
7520
                ]
7521
            )) {
7522
                $useRootAsCategoryTitle = true;
7523
            }
7524
7525
            // If the exercise is set to only show the titles of the categories
7526
            // at the root of the tree, then pre-order the categories tree by
7527
            // removing children and summing their questions into the parent
7528
            // categories
7529
            if ($useRootAsCategoryTitle) {
7530
                // The new categories list starts empty
7531
                $newCategoryList = [];
7532
                foreach ($categories as $category) {
7533
                    $rootElement = $category['root'];
7534
7535
                    if (isset($category['parent_info'])) {
7536
                        $rootElement = $category['parent_info']['id'];
7537
                    }
7538
7539
                    //$rootElement = $category['id'];
7540
                    // If the current category's ancestor was never seen
7541
                    // before, then declare it and assign the current
7542
                    // category to it.
7543
                    if (!isset($newCategoryList[$rootElement])) {
7544
                        $newCategoryList[$rootElement] = $category;
7545
                    } else {
7546
                        // If it was already seen, then merge the previous with
7547
                        // the current category
7548
                        $oldQuestionList = $newCategoryList[$rootElement]['question_list'];
7549
                        $category['question_list'] = array_merge($oldQuestionList, $category['question_list']);
7550
                        $newCategoryList[$rootElement] = $category;
7551
                    }
7552
                }
7553
                // Now use the newly built categories list, with only parents
7554
                $categories = $newCategoryList;
7555
            }
7556
7557
            foreach ($categories as $category) {
7558
                $questionList = $category['question_list'];
7559
                // Check if in this category there questions added in a media
7560
                $mediaQuestionId = $category['media_question'];
7561
                $isMedia = false;
7562
                $fixedValue = null;
7563
7564
                // Media exists!
7565
                if (999 != $mediaQuestionId) {
7566
                    $isMedia = true;
7567
                    $fixedValue = $counterNoMedias;
7568
                }
7569
7570
                //$categoryName = $category['path']; << show the path
7571
                $categoryName = $category['name'];
7572
7573
                if ($useRootAsCategoryTitle) {
7574
                    if (isset($category['parent_info'])) {
7575
                        $categoryName = $category['parent_info']['title'];
7576
                    }
7577
                }
7578
                $html .= '<div class="row">';
7579
                $html .= '<div class="span2">'.$categoryName.'</div>';
7580
                $html .= '<div class="span8">';
7581
7582
                if (!empty($nextValue)) {
7583
                    if ($wasMedia) {
7584
                        $nextValue = $nextValue - $before + 1;
7585
                    }
7586
                }
7587
                $html .= Display::progressPaginationBar(
7588
                    $nextValue,
7589
                    $questionList,
7590
                    $current,
7591
                    $fixedValue,
7592
                    $conditions,
7593
                    $link,
7594
                    $isMedia,
7595
                    true
7596
                );
7597
                $html .= '</div>';
7598
                $html .= '</div>';
7599
7600
                if (999 == $mediaQuestionId) {
7601
                    $counterNoMedias += count($questionList);
7602
                } else {
7603
                    $counterNoMedias++;
7604
                }
7605
7606
                $nextValue += count($questionList);
7607
                $before = count($questionList);
7608
7609
                if (999 != $mediaQuestionId) {
7610
                    $wasMedia = true;
7611
                } else {
7612
                    $wasMedia = false;
7613
                }
7614
            }
7615
        }
7616
7617
        return $html;
7618
    }
7619
7620
    /**
7621
     * Renders a question list.
7622
     *
7623
     * @param array $questionList    (with media questions compressed)
7624
     * @param int   $currentQuestion
7625
     * @param array $exerciseResult
7626
     * @param array $attemptList
7627
     * @param array $remindList
7628
     */
7629
    public function renderQuestionList(
7630
        $questionList,
7631
        $currentQuestion,
7632
        $exerciseResult,
7633
        $attemptList,
7634
        $remindList
7635
    ) {
7636
        $mediaQuestions = $this->getMediaList();
7637
        $i = 0;
7638
7639
        // Normal question list render (medias compressed)
7640
        foreach ($questionList as $questionId) {
7641
            $i++;
7642
            // For sequential exercises
7643
7644
            if (ONE_PER_PAGE == $this->type) {
7645
                // If it is not the right question, goes to the next loop iteration
7646
                if ($currentQuestion != $i) {
7647
                    continue;
7648
                } else {
7649
                    if (!in_array(
7650
                        $this->getFeedbackType(),
7651
                        [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
7652
                    )) {
7653
                        // if the user has already answered this question
7654
                        if (isset($exerciseResult[$questionId])) {
7655
                            echo Display::return_message(
7656
                                get_lang('You already answered the question'),
7657
                                'normal'
7658
                            );
7659
7660
                            break;
7661
                        }
7662
                    }
7663
                }
7664
            }
7665
7666
            // The $questionList contains the media id we check
7667
            // if this questionId is a media question type
7668
            if (isset($mediaQuestions[$questionId]) &&
7669
                999 != $mediaQuestions[$questionId]
7670
            ) {
7671
                // The question belongs to a media
7672
                $mediaQuestionList = $mediaQuestions[$questionId];
7673
                $objQuestionTmp = Question::read($questionId);
7674
7675
                $counter = 1;
7676
                if (MEDIA_QUESTION == $objQuestionTmp->type) {
7677
                    echo $objQuestionTmp->show_media_content();
7678
7679
                    $countQuestionsInsideMedia = count($mediaQuestionList);
7680
7681
                    // Show questions that belongs to a media
7682
                    if (!empty($mediaQuestionList)) {
7683
                        // In order to parse media questions we use letters a, b, c, etc.
7684
                        $letterCounter = 97;
7685
                        foreach ($mediaQuestionList as $questionIdInsideMedia) {
7686
                            $isLastQuestionInMedia = false;
7687
                            if ($counter == $countQuestionsInsideMedia) {
7688
                                $isLastQuestionInMedia = true;
7689
                            }
7690
                            $this->renderQuestion(
7691
                                $questionIdInsideMedia,
7692
                                $attemptList,
7693
                                $remindList,
7694
                                chr($letterCounter),
7695
                                $currentQuestion,
7696
                                $mediaQuestionList,
7697
                                $isLastQuestionInMedia,
7698
                                $questionList
7699
                            );
7700
                            $letterCounter++;
7701
                            $counter++;
7702
                        }
7703
                    }
7704
                } else {
7705
                    $this->renderQuestion(
7706
                        $questionId,
7707
                        $attemptList,
7708
                        $remindList,
7709
                        $i,
7710
                        $currentQuestion,
7711
                        null,
7712
                        null,
7713
                        $questionList
7714
                    );
7715
                    $i++;
7716
                }
7717
            } else {
7718
                // Normal question render.
7719
                $this->renderQuestion(
7720
                    $questionId,
7721
                    $attemptList,
7722
                    $remindList,
7723
                    $i,
7724
                    $currentQuestion,
7725
                    null,
7726
                    null,
7727
                    $questionList
7728
                );
7729
            }
7730
7731
            // For sequential exercises.
7732
            if (ONE_PER_PAGE == $this->type) {
7733
                // quits the loop
7734
                break;
7735
            }
7736
        }
7737
        // end foreach()
7738
7739
        if (ALL_ON_ONE_PAGE == $this->type) {
7740
            $exercise_actions = $this->show_button($questionId, $currentQuestion);
7741
            echo Display::div($exercise_actions, ['class' => 'exercise_actions']);
7742
        }
7743
    }
7744
7745
    /**
7746
     * Not implemented in 1.11.x.
7747
     *
7748
     * @param int   $questionId
7749
     * @param array $attemptList
7750
     * @param array $remindList
7751
     * @param int   $i
7752
     * @param int   $current_question
7753
     * @param array $questions_in_media
7754
     * @param bool  $last_question_in_media
7755
     * @param array $realQuestionList
7756
     * @param bool  $generateJS
7757
     */
7758
    public function renderQuestion(
7759
        $questionId,
7760
        $attemptList,
7761
        $remindList,
7762
        $i,
7763
        $current_question,
7764
        $questions_in_media = [],
7765
        $last_question_in_media = false,
7766
        $realQuestionList = [],
7767
        $generateJS = true
7768
    ) {
7769
        // With this option on the question is loaded via AJAX
7770
        //$generateJS = true;
7771
        //$this->loadQuestionAJAX = true;
7772
7773
        if ($generateJS && $this->loadQuestionAJAX) {
7774
            $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?a=get_question&id='.$questionId.'&'.api_get_cidreq();
7775
            $params = [
7776
                'questionId' => $questionId,
7777
                'attemptList' => $attemptList,
7778
                'remindList' => $remindList,
7779
                'i' => $i,
7780
                'current_question' => $current_question,
7781
                'questions_in_media' => $questions_in_media,
7782
                'last_question_in_media' => $last_question_in_media,
7783
            ];
7784
            $params = json_encode($params);
7785
7786
            $script = '<script>
7787
            $(function(){
7788
                var params = '.$params.';
7789
                $.ajax({
7790
                    type: "GET",
7791
                    data: params,
7792
                    url: "'.$url.'",
7793
                    success: function(return_value) {
7794
                        $("#ajaxquestiondiv'.$questionId.'").html(return_value);
7795
                    }
7796
                });
7797
            });
7798
            </script>
7799
            <div id="ajaxquestiondiv'.$questionId.'"></div>';
7800
            echo $script;
7801
        } else {
7802
            $origin = api_get_origin();
7803
            $question_obj = Question::read($questionId);
7804
            $user_choice = isset($attemptList[$questionId]) ? $attemptList[$questionId] : null;
7805
            $remind_highlight = null;
7806
7807
            // Hides questions when reviewing a ALL_ON_ONE_PAGE exercise
7808
            // see #4542 no_remind_highlight class hide with jquery
7809
            if (ALL_ON_ONE_PAGE == $this->type && isset($_GET['reminder']) && 2 == $_GET['reminder']) {
7810
                $remind_highlight = 'no_remind_highlight';
7811
                // @todo not implemented in 1.11.x
7812
                /*if (in_array($question_obj->type, Question::question_type_no_review())) {
7813
                    return null;
7814
                }*/
7815
            }
7816
7817
            $attributes = ['id' => 'remind_list['.$questionId.']'];
7818
7819
            // Showing the question
7820
            $exercise_actions = null;
7821
            echo '<a id="questionanchor'.$questionId.'"></a><br />';
7822
            echo '<div id="question_div_'.$questionId.'" class="main_question '.$remind_highlight.'" >';
7823
7824
            // Shows the question + possible answers
7825
            $showTitle = 1 == $this->getHideQuestionTitle() ? false : true;
7826
            // @todo not implemented in 1.11.x
7827
            /*echo $this->showQuestion(
7828
                $question_obj,
7829
                false,
7830
                $origin,
7831
                $i,
7832
                $showTitle,
7833
                false,
7834
                $user_choice,
7835
                false,
7836
                null,
7837
                false,
7838
                $this->getModelType(),
7839
                $this->categoryMinusOne
7840
            );*/
7841
7842
            // Button save and continue
7843
            switch ($this->type) {
7844
                case ONE_PER_PAGE:
7845
                    $exercise_actions .= $this->show_button(
7846
                        $questionId,
7847
                        $current_question,
7848
                        null,
7849
                        $remindList
7850
                    );
7851
7852
                    break;
7853
                case ALL_ON_ONE_PAGE:
7854
                    if (api_is_allowed_to_session_edit()) {
7855
                        $button = [
7856
                            Display::button(
7857
                                'save_now',
7858
                                get_lang('Save and continue'),
7859
                                ['type' => 'button', 'class' => 'btn btn--primary', 'data-question' => $questionId]
7860
                            ),
7861
                            '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>',
7862
                        ];
7863
                        $exercise_actions .= Display::div(
7864
                            implode(PHP_EOL, $button),
7865
                            ['class' => 'exercise_save_now_button mb-4']
7866
                        );
7867
                    }
7868
7869
                    break;
7870
            }
7871
7872
            if (!empty($questions_in_media)) {
7873
                $count_of_questions_inside_media = count($questions_in_media);
7874
                if ($count_of_questions_inside_media > 1 && api_is_allowed_to_session_edit()) {
7875
                    $button = [
7876
                        Display::button(
7877
                            'save_now',
7878
                            get_lang('Save and continue'),
7879
                            ['type' => 'button', 'class' => 'btn btn--primary', 'data-question' => $questionId]
7880
                        ),
7881
                        '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>&nbsp;',
7882
                    ];
7883
                    $exercise_actions = Display::div(
7884
                        implode(PHP_EOL, $button),
7885
                        ['class' => 'exercise_save_now_button mb-4']
7886
                    );
7887
                }
7888
7889
                if ($last_question_in_media && ONE_PER_PAGE == $this->type) {
7890
                    $exercise_actions = $this->show_button($questionId, $current_question, $questions_in_media);
7891
                }
7892
            }
7893
7894
            // Checkbox review answers. Not implemented.
7895
            /*if ($this->review_answers &&
7896
                !in_array($question_obj->type, Question::question_type_no_review())
7897
            ) {
7898
                $remind_question_div = Display::tag(
7899
                    'label',
7900
                    Display::input(
7901
                        'checkbox',
7902
                        'remind_list['.$questionId.']',
7903
                        '',
7904
                        $attributes
7905
                    ).get_lang('Revise question later'),
7906
                    [
7907
                        'class' => 'checkbox',
7908
                        'for' => 'remind_list['.$questionId.']',
7909
                    ]
7910
                );
7911
                $exercise_actions .= Display::div(
7912
                    $remind_question_div,
7913
                    ['class' => 'exercise_save_now_button']
7914
                );
7915
            }*/
7916
7917
            echo Display::div(' ', ['class' => 'clear']);
7918
7919
            $paginationCounter = null;
7920
            if (ONE_PER_PAGE == $this->type) {
7921
                if (empty($questions_in_media)) {
7922
                    $paginationCounter = Display::paginationIndicator(
7923
                        $current_question,
7924
                        count($realQuestionList)
7925
                    );
7926
                } else {
7927
                    if ($last_question_in_media) {
7928
                        $paginationCounter = Display::paginationIndicator(
7929
                            $current_question,
7930
                            count($realQuestionList)
7931
                        );
7932
                    }
7933
                }
7934
            }
7935
7936
            echo '<div class="row"><div class="pull-right">'.$paginationCounter.'</div></div>';
7937
            echo Display::div($exercise_actions, ['class' => 'form-actions']);
7938
            echo '</div>';
7939
        }
7940
    }
7941
7942
    /**
7943
     * Returns an array of categories details for the questions of the current
7944
     * exercise.
7945
     *
7946
     * @return array
7947
     */
7948
    public function getQuestionWithCategories()
7949
    {
7950
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
7951
        $categoryRelTable = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
7952
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
7953
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
7954
        $sql = "SELECT DISTINCT cat.*
7955
                FROM $TBL_EXERCICE_QUESTION e
7956
                INNER JOIN $TBL_QUESTIONS q
7957
                ON (e.question_id = q.iid)
7958
                INNER JOIN $categoryRelTable catRel
7959
                ON (catRel.question_id = e.question_id)
7960
                INNER JOIN $categoryTable cat
7961
                ON (cat.iid = catRel.category_id)
7962
                WHERE
7963
                  e.quiz_id	= ".(int) ($this->getId());
7964
7965
        $result = Database::query($sql);
7966
        $categoriesInExercise = [];
7967
        if (Database::num_rows($result)) {
7968
            $categoriesInExercise = Database::store_result($result, 'ASSOC');
7969
        }
7970
7971
        return $categoriesInExercise;
7972
    }
7973
7974
    /**
7975
     * Calculate the max_score of the quiz, depending of question inside, and quiz advanced option.
7976
     */
7977
    public function getMaxScore()
7978
    {
7979
        $outMaxScore = 0;
7980
        // list of question's id !!! the array key start at 1 !!!
7981
        $questionList = $this->selectQuestionList(true);
7982
7983
        if ($this->random > 0 && $this->randomByCat > 0) {
7984
            // test is random by category
7985
            // get the $numberRandomQuestions best score question of each category
7986
            $numberRandomQuestions = $this->random;
7987
            $tabCategoriesScores = [];
7988
            foreach ($questionList as $questionId) {
7989
                $questionCategoryId = TestCategory::getCategoryForQuestion($questionId);
7990
                if (!is_array($tabCategoriesScores[$questionCategoryId])) {
7991
                    $tabCategoriesScores[$questionCategoryId] = [];
7992
                }
7993
                $tmpObjQuestion = Question::read($questionId);
7994
                if (is_object($tmpObjQuestion)) {
7995
                    $tabCategoriesScores[$questionCategoryId][] = $tmpObjQuestion->weighting;
7996
                }
7997
            }
7998
7999
            // here we've got an array with first key, the category_id, second key, score of question for this cat
8000
            foreach ($tabCategoriesScores as $tabScores) {
8001
                rsort($tabScores);
8002
                $tabScoresCount = count($tabScores);
8003
                for ($i = 0; $i < min($numberRandomQuestions, $tabScoresCount); $i++) {
8004
                    $outMaxScore += $tabScores[$i];
8005
                }
8006
            }
8007
8008
            return $outMaxScore;
8009
        }
8010
8011
        // standard test, just add each question score
8012
        foreach ($questionList as $questionId) {
8013
            $question = Question::read($questionId, $this->course);
8014
            $outMaxScore += $question->weighting;
8015
        }
8016
8017
        return $outMaxScore;
8018
    }
8019
8020
    /**
8021
     * @return string
8022
     */
8023
    public function get_formated_title()
8024
    {
8025
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
8026
        }
8027
8028
        return api_html_entity_decode($this->selectTitle());
8029
    }
8030
8031
    /**
8032
     * @param string $title
8033
     *
8034
     * @return string
8035
     */
8036
    public static function get_formated_title_variable($title)
8037
    {
8038
        return api_html_entity_decode($title);
8039
    }
8040
8041
    /**
8042
     * @return string
8043
     */
8044
    public function format_title()
8045
    {
8046
        return api_htmlentities($this->title);
8047
    }
8048
8049
    /**
8050
     * @param string $title
8051
     *
8052
     * @return string
8053
     */
8054
    public static function format_title_variable($title)
8055
    {
8056
        return api_htmlentities($title);
8057
    }
8058
8059
    /**
8060
     * @param int $courseId
8061
     * @param int $sessionId
8062
     *
8063
     * @return array exercises
8064
     */
8065
    public function getExercisesByCourseSession($courseId, $sessionId)
8066
    {
8067
        $courseId = (int) $courseId;
8068
        $sessionId = (int) $sessionId;
8069
8070
        $tbl_quiz = Database::get_course_table(TABLE_QUIZ_TEST);
8071
        $sql = "SELECT * FROM $tbl_quiz cq
8072
                WHERE
8073
                    cq.c_id = %s AND
8074
                    (cq.session_id = %s OR cq.session_id = 0) AND
8075
                    cq.active = 0
8076
                ORDER BY cq.iid";
8077
        $sql = sprintf($sql, $courseId, $sessionId);
8078
8079
        $result = Database::query($sql);
8080
8081
        $rows = [];
8082
        while ($row = Database::fetch_assoc($result)) {
8083
            $rows[] = $row;
8084
        }
8085
8086
        return $rows;
8087
    }
8088
8089
    /**
8090
     * @param int   $courseId
8091
     * @param int   $sessionId
8092
     * @param array $quizId
8093
     *
8094
     * @return array exercises
8095
     */
8096
    public function getExerciseAndResult($courseId, $sessionId, $quizId = [])
8097
    {
8098
        if (empty($quizId)) {
8099
            return [];
8100
        }
8101
8102
        $sessionId = (int) $sessionId;
8103
        $courseId = (int) $courseId;
8104
8105
        $ids = is_array($quizId) ? $quizId : [$quizId];
8106
        $ids = array_map('intval', $ids);
8107
        $ids = implode(',', $ids);
8108
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8109
        if (0 != $sessionId) {
8110
            $sql = "SELECT * FROM $track_exercises te
8111
              INNER JOIN c_quiz cq
8112
              ON cq.iid = te.exe_exo_id
8113
              WHERE
8114
              te.c_id = %d AND
8115
              te.session_id = %s AND
8116
              cq.iid IN (%s)
8117
              ORDER BY cq.iid";
8118
8119
            $sql = sprintf($sql, $courseId, $sessionId, $ids);
8120
        } else {
8121
            $sql = "SELECT * FROM $track_exercises te
8122
              INNER JOIN c_quiz cq ON cq.iid = te.exe_exo_id
8123
              WHERE
8124
              te.c_id = %d AND
8125
              cq.iid IN (%s)
8126
              ORDER BY cq.iid";
8127
            $sql = sprintf($sql, $courseId, $ids);
8128
        }
8129
        $result = Database::query($sql);
8130
        $rows = [];
8131
        while ($row = Database::fetch_assoc($result)) {
8132
            $rows[] = $row;
8133
        }
8134
8135
        return $rows;
8136
    }
8137
8138
    /**
8139
     * @param $exeId
8140
     * @param $exercise_stat_info
8141
     * @param $remindList
8142
     * @param $currentQuestion
8143
     *
8144
     * @return int|null
8145
     */
8146
    public static function getNextQuestionId(
8147
        $exeId,
8148
        $exercise_stat_info,
8149
        $remindList,
8150
        $currentQuestion
8151
    ) {
8152
        $result = Event::get_exercise_results_by_attempt($exeId, 'incomplete');
8153
8154
        if (isset($result[$exeId])) {
8155
            $result = $result[$exeId];
8156
        } else {
8157
            return null;
8158
        }
8159
8160
        $data_tracking = $exercise_stat_info['data_tracking'];
8161
        $data_tracking = explode(',', $data_tracking);
8162
8163
        // if this is the final question do nothing.
8164
        if ($currentQuestion == count($data_tracking)) {
8165
            return null;
8166
        }
8167
8168
        $currentQuestion--;
8169
8170
        if (!empty($result['question_list'])) {
8171
            $answeredQuestions = [];
8172
            foreach ($result['question_list'] as $question) {
8173
                if (!empty($question['answer'])) {
8174
                    $answeredQuestions[] = $question['question_id'];
8175
                }
8176
            }
8177
8178
            // Checking answered questions
8179
            $counterAnsweredQuestions = 0;
8180
            foreach ($data_tracking as $questionId) {
8181
                if (!in_array($questionId, $answeredQuestions)) {
8182
                    if ($currentQuestion != $counterAnsweredQuestions) {
8183
                        break;
8184
                    }
8185
                }
8186
                $counterAnsweredQuestions++;
8187
            }
8188
8189
            $counterRemindListQuestions = 0;
8190
            // Checking questions saved in the reminder list
8191
            if (!empty($remindList)) {
8192
                foreach ($data_tracking as $questionId) {
8193
                    if (in_array($questionId, $remindList)) {
8194
                        // Skip the current question
8195
                        if ($currentQuestion != $counterRemindListQuestions) {
8196
                            break;
8197
                        }
8198
                    }
8199
                    $counterRemindListQuestions++;
8200
                }
8201
8202
                if ($counterRemindListQuestions < $currentQuestion) {
8203
                    return null;
8204
                }
8205
8206
                if (!empty($counterRemindListQuestions)) {
8207
                    if ($counterRemindListQuestions > $counterAnsweredQuestions) {
8208
                        return $counterAnsweredQuestions;
8209
                    } else {
8210
                        return $counterRemindListQuestions;
8211
                    }
8212
                }
8213
            }
8214
8215
            return $counterAnsweredQuestions;
8216
        }
8217
    }
8218
8219
    /**
8220
     * Gets the position of a questionId in the question list.
8221
     *
8222
     * @param $questionId
8223
     *
8224
     * @return int
8225
     */
8226
    public function getPositionInCompressedQuestionList($questionId)
8227
    {
8228
        $questionList = $this->getQuestionListWithMediasCompressed();
8229
        $mediaQuestions = $this->getMediaList();
8230
        $position = 1;
8231
        foreach ($questionList as $id) {
8232
            if (isset($mediaQuestions[$id]) && in_array($questionId, $mediaQuestions[$id])) {
8233
                $mediaQuestionList = $mediaQuestions[$id];
8234
                if (in_array($questionId, $mediaQuestionList)) {
8235
                    return $position;
8236
                } else {
8237
                    $position++;
8238
                }
8239
            } else {
8240
                if ($id == $questionId) {
8241
                    return $position;
8242
                } else {
8243
                    $position++;
8244
                }
8245
            }
8246
        }
8247
8248
        return 1;
8249
    }
8250
8251
    /**
8252
     * Get the correct answers in all attempts.
8253
     *
8254
     * @param int  $learnPathId
8255
     * @param int  $learnPathItemId
8256
     * @param bool $onlyCorrect
8257
     *
8258
     * @return array
8259
     */
8260
    public function getAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0, $onlyCorrect = true)
8261
    {
8262
        $attempts = Event::getExerciseResultsByUser(
8263
            api_get_user_id(),
8264
            $this->getId(),
8265
            api_get_course_int_id(),
8266
            api_get_session_id(),
8267
            $learnPathId,
8268
            $learnPathItemId,
8269
            'DESC'
8270
        );
8271
8272
        $list = [];
8273
        foreach ($attempts as $attempt) {
8274
            foreach ($attempt['question_list'] as $answers) {
8275
                foreach ($answers as $answer) {
8276
                    $objAnswer = new Answer($answer['question_id']);
8277
                    if ($onlyCorrect) {
8278
                        switch ($objAnswer->getQuestionType()) {
8279
                            case FILL_IN_BLANKS:
8280
                                $isCorrect = FillBlanks::isCorrect($answer['answer']);
8281
8282
                                break;
8283
                            case MATCHING:
8284
                            case DRAGGABLE:
8285
                            case MATCHING_DRAGGABLE:
8286
                                $isCorrect = Matching::isCorrect(
8287
                                    $answer['position'],
8288
                                    $answer['answer'],
8289
                                    $answer['question_id']
8290
                                );
8291
8292
                                break;
8293
                            case ORAL_EXPRESSION:
8294
                                $isCorrect = false;
8295
8296
                                break;
8297
                            default:
8298
                                $isCorrect = $objAnswer->isCorrectByAutoId($answer['answer']);
8299
                        }
8300
                        if ($isCorrect) {
8301
                            $list[$answer['question_id']][] = $answer;
8302
                        }
8303
                    } else {
8304
                        $list[$answer['question_id']][] = $answer;
8305
                    }
8306
                }
8307
            }
8308
8309
            if (false === $onlyCorrect) {
8310
                // Only take latest attempt
8311
                break;
8312
            }
8313
        }
8314
8315
        return $list;
8316
    }
8317
8318
    /**
8319
     * Get the correct answers in all attempts.
8320
     *
8321
     * @param int $learnPathId
8322
     * @param int $learnPathItemId
8323
     *
8324
     * @return array
8325
     */
8326
    public function getCorrectAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0)
8327
    {
8328
        return $this->getAnswersInAllAttempts($learnPathId, $learnPathItemId);
8329
    }
8330
8331
    /**
8332
     * @return bool
8333
     */
8334
    public function showPreviousButton()
8335
    {
8336
        $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
8337
        if (false === $allow) {
8338
            return true;
8339
        }
8340
8341
        return $this->showPreviousButton;
8342
    }
8343
8344
    public function getPreventBackwards()
8345
    {
8346
        return (int) $this->preventBackwards;
8347
    }
8348
8349
    /**
8350
     * @return int
8351
     */
8352
    public function getQuizCategoryId(): ?int
8353
    {
8354
        if (empty($this->quizCategoryId)) {
8355
            return null;
8356
        }
8357
8358
        return (int) $this->quizCategoryId;
8359
    }
8360
8361
    /**
8362
     * @param int $value
8363
     */
8364
    public function setQuizCategoryId($value): void
8365
    {
8366
        if (!empty($value)) {
8367
            $this->quizCategoryId = (int) $value;
8368
        }
8369
    }
8370
8371
    /**
8372
     * Set the value to 1 to hide the question number.
8373
     *
8374
     * @param int $value
8375
     */
8376
    public function setHideQuestionNumber($value = 0)
8377
    {
8378
        $this->hideQuestionNumber = (int) $value;
8379
    }
8380
8381
    /**
8382
     * Gets the value to hide or show the question number. If it does not exist, it is set to 0.
8383
     *
8384
     * @return int 1 if the question number must be hidden
8385
     */
8386
    public function getHideQuestionNumber()
8387
    {
8388
        return (int) $this->hideQuestionNumber;
8389
    }
8390
8391
    public function setPageResultConfiguration(array $values)
8392
    {
8393
        $pageConfig = ('true' === api_get_setting('exercise.allow_quiz_results_page_config'));
8394
        if ($pageConfig) {
8395
            $params = [
8396
                'hide_expected_answer' => $values['hide_expected_answer'] ?? '',
8397
                'hide_question_score' => $values['hide_question_score'] ?? '',
8398
                'hide_total_score' => $values['hide_total_score'] ?? '',
8399
                'hide_category_table' => $values['hide_category_table'] ?? '',
8400
                'hide_correct_answered_questions' => $values['hide_correct_answered_questions'] ?? '',
8401
            ];
8402
            $this->pageResultConfiguration = $params;
8403
        }
8404
    }
8405
8406
    /**
8407
     * @param array $defaults
8408
     */
8409
    public function setPageResultConfigurationDefaults(&$defaults)
8410
    {
8411
        $configuration = $this->getPageResultConfiguration();
8412
        if (!empty($configuration) && !empty($defaults)) {
8413
            $defaults = array_merge($defaults, $configuration);
8414
        }
8415
    }
8416
8417
    /**
8418
     * @return array
8419
     */
8420
    public function getPageResultConfiguration()
8421
    {
8422
        $pageConfig = ('true' === api_get_setting('exercise.allow_quiz_results_page_config'));
8423
        if ($pageConfig) {
8424
            return $this->pageResultConfiguration;
8425
        }
8426
8427
        return [];
8428
    }
8429
8430
    /**
8431
     * @param string $attribute
8432
     *
8433
     * @return mixed|null
8434
     */
8435
    public function getPageConfigurationAttribute($attribute)
8436
    {
8437
        $result = $this->getPageResultConfiguration();
8438
8439
        if (!empty($result)) {
8440
            return $result[$attribute] ?? null;
8441
        }
8442
8443
        return null;
8444
    }
8445
8446
    /**
8447
     * @param bool $showPreviousButton
8448
     *
8449
     * @return Exercise
8450
     */
8451
    public function setShowPreviousButton($showPreviousButton)
8452
    {
8453
        $this->showPreviousButton = $showPreviousButton;
8454
8455
        return $this;
8456
    }
8457
8458
    /**
8459
     * @param array $notifications
8460
     */
8461
    public function setNotifications($notifications)
8462
    {
8463
        $this->notifications = $notifications;
8464
    }
8465
8466
    /**
8467
     * @return array
8468
     */
8469
    public function getNotifications()
8470
    {
8471
        return $this->notifications;
8472
    }
8473
8474
    /**
8475
     * @return bool
8476
     */
8477
    public function showExpectedChoice()
8478
    {
8479
        return ('true' === api_get_setting('exercise.show_exercise_expected_choice'));
8480
    }
8481
8482
    /**
8483
     * @return bool
8484
     */
8485
    public function showExpectedChoiceColumn()
8486
    {
8487
        if (true === $this->forceShowExpectedChoiceColumn) {
8488
            return true;
8489
        }
8490
        if ($this->hideExpectedAnswer) {
8491
            return false;
8492
        }
8493
        if (!in_array(
8494
            $this->results_disabled,
8495
            [
8496
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
8497
            ]
8498
        )
8499
        ) {
8500
            $hide = (int) $this->getPageConfigurationAttribute('hide_expected_answer');
8501
            if (1 === $hide) {
8502
                return false;
8503
            }
8504
8505
            return true;
8506
        }
8507
8508
        return false;
8509
    }
8510
8511
    public function getQuestionRibbon(string $class, string $scoreLabel, ?string $result, array $array): string
8512
    {
8513
        $hide = (int) $this->getPageConfigurationAttribute('hide_question_score');
8514
        if (1 === $hide) {
8515
            return '';
8516
        }
8517
8518
        $ribbon = '<div class="question-answer-result__header-ribbon-title question-answer-result__header-ribbon-title--'.$class.'">'.$scoreLabel.'</div>';
8519
        if (!empty($result)) {
8520
            $ribbon .= '<div class="question-answer-result__header-ribbon-detail">'
8521
                .get_lang('Score').': '.$result
8522
                .'</div>';
8523
        }
8524
8525
        $ribbonClassModifier = '';
8526
8527
        if ($this->showExpectedChoice()) {
8528
            $hideLabel = ('true' === api_get_setting('exercise.exercise_hide_label'));
8529
            if (true === $hideLabel) {
8530
                $ribbonClassModifier = 'question-answer-result__header-ribbon--no-ribbon';
8531
                $html = '';
8532
                $answerUsed = (int) $array['used'];
8533
                $answerMissing = (int) $array['missing'] - $answerUsed;
8534
                for ($i = 1; $i <= $answerUsed; $i++) {
8535
                    $html .= Display::getMdiIcon(StateIcon::COMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL);
8536
                }
8537
                for ($i = 1; $i <= $answerMissing; $i++) {
8538
                    $html .= Display::getMdiIcon(StateIcon::INCOMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL);
8539
                }
8540
                $ribbon = '<div class="question-answer-result__header-ribbon-title hide-label-title">'
8541
                    .get_lang('Correct answers').': '.$result.'</div>'
8542
                    .'<div class="question-answer-result__header-ribbon-detail">'.$html.'</div>';
8543
            }
8544
        }
8545
8546
        return Display::div(
8547
            $ribbon,
8548
            ['class' => "question-answer-result__header-ribbon $ribbonClassModifier"]
8549
        );
8550
    }
8551
8552
    /**
8553
     * @return int
8554
     */
8555
    public function getAutoLaunch()
8556
    {
8557
        return $this->autolaunch;
8558
    }
8559
8560
    /**
8561
     * Clean auto launch settings for all exercise in course/course-session.
8562
     */
8563
    public function enableAutoLaunch()
8564
    {
8565
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8566
        $sql = "UPDATE $table SET autolaunch = 1
8567
                WHERE iid = ".$this->iId;
8568
        Database::query($sql);
8569
    }
8570
8571
    /**
8572
     * Clean auto launch settings for all exercise in course/course-session.
8573
     */
8574
    public function cleanCourseLaunchSettings()
8575
    {
8576
        $em = Database::getManager();
8577
8578
        $repo = Container::getQuizRepository();
8579
8580
        $session = api_get_session_entity();
8581
        $course = api_get_course_entity();
8582
8583
        $qb = $repo->getResourcesByCourse($course, $session);
8584
        $quizzes = $qb->getQuery()->getResult();
8585
8586
        foreach ($quizzes as $quiz) {
8587
            $quiz->setAutoLaunch(false);
8588
            $em->persist($quiz);
8589
        }
8590
8591
        $em->flush();
8592
    }
8593
8594
    /**
8595
     * Get the title without HTML tags.
8596
     *
8597
     * @return string
8598
     */
8599
    public function getUnformattedTitle()
8600
    {
8601
        return strip_tags(api_html_entity_decode($this->title));
8602
    }
8603
8604
    /**
8605
     * Get the question IDs from quiz_rel_question for the current quiz,
8606
     * using the parameters as the arguments to the SQL's LIMIT clause.
8607
     * Because the exercise_id is known, it also comes with a filter on
8608
     * the session, so sessions are not specified here.
8609
     *
8610
     * @param int $start  At which question do we want to start the list
8611
     * @param int $length Up to how many results we want
8612
     *
8613
     * @return array A list of question IDs
8614
     */
8615
    public function getQuestionForTeacher($start = 0, $length = 10)
8616
    {
8617
        $start = (int) $start;
8618
        if ($start < 0) {
8619
            $start = 0;
8620
        }
8621
8622
        $length = (int) $length;
8623
8624
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8625
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
8626
        $sql = "SELECT DISTINCT e.question_id
8627
                FROM $quizRelQuestion e
8628
                INNER JOIN $question q
8629
                ON (e.question_id = q.iid)
8630
                WHERE
8631
8632
                    e.quiz_id = '".$this->getId()."'
8633
                ORDER BY question_order
8634
                LIMIT $start, $length
8635
            ";
8636
        $result = Database::query($sql);
8637
        $questionList = [];
8638
        while ($object = Database::fetch_object($result)) {
8639
            $questionList[] = $object->question_id;
8640
        }
8641
8642
        return $questionList;
8643
    }
8644
8645
    /**
8646
     * @param int   $exerciseId
8647
     * @param array $courseInfo
8648
     * @param int   $sessionId
8649
     *
8650
     * @return bool
8651
     */
8652
    public function generateStats($exerciseId, $courseInfo, $sessionId)
8653
    {
8654
        $allowStats = ('true' === api_get_setting('gradebook.allow_gradebook_stats'));
8655
        if (!$allowStats) {
8656
            return false;
8657
        }
8658
8659
        if (empty($courseInfo)) {
8660
            return false;
8661
        }
8662
8663
        $courseId = $courseInfo['real_id'];
8664
8665
        $sessionId = (int) $sessionId;
8666
        $exerciseId = (int) $exerciseId;
8667
8668
        $result = $this->read($exerciseId);
8669
8670
        if (empty($result)) {
8671
            api_not_allowed(true);
8672
        }
8673
8674
        $statusToFilter = empty($sessionId) ? STUDENT : 0;
8675
8676
        $studentList = CourseManager::get_user_list_from_course_code(
8677
            $courseInfo['code'],
8678
            $sessionId,
8679
            null,
8680
            null,
8681
            $statusToFilter
8682
        );
8683
8684
        if (empty($studentList)) {
8685
            Display::addFlash(Display::return_message(get_lang('No users in course')));
8686
            header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
8687
            exit;
8688
        }
8689
8690
        $tblStats = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8691
8692
        $studentIdList = [];
8693
        if (!empty($studentList)) {
8694
            $studentIdList = array_column($studentList, 'user_id');
8695
        }
8696
8697
        $sessionCondition = api_get_session_condition($sessionId);
8698
        if (false == $this->exercise_was_added_in_lp) {
8699
            $sql = "SELECT * FROM $tblStats
8700
                        WHERE
8701
                            exe_exo_id = $exerciseId AND
8702
                            orig_lp_id = 0 AND
8703
                            orig_lp_item_id = 0 AND
8704
                            status <> 'incomplete' AND
8705
                            c_id = $courseId
8706
                            $sessionCondition
8707
                        ";
8708
        } else {
8709
            $lpId = null;
8710
            if (!empty($this->lpList)) {
8711
                // Taking only the first LP
8712
                $lpId = $this->getLpBySession($sessionId);
8713
                $lpId = $lpId['lp_id'];
8714
            }
8715
8716
            $sql = "SELECT *
8717
                        FROM $tblStats
8718
                        WHERE
8719
                            exe_exo_id = $exerciseId AND
8720
                            orig_lp_id = $lpId AND
8721
                            status <> 'incomplete' AND
8722
                            session_id = $sessionId AND
8723
                            c_id = $courseId ";
8724
        }
8725
8726
        $sql .= ' ORDER BY exe_id DESC';
8727
8728
        $studentCount = 0;
8729
        $sum = 0;
8730
        $bestResult = 0;
8731
        $sumResult = 0;
8732
        $result = Database::query($sql);
8733
        while ($data = Database::fetch_assoc($result)) {
8734
            // Only take into account users in the current student list.
8735
            if (!empty($studentIdList)) {
8736
                if (!in_array($data['exe_user_id'], $studentIdList)) {
8737
                    continue;
8738
                }
8739
            }
8740
8741
            if (!isset($students[$data['exe_user_id']])) {
8742
                if (0 != $data['max_score']) {
8743
                    $students[$data['exe_user_id']] = $data['score'];
8744
                    if ($data['score'] > $bestResult) {
8745
                        $bestResult = $data['score'];
8746
                    }
8747
                    $sumResult += $data['score'];
8748
                }
8749
            }
8750
        }
8751
8752
        $count = count($studentList);
8753
        $average = $sumResult / $count;
8754
        $em = Database::getManager();
8755
8756
        $links = AbstractLink::getGradebookLinksFromItem(
8757
            $this->getId(),
8758
            LINK_EXERCISE,
8759
            $courseInfo['real_id'],
8760
            $sessionId
8761
        );
8762
8763
        if (empty($links)) {
8764
            $links = AbstractLink::getGradebookLinksFromItem(
8765
                $this->iId,
8766
                LINK_EXERCISE,
8767
                $courseInfo['real_id'],
8768
                $sessionId
8769
            );
8770
        }
8771
8772
        if (!empty($links)) {
8773
            $repo = $em->getRepository(GradebookLink::class);
8774
8775
            foreach ($links as $link) {
8776
                $linkId = $link['id'];
8777
                /** @var GradebookLink $exerciseLink */
8778
                $exerciseLink = $repo->find($linkId);
8779
                if ($exerciseLink) {
8780
                    $exerciseLink
8781
                        ->setUserScoreList($students)
8782
                        ->setBestScore($bestResult)
8783
                        ->setAverageScore($average)
8784
                        ->setScoreWeight($this->getMaxScore());
8785
                    $em->persist($exerciseLink);
8786
                    $em->flush();
8787
                }
8788
            }
8789
        }
8790
    }
8791
8792
    /**
8793
     * Return an HTML table of exercises for on-screen printing, including
8794
     * action icons. If no exercise is present and the user can edit the
8795
     * course, show a "create test" button.
8796
     *
8797
     * @param int    $categoryId
8798
     * @param string $keyword
8799
     * @param int    $userId
8800
     * @param int    $courseId
8801
     * @param int    $sessionId
8802
     * @param bool   $returnData
8803
     * @param int    $minCategoriesInExercise
8804
     * @param int    $filterByResultDisabled
8805
     * @param int    $filterByAttempt
8806
     *
8807
     * @return string|SortableTableFromArrayConfig
8808
     */
8809
    public static function exerciseGridResource(
8810
        $categoryId,
8811
        $keyword = '',
8812
        $userId = 0,
8813
        $courseId = 0,
8814
        $sessionId = 0,
8815
        $returnData = false,
8816
        $minCategoriesInExercise = 0,
8817
        $filterByResultDisabled = 0,
8818
        $filterByAttempt = 0,
8819
        $myActions = null,
8820
        $returnTable = false
8821
    ) {
8822
        $is_allowedToEdit = api_is_allowed_to_edit(null, true);
8823
        $courseId = $courseId ? (int) $courseId : api_get_course_int_id();
8824
        $sessionId = $sessionId ? (int) $sessionId : api_get_session_id();
8825
8826
        $course = api_get_course_entity($courseId);
8827
        $session = api_get_session_entity($sessionId);
8828
8829
        $userId = $userId ? (int) $userId : api_get_user_id();
8830
        $user = api_get_user_entity($userId);
8831
8832
        $repo = Container::getQuizRepository();
8833
8834
        $trackEExerciseRepo = Container::getTrackEExerciseRepository();
8835
        $pendingCorrections = $trackEExerciseRepo->getPendingCorrectionsByExercise($courseId);
8836
        $pendingAttempts = [];
8837
        foreach ($pendingCorrections as $correction) {
8838
            $pendingAttempts[$correction['exerciseId']] = $correction['pendingCount'];
8839
        }
8840
8841
        // 2. Get query builder from repo.
8842
        $qb = $repo->getResourcesByCourse($course, $session);
8843
8844
        if (!empty($categoryId)) {
8845
            $qb->andWhere($qb->expr()->eq('resource.quizCategory', $categoryId));
8846
        } else {
8847
            $qb->andWhere($qb->expr()->isNull('resource.quizCategory'));
8848
        }
8849
8850
        $allowDelete = self::allowAction('delete');
8851
        $allowClean = self::allowAction('clean_results');
8852
8853
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8854
8855
        $categoryId = (int) $categoryId;
8856
        $keyword = Database::escape_string($keyword);
8857
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
8858
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
8859
8860
        $courseId = $course->getId();
8861
        $tableRows = [];
8862
        $origin = api_get_origin();
8863
        $charset = 'utf-8';
8864
        $token = Security::get_token();
8865
        $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh($userId, ['real_id' => $courseId]);
8866
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
8867
        $content = '';
8868
        $column = 0;
8869
        if ($is_allowedToEdit) {
8870
            $column = 1;
8871
        }
8872
8873
        $table = new SortableTableFromArrayConfig(
8874
            [],
8875
            $column,
8876
            self::PAGINATION_ITEMS_PER_PAGE,
8877
            'exercises_cat_'.$categoryId.'_'.api_get_course_int_id().'_'.api_get_session_id()
8878
        );
8879
8880
        $limit = $table->per_page;
8881
        $page = $table->page_nr;
8882
        $from = $limit * ($page - 1);
8883
8884
        $categoryCondition = '';
8885
        if ('true' === api_get_setting('exercise.allow_exercise_categories')) {
8886
            if (!empty($categoryId)) {
8887
                $categoryCondition = " AND quiz_category_id = $categoryId ";
8888
            } else {
8889
                $categoryCondition = ' AND quiz_category_id IS NULL ';
8890
            }
8891
        }
8892
8893
        if (!empty($keyword)) {
8894
            $qb->andWhere($qb->expr()->like('resource.title', ':keyword'));
8895
            $qb->setParameter('keyword', '%'.$keyword.'%');
8896
        }
8897
8898
        // Only for administrators
8899
        if ($is_allowedToEdit) {
8900
            $qb->andWhere($qb->expr()->neq('resource.active', -1));
8901
        } else {
8902
            $qb->andWhere($qb->expr()->eq('resource.active', 1));
8903
        }
8904
8905
        $qb->setFirstResult($from);
8906
        $qb->setMaxResults($limit);
8907
8908
        $filterByResultDisabledCondition = '';
8909
        $filterByResultDisabled = (int) $filterByResultDisabled;
8910
        if (!empty($filterByResultDisabled)) {
8911
            $filterByResultDisabledCondition = ' AND e.results_disabled = '.$filterByResultDisabled;
8912
        }
8913
        $filterByAttemptCondition = '';
8914
        $filterByAttempt = (int) $filterByAttempt;
8915
        if (!empty($filterByAttempt)) {
8916
            $filterByAttemptCondition = ' AND e.max_attempt = '.$filterByAttempt;
8917
        }
8918
8919
        $exerciseList = $qb->getQuery()->getResult();
8920
8921
        $total = $repo->getCount($qb);
8922
8923
        $webPath = api_get_path(WEB_CODE_PATH);
8924
        if (!empty($exerciseList)) {
8925
            $visibilitySetting = ('true' === api_get_setting('lp.show_hidden_exercise_added_to_lp'));
8926
            //avoid sending empty parameters
8927
            $mylpid = empty($learnpath_id) ? '' : '&learnpath_id='.$learnpath_id;
8928
            $mylpitemid = empty($learnpath_item_id) ? '' : '&learnpath_item_id='.$learnpath_item_id;
8929
8930
            /** @var CQuiz $exerciseEntity */
8931
            foreach ($exerciseList as $exerciseEntity) {
8932
                $currentRow = [];
8933
                $exerciseId = $exerciseEntity->getIid();
8934
                $actions = '';
8935
                $attempt_text = '';
8936
                $exercise = new Exercise($courseId);
8937
                $exercise->read($exerciseId, false);
8938
8939
                if (empty($exercise->iId)) {
8940
                    continue;
8941
                }
8942
8943
                $sessionId = api_get_session_id();
8944
                $allowToEditBaseCourse = true;
8945
                $visibility = $visibilityInCourse = $exerciseEntity->isVisible($course);
8946
                $visibilityInSession = false;
8947
                if (!empty($sessionId)) {
8948
                    // If we are in a session, the test is invisible
8949
                    // in the base course, it is included in a LP
8950
                    // *and* the setting to show it is *not*
8951
                    // specifically set to true, then hide it.
8952
                    if (false === $visibility) {
8953
                        if (!$visibilitySetting) {
8954
                            if ($exercise->exercise_was_added_in_lp) {
8955
                                continue;
8956
                            }
8957
                        }
8958
                    }
8959
8960
                    $visibility = $visibilityInSession = $exerciseEntity->isVisible($course, $session);
8961
                }
8962
8963
                // Validation when belongs to a session
8964
                $isBaseCourseExercise = true;
8965
                if (!($visibilityInCourse && $visibilityInSession)) {
8966
                    $isBaseCourseExercise = false;
8967
                }
8968
8969
                if (!empty($sessionId) && $isBaseCourseExercise) {
8970
                    $allowToEditBaseCourse = false;
8971
                }
8972
8973
                $resourceLink = $exerciseEntity->getFirstResourceLink();
8974
                if ($resourceLink && !$sessionId && $resourceLink->getSession() === null) {
8975
                    $allowToEditBaseCourse = true;
8976
                }
8977
8978
                $allowToEditSession = ($resourceLink && $resourceLink->getSession() && $resourceLink->getSession()->getId() === $sessionId);
8979
                $sessionStar = null;
8980
                if ($allowToEditSession) {
8981
                    $sessionStar = api_get_session_image($sessionId, $user);
8982
                }
8983
8984
                $locked = $exercise->is_gradebook_locked;
8985
8986
                $startTime = $exerciseEntity->getStartTime();
8987
                $endTime = $exerciseEntity->getEndTime();
8988
                $time_limits = false;
8989
                if (!empty($startTime) || !empty($endTime)) {
8990
                    $time_limits = true;
8991
                }
8992
8993
                $is_actived_time = false;
8994
                if ($time_limits) {
8995
                    // check if start time
8996
                    $start_time = false;
8997
                    if (!empty($startTime)) {
8998
                        $start_time = api_strtotime($startTime->format('Y-m-d H:i:s'), 'UTC');
8999
                    }
9000
                    $end_time = false;
9001
                    if (!empty($endTime)) {
9002
                        $end_time = api_strtotime($endTime->format('Y-m-d H:i:s'), 'UTC');
9003
                    }
9004
                    $now = time();
9005
                    //If both "clocks" are enable
9006
                    if ($start_time && $end_time) {
9007
                        if ($now > $start_time && $end_time > $now) {
9008
                            $is_actived_time = true;
9009
                        }
9010
                    } else {
9011
                        //we check the start and end
9012
                        if ($start_time) {
9013
                            if ($now > $start_time) {
9014
                                $is_actived_time = true;
9015
                            }
9016
                        }
9017
                        if ($end_time) {
9018
                            if ($end_time > $now) {
9019
                                $is_actived_time = true;
9020
                            }
9021
                        }
9022
                    }
9023
                }
9024
9025
                $cut_title = $exercise->getCutTitle();
9026
                $alt_title = '';
9027
                if ($cut_title != $exerciseEntity->getTitle()) {
9028
                    $alt_title = ' title = "'.$exercise->getUnformattedTitle().'" ';
9029
                }
9030
9031
                // Teacher only.
9032
                if ($is_allowedToEdit) {
9033
                    $lp_blocked = null;
9034
                    if (true == $exercise->exercise_was_added_in_lp) {
9035
                        $lp_blocked = Display::div(
9036
                            get_lang(
9037
                                'This exercise has been included in a learning path, so it cannot be accessed by students directly from here. If you want to put the same exercise available through the exercises tool, please make a copy of the current exercise using the copy icon.'
9038
                            ),
9039
                            ['class' => 'lp_content_type_label']
9040
                        );
9041
                    }
9042
9043
                    $style = '';
9044
                    if (!$visibility) {
9045
                        $style = 'color:grey';
9046
                    }
9047
9048
                    $title = $cut_title;
9049
9050
                    $url = '<a
9051
                        '.$alt_title.'
9052
                        id="tooltip_'.$exerciseId.'"
9053
                        href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$exerciseId.'"
9054
                        style = "'.$style.';float:left;"
9055
                        >
9056
                         '.Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, ICON_SIZE_SMALL, $title).$title.
9057
                        '</a>'.$sessionStar;
9058
9059
                    if (ExerciseLib::isQuizEmbeddable($exerciseEntity)) {
9060
                        $embeddableIcon = Display::getMdiIcon('book-music-outline', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('This quiz can be embeddable on videos or mobile content'));
9061
                        $url .= Display::div($embeddableIcon, ['class' => 'pull-right']);
9062
                    }
9063
9064
                    $pendingCount = $pendingAttempts[$exerciseId] ?? 0;
9065
                    if ($pendingCount > 0) {
9066
                        $pendingIcon = Display::getMdiIcon(
9067
                            ActionIcon::ALERT->value,
9068
                            'ch-tool-icon',
9069
                            null,
9070
                            ICON_SIZE_SMALL,
9071
                            get_lang('Pending attempts') . ": $pendingCount"
9072
                        );
9073
                        $url .= " $pendingIcon";
9074
                    }
9075
9076
                    $currentRow['title'] = $url.$lp_blocked;
9077
                    $rowi = $exerciseEntity->getQuestions()->count();
9078
                    if ($allowToEditBaseCourse || $allowToEditSession) {
9079
                        // Questions list
9080
                        $actions = Display::url(
9081
                            Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit')),
9082
                            'admin.php?'.api_get_cidreq().'&exerciseId='.$exerciseId
9083
                        );
9084
9085
                        // Test settings
9086
                        $settings = Display::url(
9087
                            Display::getMdiIcon(ToolIcon::SETTINGS, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Configure')),
9088
                            'exercise_admin.php?'.api_get_cidreq().'&exerciseId='.$exerciseId
9089
                        );
9090
9091
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9092
                            $settings = '';
9093
                        }
9094
                        $actions .= $settings;
9095
9096
                        // Exercise results
9097
                        $resultsLink = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9098
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).'</a>';
9099
9100
                        if ($limitTeacherAccess) {
9101
                            if (api_is_platform_admin()) {
9102
                                $actions .= $resultsLink;
9103
                            }
9104
                        } else {
9105
                            // Exercise results
9106
                            $actions .= $resultsLink;
9107
                        }
9108
9109
                        // Auto launch
9110
                        $autoLaunch = $exercise->getAutoLaunch();
9111
                        if (empty($autoLaunch)) {
9112
                            $actions .= Display::url(
9113
                                Display::getMdiIcon('rocket-launch', 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang('Enable')),
9114
                                'exercise.php?'.api_get_cidreq(
9115
                                ).'&action=enable_launch&sec_token='.$token.'&exerciseId='.$exerciseId
9116
                            );
9117
                        } else {
9118
                            $actions .= Display::url(
9119
                                Display::getMdiIcon('rocket-launch', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Disable')),
9120
                                'exercise.php?'.api_get_cidreq(
9121
                                ).'&action=disable_launch&sec_token='.$token.'&exerciseId='.$exerciseId
9122
                            );
9123
                        }
9124
9125
                        // Export
9126
                        $actions .= Display::url(
9127
                            Display::getMdiIcon('disc', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Copy this exercise as a new one')),
9128
                            '',
9129
                            [
9130
                                'onclick' => "javascript:if(!confirm('".addslashes(
9131
                                        api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES)
9132
                                    )." ".addslashes($title)."?"."')) return false;",
9133
                                'href' => 'exercise.php?'.api_get_cidreq(
9134
                                    ).'&action=copy_exercise&sec_token='.$token.'&exerciseId='.$exerciseId,
9135
                            ]
9136
                        );
9137
9138
                        // Clean exercise
9139
                        $clean = '';
9140
                        if (true === $allowClean) {
9141
                            if (!$locked) {
9142
                                $clean = Display::url(
9143
                                    Display::getMdiIcon(ActionIcon::RESET, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Clear all learners results for this exercise')
9144
                                    ),
9145
                                    '',
9146
                                    [
9147
                                        'onclick' => "javascript:if(!confirm('".
9148
                                            addslashes(
9149
                                                api_htmlentities(
9150
                                                    get_lang('Are you sure to delete results'),
9151
                                                    ENT_QUOTES
9152
                                                )
9153
                                            )." ".addslashes($title)."?"."')) return false;",
9154
                                        'href' => 'exercise.php?'.api_get_cidreq(
9155
                                            ).'&action=clean_results&sec_token='.$token.'&exerciseId='.$exerciseId,
9156
                                    ]
9157
                                );
9158
                            } else {
9159
                                $clean = Display::getMdiIcon(ActionIcon::RESET, 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang('This option is not available because this activity is contained by an assessment, which is currently locked. To unlock the assessment, ask your platform administrator.')
9160
                                );
9161
                            }
9162
                        }
9163
9164
                        $actions .= $clean;
9165
                        // Visible / invisible
9166
                        // Check if this exercise was added in a LP
9167
                        $visibility = '';
9168
                        if (api_is_platform_admin()) {
9169
                            if ($exercise->exercise_was_added_in_lp) {
9170
                                $visibility = Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('This exercise has been included in a learning path, so it cannot be accessed by students directly from here. If you want to put the same exercise available through the exercises tool, please make a copy of the current exercise using the copy icon.')
9171
                                );
9172
                            } else {
9173
                                if (!$exerciseEntity->isVisible($course, $session)) {
9174
                                    $visibility = Display::url(
9175
                                        Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Activate')
9176
                                        ),
9177
                                        'exercise.php?' . api_get_cidreq() . '&action=enable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9178
                                    );
9179
                                } else {
9180
                                    // else if not active
9181
                                    $visibility = Display::url(
9182
                                        Display::getMdiIcon(StateIcon::PUBLIC_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Deactivate')
9183
                                        ),
9184
                                        'exercise.php?' . api_get_cidreq() . '&action=disable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9185
                                    );
9186
                                }
9187
                            }
9188
                        }
9189
9190
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9191
                            $visibility = '';
9192
                        }
9193
9194
                        $actions .= $visibility;
9195
9196
                        // Export qti ...
9197
                        $export = Display::url(
9198
                            Display::getMdiIcon(
9199
                                'database',
9200
                                'ch-tool-icon',
9201
                                null,
9202
                                ICON_SIZE_SMALL,
9203
                                'IMS/QTI'
9204
                            ),
9205
                            'exercise.php?action=exportqti2&exerciseId='.$exerciseId.'&'.api_get_cidreq()
9206
                        );
9207
9208
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9209
                            $export = '';
9210
                        }
9211
9212
                        $actions .= $export;
9213
                    } else {
9214
                        // not session
9215
                        $actions = Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang("You can't edit this course exercise from inside a session")
9216
                        );
9217
9218
                        // Check if this exercise was added in a LP
9219
                        $visibility = '';
9220
                        if (api_is_platform_admin()) {
9221
                            if ($exercise->exercise_was_added_in_lp) {
9222
                                $visibility = Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('This exercise has been included in a learning path, so it cannot be accessed by students directly from here. If you want to put the same exercise available through the exercises tool, please make a copy of the current exercise using the copy icon.')
9223
                                );
9224
                            } else {
9225
                                if (0 === $exerciseEntity->getActive() || 0 == $visibility) {
9226
                                    $visibility = Display::url(
9227
                                        Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Activate')
9228
                                        ),
9229
                                        'exercise.php?' . api_get_cidreq() . '&action=enable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9230
                                    );
9231
                                } else {
9232
                                    // else if not active
9233
                                    $visibility = Display::url(
9234
                                        Display::getMdiIcon(StateIcon::PUBLIC_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Deactivate')
9235
                                        ),
9236
                                        'exercise.php?' . api_get_cidreq() . '&action=disable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9237
                                    );
9238
                                }
9239
                            }
9240
                        }
9241
9242
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9243
                            $visibility = '';
9244
                        }
9245
9246
                        $actions .= $visibility;
9247
                        $actions .= '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9248
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).'</a>';
9249
                        $actions .= Display::url(
9250
                            Display::getMdiIcon('disc', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Copy this exercise as a new one')),
9251
                            '',
9252
                            [
9253
                                'onclick' => "javascript:if(!confirm('".addslashes(
9254
                                        api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES)
9255
                                    )." ".addslashes($title)."?"."')) return false;",
9256
                                'href' => 'exercise.php?'.api_get_cidreq(
9257
                                    ).'&action=copy_exercise&sec_token='.$token.'&exerciseId='.$exerciseId,
9258
                            ]
9259
                        );
9260
                    }
9261
9262
                    // Delete
9263
                    $delete = '';
9264
                    if ($repo->isGranted('DELETE', $exerciseEntity) && $allowToEditBaseCourse) {
9265
                        if (!$locked) {
9266
                            $deleteUrl = 'exercise.php?'.api_get_cidreq().'&action=delete&sec_token='.$token.'&exerciseId='.$exerciseId;
9267
                            $delete = Display::url(
9268
                                Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete')),
9269
                                '',
9270
                                [
9271
                                    'onclick' => "javascript:if(!confirm('".
9272
                                        addslashes(api_htmlentities(get_lang('Are you sure you want to delete')))." ".
9273
                                        addslashes($exercise->getUnformattedTitle())."?"."')) return false;",
9274
                                    'href' => $deleteUrl,
9275
                                ]
9276
                            );
9277
                        } else {
9278
                            $delete = Display::getMdiIcon(
9279
                                ActionIcon::DELETE,
9280
                                'ch-tool-icon-disabled',
9281
                                null,
9282
                                ICON_SIZE_SMALL,
9283
                                get_lang(
9284
                                    'This option is not available because this activity is contained by an assessment, which is currently locked. To unlock the assessment, ask your platform administrator.'
9285
                                )
9286
                            );
9287
                        }
9288
                    }
9289
9290
                    if ($limitTeacherAccess && !api_is_platform_admin()) {
9291
                        $delete = '';
9292
                    }
9293
9294
                    if (!empty($minCategoriesInExercise)) {
9295
                        $cats = TestCategory::getListOfCategoriesForTest($exercise);
9296
                        if (!(count($cats) >= $minCategoriesInExercise)) {
9297
                            continue;
9298
                        }
9299
                    }
9300
                    $actions .= $delete;
9301
9302
                    // Number of questions.
9303
                    $random = $exerciseEntity->getRandom();
9304
                    if ($random > 0 || -1 == $random) {
9305
                        // if random == -1 means use random questions with all questions
9306
                        $random_number_of_question = $random;
9307
                        if (-1 == $random_number_of_question) {
9308
                            $random_number_of_question = $rowi;
9309
                        }
9310
                        if ($exerciseEntity->getRandomByCategory() > 0) {
9311
                            $nbQuestionsTotal = TestCategory::getNumberOfQuestionRandomByCategory(
9312
                                $exerciseId,
9313
                                $random_number_of_question
9314
                            );
9315
                            $number_of_questions = $nbQuestionsTotal.' ';
9316
                            $number_of_questions .= ($nbQuestionsTotal > 1) ? get_lang('questions') : get_lang(
9317
                                'Question lower case'
9318
                            );
9319
                            $number_of_questions .= ' - ';
9320
                            $number_of_questions .= min(
9321
                                    TestCategory::getNumberMaxQuestionByCat($exerciseId),
9322
                                    $random_number_of_question
9323
                                ).' '.get_lang('Question by category');
9324
                        } else {
9325
                            $random_label = ' ('.get_lang('Random').') ';
9326
                            $number_of_questions = $random_number_of_question.' '.$random_label.' / '.$rowi;
9327
                            // Bug if we set a random value bigger than the real number of questions
9328
                            if ($random_number_of_question > $rowi) {
9329
                                $number_of_questions = $rowi.' '.$random_label;
9330
                            }
9331
                        }
9332
                    } else {
9333
                        $number_of_questions = $rowi;
9334
                    }
9335
9336
                    $currentRow['count_questions'] = $number_of_questions;
9337
                } else {
9338
                    // Student only.
9339
                    $visibility = $exerciseEntity->isVisible($course, null);
9340
                    if (false === $visibility && !empty($sessionId)) {
9341
                        $visibility = $exerciseEntity->isVisible($course, $session);
9342
                    }
9343
9344
                    if (false === $visibility) {
9345
                        continue;
9346
                    }
9347
9348
                    $url = '<a '.$alt_title.'
9349
                        href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$exerciseId.'">'.
9350
                        $cut_title.'</a>';
9351
9352
                    // Link of the exercise.
9353
                    $currentRow['title'] = $url.' '.$sessionStar;
9354
                    // This query might be improved later on by ordering by the new "tms" field rather than by exe_id
9355
                    if ($returnData) {
9356
                        $currentRow['title'] = $exercise->getUnformattedTitle();
9357
                    }
9358
9359
                    $sessionCondition = api_get_session_condition(api_get_session_id());
9360
                    // Don't remove this marker: note-query-exe-results
9361
                    $sql = "SELECT * FROM $TBL_TRACK_EXERCISES
9362
                            WHERE
9363
                                exe_exo_id = ".$exerciseId." AND
9364
                                exe_user_id = $userId AND
9365
                                c_id = ".api_get_course_int_id()." AND
9366
                                status <> 'incomplete' AND
9367
                                orig_lp_id = 0 AND
9368
                                orig_lp_item_id = 0
9369
                                $sessionCondition
9370
                            ORDER BY exe_id DESC";
9371
9372
                    $qryres = Database::query($sql);
9373
                    $num = Database:: num_rows($qryres);
9374
9375
                    // Hide the results.
9376
                    $my_result_disabled = $exerciseEntity->getResultsDisabled();
9377
                    $attempt_text = '-';
9378
                    // Time limits are on
9379
                    if ($time_limits) {
9380
                        // Exam is ready to be taken
9381
                        if ($is_actived_time) {
9382
                            // Show results
9383
                            if (
9384
                            in_array(
9385
                                $my_result_disabled,
9386
                                [
9387
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9388
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9389
                                    RESULT_DISABLE_SHOW_SCORE_ONLY,
9390
                                    RESULT_DISABLE_RANKING,
9391
                                ]
9392
                            )
9393
                            ) {
9394
                                // More than one attempt
9395
                                if ($num > 0) {
9396
                                    $row_track = Database:: fetch_array($qryres);
9397
                                    $attempt_text = get_lang('Latest attempt').' : ';
9398
                                    $attempt_text .= ExerciseLib::show_score(
9399
                                        $row_track['score'],
9400
                                        $row_track['max_score']
9401
                                    );
9402
                                } else {
9403
                                    //No attempts
9404
                                    $attempt_text = get_lang('Not attempted');
9405
                                }
9406
                            } else {
9407
                                $attempt_text = '-';
9408
                            }
9409
                        } else {
9410
                            // Quiz not ready due to time limits
9411
                            //@todo use the is_visible function
9412
                            if (!empty($startTime) && !empty($endTime)) {
9413
                                $today = time();
9414
                                if ($today < $start_time) {
9415
                                    $attempt_text = sprintf(
9416
                                        get_lang('Exercise will be activated from %s to %s'),
9417
                                        api_convert_and_format_date($start_time),
9418
                                        api_convert_and_format_date($end_time)
9419
                                    );
9420
                                } else {
9421
                                    if ($today > $end_time) {
9422
                                        $attempt_text = sprintf(
9423
                                            get_lang('Exercise was activated from %s to %s'),
9424
                                            api_convert_and_format_date($start_time),
9425
                                            api_convert_and_format_date($end_time)
9426
                                        );
9427
                                    }
9428
                                }
9429
                            } else {
9430
                                if (!empty($startTime)) {
9431
                                    $attempt_text = sprintf(
9432
                                        get_lang('Exercise available from %s'),
9433
                                        api_convert_and_format_date($start_time)
9434
                                    );
9435
                                }
9436
                                if (!empty($endTime)) {
9437
                                    $attempt_text = sprintf(
9438
                                        get_lang('Exercise available until %s'),
9439
                                        api_convert_and_format_date($end_time)
9440
                                    );
9441
                                }
9442
                            }
9443
                        }
9444
                    } else {
9445
                        // Normal behaviour.
9446
                        // Show results.
9447
                        if (
9448
                        in_array(
9449
                            $my_result_disabled,
9450
                            [
9451
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9452
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9453
                                RESULT_DISABLE_SHOW_SCORE_ONLY,
9454
                                RESULT_DISABLE_RANKING,
9455
                                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
9456
                            ]
9457
                        )
9458
                        ) {
9459
                            if ($num > 0) {
9460
                                $row_track = Database::fetch_array($qryres);
9461
                                $attempt_text = get_lang('Latest attempt').' : ';
9462
                                $attempt_text .= ExerciseLib::show_score(
9463
                                    $row_track['score'],
9464
                                    $row_track['max_score']
9465
                                );
9466
                            } else {
9467
                                $attempt_text = get_lang('Not attempted');
9468
                            }
9469
                        }
9470
                    }
9471
                    if ($returnData) {
9472
                        $attempt_text = $num;
9473
                    }
9474
                }
9475
9476
                $currentRow['attempt'] = $attempt_text;
9477
                $currentRow['iid'] = $exerciseId;
9478
9479
                if ($is_allowedToEdit) {
9480
                    $additionalActions = ExerciseLib::getAdditionalTeacherActions($exerciseId);
9481
9482
                    if (!empty($additionalActions)) {
9483
                        $actions .= $additionalActions.PHP_EOL;
9484
                    }
9485
9486
                    if (!empty($myActions) && is_callable($myActions)) {
9487
                        $actions = $myActions($currentRow);
9488
                    }
9489
                    $currentRow = [
9490
                        $exerciseId,
9491
                        $currentRow['title'],
9492
                        $currentRow['count_questions'],
9493
                        $actions,
9494
                    ];
9495
                } else {
9496
                    $currentRow = [
9497
                        $currentRow['title'],
9498
                        $currentRow['attempt'],
9499
                    ];
9500
9501
                    if ($isDrhOfCourse) {
9502
                        $currentRow[] = '<a
9503
                            href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9504
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).
9505
                            '</a>';
9506
                    }
9507
                    if ($returnData) {
9508
                        $currentRow['id'] = $exercise->id;
9509
                        $currentRow['url'] = $webPath.'exercise/overview.php?'
9510
                            .api_get_cidreq().'&'
9511
                            ."$mylpid$mylpitemid&exerciseId={$exercise->id}";
9512
                        $currentRow['name'] = $currentRow[0];
9513
                    }
9514
                }
9515
                $tableRows[] = $currentRow;
9516
            }
9517
        }
9518
9519
        if (empty($tableRows) && empty($categoryId)) {
9520
            if ($is_allowedToEdit && 'learnpath' !== $origin) {
9521
                if (!empty($_GET['keyword'])) {
9522
                    $content .= Display::return_message(
9523
                        sprintf(get_lang('No result for keyword %s'),Security::remove_XSS($_GET['keyword'])),
9524
                        'warning'
9525
                    );
9526
                } else {
9527
                    $content .= Display::noDataView(
9528
                        get_lang('Test'),
9529
                        Display::getMdiIcon(ToolIcon::QUIZ, 'ch-tool-icon', null, ICON_SIZE_BIG),
9530
                        get_lang('Create a new test'),
9531
                        'exercise_admin.php?'.api_get_cidreq()
9532
                    );
9533
                }
9534
            }
9535
        } else {
9536
            if (empty($tableRows)) {
9537
                return '';
9538
            }
9539
            $table->setTableData($tableRows);
9540
            $table->setTotalNumberOfItems($total);
9541
            $table->set_additional_parameters(
9542
                [
9543
                    'cid' => api_get_course_int_id(),
9544
                    'sid' => api_get_session_id(),
9545
                    'category_id' => $categoryId,
9546
                ]
9547
            );
9548
9549
            if ($is_allowedToEdit) {
9550
                $formActions = [];
9551
                $formActions['visible'] = get_lang('Activate');
9552
                $formActions['invisible'] = get_lang('Deactivate');
9553
                $formActions['delete'] = get_lang('Delete');
9554
                $table->set_form_actions($formActions);
9555
            }
9556
9557
            $i = 0;
9558
            if ($is_allowedToEdit) {
9559
                $table->set_header($i++, '', false, 'width="18px"');
9560
            }
9561
            $table->set_header($i++, get_lang('Test name'), false);
9562
9563
            if ($is_allowedToEdit) {
9564
                $table->set_header($i++, get_lang('Questions'), false, [], ['class' => 'text-center']);
9565
                $table->set_header($i++, get_lang('Actions'), false, [], ['class' => 'text-center']);
9566
            } else {
9567
                $table->set_header($i++, get_lang('Status'), false);
9568
                if ($isDrhOfCourse) {
9569
                    $table->set_header($i++, get_lang('Actions'), false, [], ['class' => 'text-center']);
9570
                }
9571
            }
9572
9573
            if ($returnTable) {
9574
                return $table;
9575
            }
9576
            $content .= $table->return_table();
9577
        }
9578
9579
        return $content;
9580
    }
9581
9582
    /**
9583
     * @return int value in minutes
9584
     */
9585
    public function getResultAccess()
9586
    {
9587
        $extraFieldValue = new ExtraFieldValue('exercise');
9588
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9589
            $this->iId,
9590
            'results_available_for_x_minutes'
9591
        );
9592
9593
        if (!empty($value) && isset($value['value'])) {
9594
            return (int) $value['value'];
9595
        }
9596
9597
        return 0;
9598
    }
9599
9600
    /**
9601
     * @param array $exerciseResultInfo
9602
     *
9603
     * @return bool
9604
     */
9605
    public function getResultAccessTimeDiff($exerciseResultInfo)
9606
    {
9607
        $value = $this->getResultAccess();
9608
        if (!empty($value)) {
9609
            $endDate = new DateTime($exerciseResultInfo['exe_date'], new DateTimeZone('UTC'));
9610
            $endDate->add(new DateInterval('PT'.$value.'M'));
9611
            $now = time();
9612
            if ($endDate->getTimestamp() > $now) {
9613
                return (int) $endDate->getTimestamp() - $now;
9614
            }
9615
        }
9616
9617
        return 0;
9618
    }
9619
9620
    /**
9621
     * @param array $exerciseResultInfo
9622
     *
9623
     * @return bool
9624
     */
9625
    public function hasResultsAccess($exerciseResultInfo)
9626
    {
9627
        $diff = $this->getResultAccessTimeDiff($exerciseResultInfo);
9628
        if (0 === $diff) {
9629
            return false;
9630
        }
9631
9632
        return true;
9633
    }
9634
9635
    /**
9636
     * @return int
9637
     */
9638
    public function getResultsAccess()
9639
    {
9640
        $extraFieldValue = new ExtraFieldValue('exercise');
9641
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9642
            $this->iId,
9643
            'results_available_for_x_minutes'
9644
        );
9645
        if (!empty($value)) {
9646
            return (int) $value;
9647
        }
9648
9649
        return 0;
9650
    }
9651
9652
    /**
9653
     * @param int   $questionId
9654
     * @param bool  $show_results
9655
     * @param array $question_result
9656
     */
9657
    public function getDelineationResult(Question $objQuestionTmp, $questionId, $show_results, $question_result)
9658
    {
9659
        $id = (int) $objQuestionTmp->id;
9660
        $questionId = (int) $questionId;
9661
9662
        $final_overlap = $question_result['extra']['final_overlap'];
9663
        $final_missing = $question_result['extra']['final_missing'];
9664
        $final_excess = $question_result['extra']['final_excess'];
9665
9666
        $overlap_color = $question_result['extra']['overlap_color'];
9667
        $missing_color = $question_result['extra']['missing_color'];
9668
        $excess_color = $question_result['extra']['excess_color'];
9669
9670
        $threadhold1 = $question_result['extra']['threadhold1'];
9671
        $threadhold2 = $question_result['extra']['threadhold2'];
9672
        $threadhold3 = $question_result['extra']['threadhold3'];
9673
9674
        if ($show_results) {
9675
            if ($overlap_color) {
9676
                $overlap_color = 'green';
9677
            } else {
9678
                $overlap_color = 'red';
9679
            }
9680
9681
            if ($missing_color) {
9682
                $missing_color = 'green';
9683
            } else {
9684
                $missing_color = 'red';
9685
            }
9686
            if ($excess_color) {
9687
                $excess_color = 'green';
9688
            } else {
9689
                $excess_color = 'red';
9690
            }
9691
9692
            if (!is_numeric($final_overlap)) {
9693
                $final_overlap = 0;
9694
            }
9695
9696
            if (!is_numeric($final_missing)) {
9697
                $final_missing = 0;
9698
            }
9699
            if (!is_numeric($final_excess)) {
9700
                $final_excess = 0;
9701
            }
9702
9703
            if ($final_excess > 100) {
9704
                $final_excess = 100;
9705
            }
9706
9707
            $table_resume = '
9708
                    <table class="table table-hover table-striped data_table">
9709
                        <tr class="row_odd" >
9710
                            <td>&nbsp;</td>
9711
                            <td><b>'.get_lang('Requirements').'</b></td>
9712
                            <td><b>'.get_lang('Your answer').'</b></td>
9713
                        </tr>
9714
                        <tr class="row_even">
9715
                            <td><b>'.get_lang('Overlapping area').'</b></td>
9716
                            <td>'.get_lang('Minimum overlap').' '.$threadhold1.'</td>
9717
                            <td>
9718
                                <div style="color:'.$overlap_color.'">
9719
                                    '.(($final_overlap < 0) ? 0 : intval($final_overlap)).'
9720
                                </div>
9721
                            </td>
9722
                        </tr>
9723
                        <tr>
9724
                            <td><b>'.get_lang('Excessive area').'</b></td>
9725
                            <td>'.get_lang('Maximum excess').' '.$threadhold2.'</td>
9726
                            <td>
9727
                                <div style="color:'.$excess_color.'">
9728
                                    '.(($final_excess < 0) ? 0 : intval($final_excess)).'
9729
                                </div>
9730
                            </td>
9731
                        </tr>
9732
                        <tr class="row_even">
9733
                            <td><b>'.get_lang('Missing area').'</b></td>
9734
                            <td>'.get_lang('Maximum missing').' '.$threadhold3.'</td>
9735
                            <td>
9736
                                <div style="color:'.$missing_color.'">
9737
                                    '.(($final_missing < 0) ? 0 : intval($final_missing)).'
9738
                                </div>
9739
                            </td>
9740
                        </tr>
9741
                    </table>
9742
                ';
9743
9744
            $answerType = $objQuestionTmp->selectType();
9745
            /*if ($next == 0) {
9746
                $try = $try_hotspot;
9747
                $lp = $lp_hotspot;
9748
                $destinationid = $select_question_hotspot;
9749
                $url = $url_hotspot;
9750
            } else {
9751
                //show if no error
9752
                $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
9753
                $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
9754
            }
9755
            echo '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>';
9756
            if ($organs_at_risk_hit > 0) {
9757
                $message = '<br />'.get_lang('Your result is :').' <b>'.$result_comment.'</b><br />';
9758
                $message .= '<p style="color:#DC0A0A;"><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
9759
            } else {
9760
                $message = '<p>'.get_lang('Your delineation :').'</p>';
9761
                $message .= $table_resume;
9762
                $message .= '<br />'.get_lang('Your result is :').' <b>'.$result_comment.'</b><br />';
9763
            }
9764
            $message .= '<p>'.$comment.'</p>';
9765
            echo $message;*/
9766
9767
            // Showing the score
9768
            /*$queryfree = "SELECT marks FROM $TBL_TRACK_ATTEMPT
9769
                          WHERE exe_id = $id AND question_id =  $questionId";
9770
            $resfree = Database::query($queryfree);
9771
            $questionScore = Database::result($resfree, 0, 'marks');
9772
            $totalScore += $questionScore;*/
9773
            $relPath = api_get_path(REL_CODE_PATH);
9774
            echo '</table></td></tr>';
9775
            echo "
9776
                        <tr>
9777
                            <td colspan=\"2\">
9778
                                <div id=\"hotspot-solution\"></div>
9779
                                <script>
9780
                                    $(function() {
9781
                                        new HotspotQuestion({
9782
                                            questionId: $questionId,
9783
                                            exerciseId: {$this->id},
9784
                                            exeId: $id,
9785
                                            selector: '#hotspot-solution',
9786
                                            for: 'solution',
9787
                                            relPath: '$relPath'
9788
                                        });
9789
                                    });
9790
                                </script>
9791
                            </td>
9792
                        </tr>
9793
                    </table>
9794
                ";
9795
        }
9796
    }
9797
9798
    /**
9799
     * Clean exercise session variables.
9800
     */
9801
    public static function cleanSessionVariables()
9802
    {
9803
        Session::erase('objExercise');
9804
        Session::erase('exe_id');
9805
        Session::erase('calculatedAnswerId');
9806
        Session::erase('duration_time_previous');
9807
        Session::erase('duration_time');
9808
        Session::erase('objQuestion');
9809
        Session::erase('objAnswer');
9810
        Session::erase('questionList');
9811
        Session::erase('categoryList');
9812
        Session::erase('exerciseResult');
9813
        Session::erase('firstTime');
9814
9815
        Session::erase('time_per_question');
9816
        Session::erase('question_start');
9817
        Session::erase('exerciseResultCoordinates');
9818
        Session::erase('hotspot_coord');
9819
        Session::erase('hotspot_dest');
9820
        Session::erase('hotspot_delineation_result');
9821
    }
9822
9823
    /**
9824
     * Get the first LP found matching the session ID.
9825
     *
9826
     * @param int $sessionId
9827
     *
9828
     * @return array
9829
     */
9830
    public function getLpBySession($sessionId)
9831
    {
9832
        if (!empty($this->lpList)) {
9833
            $sessionId = (int) $sessionId;
9834
9835
            foreach ($this->lpList as $lp) {
9836
                if (isset($lp['session_id']) && (int) $lp['session_id'] == $sessionId) {
9837
                    return $lp;
9838
                }
9839
            }
9840
9841
            return current($this->lpList);
9842
        }
9843
9844
        return [
9845
            'lp_id' => 0,
9846
            'max_score' => 0,
9847
            'session_id' => 0,
9848
        ];
9849
    }
9850
9851
    public static function saveExerciseInLp($safe_item_id, $safe_exe_id, $course_id = null)
9852
    {
9853
        $lp = Session::read('oLP');
9854
9855
        $safe_exe_id = (int) $safe_exe_id;
9856
        $safe_item_id = (int) $safe_item_id;
9857
9858
        if (empty($lp) || empty($safe_exe_id) || empty($safe_item_id)) {
9859
            return false;
9860
        }
9861
9862
        $viewId = $lp->get_view_id();
9863
        if (!isset($course_id)) {
9864
            $course_id = api_get_course_int_id();
9865
        }
9866
        $userId = (int) api_get_user_id();
9867
        $viewId = (int) $viewId;
9868
9869
        $TBL_TRACK_EXERCICES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
9870
        $TBL_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
9871
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
9872
9873
        $sql = "SELECT start_date, exe_date, score, max_score, exe_exo_id, exe_duration
9874
                FROM $TBL_TRACK_EXERCICES
9875
                WHERE exe_id = $safe_exe_id AND exe_user_id = $userId";
9876
        $res = Database::query($sql);
9877
        $row_dates = Database::fetch_array($res);
9878
9879
        if (empty($row_dates)) {
9880
            return false;
9881
        }
9882
9883
        $duration = (int) $row_dates['exe_duration'];
9884
        $score = (float) $row_dates['score'];
9885
        $max_score = (float) $row_dates['max_score'];
9886
9887
        $sql = "UPDATE $TBL_LP_ITEM SET
9888
                    max_score = '$max_score'
9889
                WHERE iid = $safe_item_id";
9890
        Database::query($sql);
9891
9892
        $sql = "SELECT iid FROM $TBL_LP_ITEM_VIEW
9893
                WHERE
9894
                    lp_item_id = $safe_item_id AND
9895
                    lp_view_id = $viewId
9896
                ORDER BY iid DESC
9897
                LIMIT 1";
9898
        $res_last_attempt = Database::query($sql);
9899
9900
        if (Database::num_rows($res_last_attempt) && !api_is_invitee()) {
9901
            $row_last_attempt = Database::fetch_row($res_last_attempt);
9902
            $lp_item_view_id = $row_last_attempt[0];
9903
9904
            $exercise = new Exercise($course_id);
9905
            $exercise->read($row_dates['exe_exo_id']);
9906
            $status = 'completed';
9907
9908
            if (!empty($exercise->pass_percentage)) {
9909
                $status = 'failed';
9910
                $success = ExerciseLib::isSuccessExerciseResult(
9911
                    $score,
9912
                    $max_score,
9913
                    $exercise->pass_percentage
9914
                );
9915
                if ($success) {
9916
                    $status = 'passed';
9917
                }
9918
            }
9919
9920
            $sql = "UPDATE $TBL_LP_ITEM_VIEW SET
9921
                        status = '$status',
9922
                        score = '$score',
9923
                        total_time = '$duration'
9924
                    WHERE iid = $lp_item_view_id";
9925
            Database::query($sql);
9926
9927
            $sql = "UPDATE $TBL_TRACK_EXERCICES SET
9928
                        orig_lp_item_view_id = '$lp_item_view_id'
9929
                    WHERE exe_id = ".$safe_exe_id;
9930
            Database::query($sql);
9931
        }
9932
    }
9933
9934
    /**
9935
     * Get the user answers saved in exercise.
9936
     *
9937
     * @param int $attemptId
9938
     *
9939
     * @return array
9940
     */
9941
    public function getUserAnswersSavedInExercise($attemptId)
9942
    {
9943
        $exerciseResult = [];
9944
9945
        $attemptList = Event::getAllExerciseEventByExeId($attemptId);
9946
9947
        foreach ($attemptList as $questionId => $options) {
9948
            foreach ($options as $option) {
9949
                $question = Question::read($option['question_id']);
9950
9951
                if ($question) {
9952
                    switch ($question->type) {
9953
                        case FILL_IN_BLANKS:
9954
                            $option['answer'] = $this->fill_in_blank_answer_to_string($option['answer']);
9955
                            break;
9956
                    }
9957
                }
9958
9959
                if (!empty($option['answer'])) {
9960
                    $exerciseResult[] = $questionId;
9961
9962
                    break;
9963
                }
9964
            }
9965
        }
9966
9967
        return $exerciseResult;
9968
    }
9969
9970
    /**
9971
     * Get the number of user answers saved in exercise.
9972
     *
9973
     * @param int $attemptId
9974
     *
9975
     * @return int
9976
     */
9977
    public function countUserAnswersSavedInExercise($attemptId)
9978
    {
9979
        $answers = $this->getUserAnswersSavedInExercise($attemptId);
9980
9981
        return count($answers);
9982
    }
9983
9984
    public static function allowAction($action)
9985
    {
9986
        if (api_is_platform_admin()) {
9987
            return true;
9988
        }
9989
9990
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
9991
        $disableClean = ('true' === api_get_setting('exercise.disable_clean_exercise_results_for_teachers'));
9992
9993
        switch ($action) {
9994
            case 'delete':
9995
                if (api_is_allowed_to_edit(null, true)) {
9996
                    if ($limitTeacherAccess) {
9997
                        return false;
9998
                    }
9999
10000
                    return true;
10001
                }
10002
                break;
10003
            case 'clean_results':
10004
                if (api_is_allowed_to_edit(null, true)) {
10005
                    if ($limitTeacherAccess) {
10006
                        return false;
10007
                    }
10008
10009
                    if ($disableClean) {
10010
                        return false;
10011
                    }
10012
10013
                    return true;
10014
                }
10015
10016
                break;
10017
        }
10018
10019
        return false;
10020
    }
10021
10022
    public static function getLpListFromExercise($exerciseId, $courseId)
10023
    {
10024
        $tableLpItem = Database::get_course_table(TABLE_LP_ITEM);
10025
        $tblLp = Database::get_course_table(TABLE_LP_MAIN);
10026
10027
        $exerciseId = (int) $exerciseId;
10028
        $courseId = (int) $courseId;
10029
10030
        $sql = "SELECT
10031
                    lp.title,
10032
                    lpi.lp_id,
10033
                    lpi.max_score
10034
                FROM $tableLpItem lpi
10035
                INNER JOIN $tblLp lp
10036
                ON (lpi.lp_id = lp.iid)
10037
                WHERE
10038
                    lpi.item_type = '".TOOL_QUIZ."' AND
10039
                    lpi.path = '$exerciseId'";
10040
        $result = Database::query($sql);
10041
        $lpList = [];
10042
        if (Database::num_rows($result) > 0) {
10043
            $lpList = Database::store_result($result, 'ASSOC');
10044
        }
10045
10046
        return $lpList;
10047
    }
10048
10049
    public function getReminderTable($questionList, $exercise_stat_info, $disableCheckBoxes = false)
10050
    {
10051
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10052
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10053
        $learnpath_item_view_id = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10054
        $categoryId = isset($_REQUEST['category_id']) ? (int) $_REQUEST['category_id'] : 0;
10055
10056
        if (empty($exercise_stat_info)) {
10057
            return '';
10058
        }
10059
10060
        $remindList = $exercise_stat_info['questions_to_check'];
10061
        $remindList = explode(',', $remindList);
10062
10063
        $exeId = $exercise_stat_info['exe_id'];
10064
        $exerciseId = $exercise_stat_info['exe_exo_id'];
10065
        $exercise_result = $this->getUserAnswersSavedInExercise($exeId);
10066
10067
        $content = Display::label(get_lang('Questions without answer'), 'danger');
10068
        $content .= '<div class="clear"></div><br />';
10069
        $table = '';
10070
        $counter = 0;
10071
        // Loop over all question to show results for each of them, one by one
10072
        foreach ($questionList as $questionId) {
10073
            $objQuestionTmp = Question::read($questionId);
10074
            $check_id = 'remind_list['.$questionId.']';
10075
            $attributes = [
10076
                'id' => $check_id,
10077
                'onclick' => "save_remind_item(this, '$questionId');",
10078
                'data-question-id' => $questionId,
10079
            ];
10080
            if (in_array($questionId, $remindList)) {
10081
                $attributes['checked'] = 1;
10082
            }
10083
10084
            $checkbox = Display::input('checkbox', 'remind_list['.$questionId.']', '', $attributes);
10085
            $checkbox = '<div class="pretty p-svg p-curve">
10086
                        '.$checkbox.'
10087
                        <div class="state p-primary ">
10088
                         <svg class="svg svg-icon" viewBox="0 0 20 20">
10089
                            <path d="M7.629,14.566c0.125,0.125,0.291,0.188,0.456,0.188c0.164,0,0.329-0.062,0.456-0.188l8.219-8.221c0.252-0.252,0.252-0.659,0-0.911c-0.252-0.252-0.659-0.252-0.911,0l-7.764,7.763L4.152,9.267c-0.252-0.251-0.66-0.251-0.911,0c-0.252,0.252-0.252,0.66,0,0.911L7.629,14.566z" style="stroke: white;fill:white;"></path>
10090
                         </svg>
10091
                         <label>&nbsp;</label>
10092
                        </div>
10093
                    </div>';
10094
            $counter++;
10095
            $questionTitle = $counter.'. '.strip_tags($objQuestionTmp->selectTitle());
10096
            // Check if the question doesn't have an answer.
10097
            if (!in_array($questionId, $exercise_result)) {
10098
                $questionTitle = Display::label($questionTitle, 'danger');
10099
            }
10100
10101
            $label_attributes = [];
10102
            $label_attributes['for'] = $check_id;
10103
            if (false === $disableCheckBoxes) {
10104
                $questionTitle = Display::tag('label', $checkbox.$questionTitle, $label_attributes);
10105
            }
10106
            $table .= Display::div($questionTitle, ['class' => 'exercise_reminder_item ']);
10107
        }
10108
10109
        $content .= Display::div('', ['id' => 'message']).
10110
            Display::div($table, ['class' => 'question-check-test']);
10111
10112
        $content .= '<script>
10113
        var lp_data = $.param({
10114
            "learnpath_id": '.$learnpath_id.',
10115
            "learnpath_item_id" : '.$learnpath_item_id.',
10116
            "learnpath_item_view_id": '.$learnpath_item_view_id.'
10117
        });
10118
10119
        function final_submit() {
10120
            // Normal inputs.
10121
            window.location = "'.api_get_path(WEB_CODE_PATH).'exercise/exercise_result.php?'.api_get_cidreq().'&exe_id='.$exeId.'&" + lp_data;
10122
        }
10123
10124
        function selectAll() {
10125
            $("input[type=checkbox]").each(function () {
10126
                $(this).prop("checked", 1);
10127
                var question_id = $(this).data("question-id");
10128
                var action = "add";
10129
                $.ajax({
10130
                    url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10131
                    data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
10132
                    success: function(returnValue) {
10133
                    }
10134
                });
10135
            });
10136
        }
10137
10138
        function changeOptionStatus(status)
10139
        {
10140
            $("input[type=checkbox]").each(function () {
10141
                $(this).prop("checked", status);
10142
            });
10143
10144
            var action = "";
10145
            var option = "remove_all";
10146
            if (status == 1) {
10147
                option = "add_all";
10148
            }
10149
            $.ajax({
10150
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10151
                data: "option="+option+"&exe_id='.$exeId.'&action="+action,
10152
                success: function(returnValue) {
10153
                }
10154
            });
10155
        }
10156
10157
        function reviewQuestions() {
10158
            var isChecked = 1;
10159
            $("input[type=checkbox]").each(function () {
10160
                if ($(this).prop("checked")) {
10161
                    isChecked = 2;
10162
                    return false;
10163
                }
10164
            });
10165
10166
            if (isChecked == 1) {
10167
                $("#message").addClass("warning-message");
10168
                $("#message").html("'.addslashes(get_lang('Select a question to revise')).'");
10169
            } else {
10170
                window.location = "exercise_submit.php?'.api_get_cidreq().'&category_id='.$categoryId.'&exerciseId='.$exerciseId.'&reminder=2&" + lp_data;
10171
            }
10172
        }
10173
10174
        function save_remind_item(obj, question_id) {
10175
            var action = "";
10176
            if ($(obj).prop("checked")) {
10177
                action = "add";
10178
            } else {
10179
                action = "delete";
10180
            }
10181
            $.ajax({
10182
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10183
                data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
10184
                success: function(returnValue) {
10185
                }
10186
            });
10187
        }
10188
        </script>';
10189
10190
        return $content;
10191
    }
10192
10193
    public function getRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10194
    {
10195
        $dataSet = [];
10196
        $labels = [];
10197
        $labelsWithId = [];
10198
        /** @var Exercise $exercise */
10199
        foreach ($exercises as $exercise) {
10200
            if (empty($labels)) {
10201
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10202
                if (!empty($categoryNameList)) {
10203
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10204
                    asort($labelsWithId);
10205
                    $labels = array_values($labelsWithId);
10206
                }
10207
            }
10208
10209
            foreach ($userList as $userId) {
10210
                $results = Event::getExerciseResultsByUser(
10211
                    $userId,
10212
                    $exercise->iId,
10213
                    $courseId,
10214
                    $sessionId
10215
                );
10216
10217
                if ($results) {
10218
                    $firstAttempt = end($results);
10219
                    $exeId = $firstAttempt['exe_id'];
10220
10221
                    ob_start();
10222
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10223
                        $exercise,
10224
                        $exeId,
10225
                        false
10226
                    );
10227
                    ob_end_clean();
10228
10229
                    $categoryList = $stats['category_list'];
10230
                    $tempResult = [];
10231
                    foreach ($labelsWithId as $category_id => $title) {
10232
                        if (isset($categoryList[$category_id])) {
10233
                            $category_item = $categoryList[$category_id];
10234
                            $tempResult[] = round($category_item['score'] / $category_item['total'] * 10);
10235
                        } else {
10236
                            $tempResult[] = 0;
10237
                        }
10238
                    }
10239
                    $dataSet[] = $tempResult;
10240
                }
10241
            }
10242
        }
10243
10244
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10245
    }
10246
10247
    public function getAverageRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10248
    {
10249
        $dataSet = [];
10250
        $labels = [];
10251
        $labelsWithId = [];
10252
10253
        $tempResult = [];
10254
        /** @var Exercise $exercise */
10255
        foreach ($exercises as $exercise) {
10256
            $exerciseId = $exercise->iId;
10257
            if (empty($labels)) {
10258
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10259
                if (!empty($categoryNameList)) {
10260
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10261
                    asort($labelsWithId);
10262
                    $labels = array_values($labelsWithId);
10263
                }
10264
            }
10265
10266
            foreach ($userList as $userId) {
10267
                $results = Event::getExerciseResultsByUser(
10268
                    $userId,
10269
                    $exerciseId,
10270
                    $courseId,
10271
                    $sessionId
10272
                );
10273
10274
                if ($results) {
10275
                    $firstAttempt = end($results);
10276
                    $exeId = $firstAttempt['exe_id'];
10277
10278
                    ob_start();
10279
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10280
                        $exercise,
10281
                        $exeId,
10282
                        false
10283
                    );
10284
                    ob_end_clean();
10285
10286
                    $categoryList = $stats['category_list'];
10287
                    foreach ($labelsWithId as $category_id => $title) {
10288
                        if (isset($categoryList[$category_id])) {
10289
                            $category_item = $categoryList[$category_id];
10290
                            if (!isset($tempResult[$exerciseId][$category_id])) {
10291
                                $tempResult[$exerciseId][$category_id] = 0;
10292
                            }
10293
                            $tempResult[$exerciseId][$category_id] += $category_item['score'] / $category_item['total'] * 10;
10294
                        }
10295
                    }
10296
                }
10297
            }
10298
        }
10299
10300
        $totalUsers = count($userList);
10301
10302
        foreach ($exercises as $exercise) {
10303
            $exerciseId = $exercise->iId;
10304
            $data = [];
10305
            foreach ($labelsWithId as $category_id => $title) {
10306
                if (isset($tempResult[$exerciseId]) && isset($tempResult[$exerciseId][$category_id])) {
10307
                    $data[] = round($tempResult[$exerciseId][$category_id] / $totalUsers);
10308
                } else {
10309
                    $data[] = 0;
10310
                }
10311
            }
10312
            $dataSet[] = $data;
10313
        }
10314
10315
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10316
    }
10317
10318
    public function getRadar($labels, $dataSet, $dataSetLabels = [])
10319
    {
10320
        if (empty($labels) || empty($dataSet)) {
10321
            return '';
10322
        }
10323
10324
        $displayLegend = 0;
10325
        if (!empty($dataSetLabels)) {
10326
            $displayLegend = 1;
10327
        }
10328
10329
        $labels = json_encode($labels);
10330
10331
        $colorList = ChamiloHelper::getColorPalette(true, true);
10332
10333
        $dataSetToJson = [];
10334
        $counter = 0;
10335
        foreach ($dataSet as $index => $resultsArray) {
10336
            $color = isset($colorList[$counter]) ? $colorList[$counter] : 'rgb('.rand(0, 255).', '.rand(0, 255).', '.rand(0, 255).', 1.0)';
10337
10338
            $label = isset($dataSetLabels[$index]) ? $dataSetLabels[$index] : '';
10339
            $background = str_replace('1.0', '0.2', $color);
10340
            $dataSetToJson[] = [
10341
                'fill' => false,
10342
                'label' => $label,
10343
                'backgroundColor' => $background,
10344
                'borderColor' => $color,
10345
                'pointBackgroundColor' => $color,
10346
                'pointBorderColor' => '#fff',
10347
                'pointHoverBackgroundColor' => '#fff',
10348
                'pointHoverBorderColor' => $color,
10349
                'pointRadius' => 6,
10350
                'pointBorderWidth' => 3,
10351
                'pointHoverRadius' => 10,
10352
                'data' => $resultsArray,
10353
            ];
10354
            $counter++;
10355
        }
10356
        $resultsToJson = json_encode($dataSetToJson);
10357
10358
        return "
10359
                <canvas id='categoryRadar' height='200'></canvas>
10360
                <script>
10361
                    var data = {
10362
                        labels: $labels,
10363
                        datasets: $resultsToJson
10364
                    }
10365
                    var options = {
10366
                        responsive: true,
10367
                        scale: {
10368
                            angleLines: {
10369
                                display: false
10370
                            },
10371
                            ticks: {
10372
                                beginAtZero: true,
10373
                                  min: 0,
10374
                                  max: 10,
10375
                                stepSize: 1,
10376
                            },
10377
                            pointLabels: {
10378
                              fontSize: 14,
10379
                              //fontStyle: 'bold'
10380
                            },
10381
                        },
10382
                        elements: {
10383
                            line: {
10384
                                tension: 0,
10385
                                borderWidth: 3
10386
                            }
10387
                        },
10388
                        legend: {
10389
                            //position: 'bottom'
10390
                            display: $displayLegend
10391
                        },
10392
                        animation: {
10393
                            animateScale: true,
10394
                            animateRotate: true
10395
                        },
10396
                    };
10397
                    var ctx = document.getElementById('categoryRadar').getContext('2d');
10398
                    var myRadarChart = new Chart(ctx, {
10399
                        type: 'radar',
10400
                        data: data,
10401
                        options: options
10402
                    });
10403
                </script>
10404
                ";
10405
    }
10406
10407
    /**
10408
     * Returns true if the exercise is locked by percentage. an exercise attempt must be passed.
10409
     */
10410
    public function isBlockedByPercentage(array $attempt = []): bool
10411
    {
10412
        if (empty($attempt)) {
10413
            return false;
10414
        }
10415
        $extraFieldValue = new ExtraFieldValue('exercise');
10416
        $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
10417
            $this->iId,
10418
            'blocking_percentage'
10419
        );
10420
10421
        if (empty($blockExercise['value'])) {
10422
            return false;
10423
        }
10424
10425
        $blockPercentage = (int) $blockExercise['value'];
10426
10427
        if (0 === $blockPercentage) {
10428
            return false;
10429
        }
10430
10431
        $resultPercentage = 0;
10432
10433
        if (isset($attempt['score']) && isset($attempt['max_score'])) {
10434
            $weight = (int) $attempt['max_score'];
10435
            $weight = (0 == $weight) ? 1 : $weight;
10436
            $resultPercentage = float_format(
10437
                ($attempt['score'] / $weight) * 100,
10438
                1
10439
            );
10440
        }
10441
        if ($resultPercentage <= $blockPercentage) {
10442
            return true;
10443
        }
10444
10445
        return false;
10446
    }
10447
10448
    /**
10449
     * Gets the question list ordered by the question_order setting (drag and drop).
10450
     *
10451
     * @param bool $adminView Optional.
10452
     *
10453
     * @return array
10454
     */
10455
    public function getQuestionOrderedList($adminView = false)
10456
    {
10457
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
10458
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
10459
10460
        // Getting question_order to verify that the question
10461
        // list is correct and all question_order's were set
10462
        $sql = "SELECT DISTINCT count(e.question_order) as count
10463
                FROM $TBL_EXERCICE_QUESTION e
10464
                INNER JOIN $TBL_QUESTIONS q
10465
                ON (e.question_id = q.iid)
10466
                WHERE
10467
                  e.quiz_id	= ".$this->getId();
10468
10469
        $result = Database::query($sql);
10470
        $row = Database::fetch_array($result);
10471
        $count_question_orders = $row['count'];
10472
10473
        // Getting question list from the order (question list drag n drop interface).
10474
        $sql = "SELECT DISTINCT e.question_id, e.question_order
10475
                FROM $TBL_EXERCICE_QUESTION e
10476
                INNER JOIN $TBL_QUESTIONS q
10477
                ON (e.question_id = q.iid)
10478
                WHERE
10479
10480
                    e.quiz_id = '".$this->getId()."'
10481
                ORDER BY question_order";
10482
        $result = Database::query($sql);
10483
10484
        // Fills the array with the question ID for this exercise
10485
        // the key of the array is the question position
10486
        $temp_question_list = [];
10487
        $counter = 1;
10488
        $questionList = [];
10489
        while ($new_object = Database::fetch_object($result)) {
10490
            if (!$adminView) {
10491
                // Correct order.
10492
                $questionList[$new_object->question_order] = $new_object->question_id;
10493
            } else {
10494
                $questionList[$counter] = $new_object->question_id;
10495
            }
10496
10497
            // Just in case we save the order in other array
10498
            $temp_question_list[$counter] = $new_object->question_id;
10499
            $counter++;
10500
        }
10501
10502
        if (!empty($temp_question_list)) {
10503
            /* If both array don't match it means that question_order was not correctly set
10504
               for all questions using the default mysql order */
10505
            if (count($temp_question_list) != $count_question_orders) {
10506
                $questionList = $temp_question_list;
10507
            }
10508
        }
10509
10510
        return $questionList;
10511
    }
10512
10513
    /**
10514
     * Get number of questions in exercise by user attempt.
10515
     *
10516
     * @return int
10517
     */
10518
    private function countQuestionsInExercise()
10519
    {
10520
        $lpId = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10521
        $lpItemId = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10522
        $lpItemViewId = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10523
10524
        $trackInfo = $this->get_stat_track_exercise_info($lpId, $lpItemId, $lpItemViewId);
10525
10526
        if (!empty($trackInfo)) {
10527
            $questionIds = explode(',', $trackInfo['data_tracking']);
10528
10529
            return count($questionIds);
10530
        }
10531
10532
        return $this->getQuestionCount();
10533
    }
10534
10535
    /**
10536
     * Select N values from the questions per category array.
10537
     *
10538
     * @param array $categoriesAddedInExercise
10539
     * @param array $question_list
10540
     * @param array $questions_by_category
10541
     * @param bool  $flatResult
10542
     * @param bool  $randomizeQuestions
10543
     * @param array $questionsByCategoryMandatory
10544
     *
10545
     * @return array
10546
     */
10547
    private function pickQuestionsPerCategory(
10548
        $categoriesAddedInExercise,
10549
        $question_list,
10550
        &$questions_by_category,
10551
        $flatResult = true,
10552
        $randomizeQuestions = false,
10553
        $questionsByCategoryMandatory = []
10554
    ) {
10555
        $addAll = true;
10556
        $categoryCountArray = [];
10557
10558
        // Getting how many questions will be selected per category.
10559
        if (!empty($categoriesAddedInExercise)) {
10560
            $addAll = false;
10561
            // Parsing question according the category rel exercise settings
10562
            foreach ($categoriesAddedInExercise as $category_info) {
10563
                $category_id = $category_info['category_id'];
10564
                if (isset($questions_by_category[$category_id])) {
10565
                    // How many question will be picked from this category.
10566
                    $count = $category_info['count_questions'];
10567
                    // -1 means all questions
10568
                    $categoryCountArray[$category_id] = $count;
10569
                    if (-1 == $count) {
10570
                        $categoryCountArray[$category_id] = 999;
10571
                    }
10572
                }
10573
            }
10574
        }
10575
10576
        if (!empty($questions_by_category)) {
10577
            $temp_question_list = [];
10578
            foreach ($questions_by_category as $category_id => &$categoryQuestionList) {
10579
                if (isset($categoryCountArray) && !empty($categoryCountArray)) {
10580
                    $numberOfQuestions = 0;
10581
                    if (isset($categoryCountArray[$category_id])) {
10582
                        $numberOfQuestions = $categoryCountArray[$category_id];
10583
                    }
10584
                }
10585
10586
                if ($addAll) {
10587
                    $numberOfQuestions = 999;
10588
                }
10589
                if (!empty($numberOfQuestions)) {
10590
                    $mandatoryQuestions = [];
10591
                    if (isset($questionsByCategoryMandatory[$category_id])) {
10592
                        $mandatoryQuestions = $questionsByCategoryMandatory[$category_id];
10593
                    }
10594
10595
                    $elements = TestCategory::getNElementsFromArray(
10596
                        $categoryQuestionList,
10597
                        $numberOfQuestions,
10598
                        $randomizeQuestions,
10599
                        $mandatoryQuestions
10600
                    );
10601
10602
                    if (!empty($elements)) {
10603
                        $temp_question_list[$category_id] = $elements;
10604
                        $categoryQuestionList = $elements;
10605
                    }
10606
                }
10607
            }
10608
10609
            if (!empty($temp_question_list)) {
10610
                if ($flatResult) {
10611
                    $temp_question_list = array_flatten($temp_question_list);
10612
                }
10613
                $question_list = $temp_question_list;
10614
            }
10615
        }
10616
10617
        return $question_list;
10618
    }
10619
10620
    /**
10621
     * Sends a notification when a user ends an examn.
10622
     *
10623
     * @param array  $question_list_answers
10624
     * @param string $origin
10625
     * @param array  $user_info
10626
     * @param string $url_email
10627
     * @param array  $teachers
10628
     */
10629
    private function sendNotificationForOpenQuestions(
10630
        $question_list_answers,
10631
        $origin,
10632
        $user_info,
10633
        $url_email,
10634
        $teachers
10635
    ) {
10636
        // Email configuration settings
10637
        $courseCode = api_get_course_id();
10638
        $courseInfo = api_get_course_info($courseCode);
10639
        $sessionId = api_get_session_id();
10640
        $sessionData = '';
10641
        if (!empty($sessionId)) {
10642
            $sessionInfo = api_get_session_info($sessionId);
10643
            if (!empty($sessionInfo)) {
10644
                $sessionData = '<tr>'
10645
                    .'<td><em>'.get_lang('Session name').'</em></td>'
10646
                    .'<td>&nbsp;<b>'.$sessionInfo['name'].'</b></td>'
10647
                    .'</tr>';
10648
            }
10649
        }
10650
10651
        $msg = get_lang('A learner has answered an open question').'<br /><br />'
10652
            .get_lang('Attempt details').' : <br /><br />'
10653
            .'<table>'
10654
            .'<tr>'
10655
            .'<td><em>'.get_lang('Course name').'</em></td>'
10656
            .'<td>&nbsp;<b>#course#</b></td>'
10657
            .'</tr>'
10658
            .$sessionData
10659
            .'<tr>'
10660
            .'<td>'.get_lang('Test attempted').'</td>'
10661
            .'<td>&nbsp;#exercise#</td>'
10662
            .'</tr>'
10663
            .'<tr>'
10664
            .'<td>'.get_lang('Learner name').'</td>'
10665
            .'<td>&nbsp;#firstName# #lastName#</td>'
10666
            .'</tr>'
10667
            .'<tr>'
10668
            .'<td>'.get_lang('Learner e-mail').'</td>'
10669
            .'<td>&nbsp;#mail#</td>'
10670
            .'</tr>'
10671
            .'</table>';
10672
10673
        $open_question_list = null;
10674
        foreach ($question_list_answers as $item) {
10675
            $question = $item['question'];
10676
            $answer = $item['answer'];
10677
            $answer_type = $item['answer_type'];
10678
10679
            if (!empty($question) && !empty($answer) && FREE_ANSWER == $answer_type) {
10680
                $open_question_list .=
10681
                    '<tr>
10682
                    <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
10683
                    <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
10684
                    </tr>
10685
                    <tr>
10686
                    <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
10687
                    <td valign="top" bgcolor="#F3F3F3">'.$answer.'</td>
10688
                    </tr>';
10689
            }
10690
        }
10691
10692
        if (!empty($open_question_list)) {
10693
            $msg .= '<p><br />'.get_lang('A learner has answered an open question').' :</p>'.
10694
                '<table width="730" height="136" border="0" cellpadding="3" cellspacing="3">';
10695
            $msg .= $open_question_list;
10696
            $msg .= '</table><br />';
10697
10698
            $msg = str_replace('#exercise#', $this->exercise, $msg);
10699
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg);
10700
            $msg = str_replace('#lastName#', $user_info['lastname'], $msg);
10701
            $msg = str_replace('#mail#', $user_info['email'], $msg);
10702
            $msg = str_replace(
10703
                '#course#',
10704
                Display::url($courseInfo['title'], $courseInfo['course_public_url'].'?sid='.$sessionId),
10705
                $msg
10706
            );
10707
10708
            if ('learnpath' !== $origin) {
10709
                $msg .= '<br /><a href="#url#">'.get_lang(
10710
                        'Click this link to check the answer and/or give feedback'
10711
                    ).'</a>';
10712
            }
10713
            $msg = str_replace('#url#', $url_email, $msg);
10714
            $subject = get_lang('A learner has answered an open question');
10715
10716
            if (!empty($teachers)) {
10717
                foreach ($teachers as $user_id => $teacher_data) {
10718
                    MessageManager::send_message_simple(
10719
                        $user_id,
10720
                        $subject,
10721
                        $msg
10722
                    );
10723
                }
10724
            }
10725
        }
10726
    }
10727
10728
    /**
10729
     * Send notification for oral questions.
10730
     *
10731
     * @param array  $question_list_answers
10732
     * @param string $origin
10733
     * @param int    $exe_id
10734
     * @param array  $user_info
10735
     * @param string $url_email
10736
     * @param array  $teachers
10737
     */
10738
    private function sendNotificationForOralQuestions(
10739
        $question_list_answers,
10740
        $origin,
10741
        $exe_id,
10742
        $user_info,
10743
        $url_email,
10744
        $teachers
10745
    ): void {
10746
10747
        // Email configuration settings
10748
        $courseCode = api_get_course_id();
10749
        $courseInfo = api_get_course_info($courseCode);
10750
        $oral_question_list = null;
10751
        foreach ($question_list_answers as $item) {
10752
            $question = $item['question'];
10753
            $file = $item['generated_oral_file'];
10754
            $answer = $item['answer'];
10755
            if (0 == $answer) {
10756
                $answer = '';
10757
            }
10758
            $answer_type = $item['answer_type'];
10759
            if (!empty($question) && (!empty($answer) || !empty($file)) && ORAL_EXPRESSION == $answer_type) {
10760
                $oral_question_list .= '<br />
10761
                    <table width="730" height="136" border="0" cellpadding="3" cellspacing="3">
10762
                    <tr>
10763
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
10764
                        <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
10765
                    </tr>
10766
                    <tr>
10767
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
10768
                        <td valign="top" bgcolor="#F3F3F3"><p>'.$answer.'</p><p>'.$file.'</p></td>
10769
                    </tr></table>';
10770
            }
10771
        }
10772
10773
        if (!empty($oral_question_list)) {
10774
            $msg = get_lang('A learner has attempted one or more oral question').'<br /><br />
10775
                    '.get_lang('Attempt details').' : <br /><br />
10776
                    <table>
10777
                        <tr>
10778
                            <td><em>'.get_lang('Course name').'</em></td>
10779
                            <td>&nbsp;<b>#course#</b></td>
10780
                        </tr>
10781
                        <tr>
10782
                            <td>'.get_lang('Test attempted').'</td>
10783
                            <td>&nbsp;#exercise#</td>
10784
                        </tr>
10785
                        <tr>
10786
                            <td>'.get_lang('Learner name').'</td>
10787
                            <td>&nbsp;#firstName# #lastName#</td>
10788
                        </tr>
10789
                        <tr>
10790
                            <td>'.get_lang('Learner e-mail').'</td>
10791
                            <td>&nbsp;#mail#</td>
10792
                        </tr>
10793
                    </table>';
10794
            $msg .= '<br />'.sprintf(
10795
                    get_lang('The attempted oral questions are %s'),
10796
                    $oral_question_list
10797
                ).'<br />';
10798
            $msg1 = str_replace('#exercise#', $this->exercise, $msg);
10799
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg1);
10800
            $msg1 = str_replace('#lastName#', $user_info['lastname'], $msg);
10801
            $msg = str_replace('#mail#', $user_info['email'], $msg1);
10802
            $msg1 = str_replace('#course#', $courseInfo['name'], $msg);
10803
10804
            if (!in_array($origin, ['learnpath', 'embeddable'])) {
10805
                $msg1 .= '<br /><a href="#url#">'.get_lang(
10806
                        'Click this link to check the answer and/or give feedback'
10807
                    ).'</a>';
10808
            }
10809
            $msg = str_replace('#url#', $url_email, $msg1);
10810
            $mail_content = $msg;
10811
            $subject = get_lang('A learner has attempted one or more oral question');
10812
10813
            if (!empty($teachers)) {
10814
                foreach ($teachers as $user_id => $teacher_data) {
10815
                    MessageManager::send_message_simple(
10816
                        $user_id,
10817
                        $subject,
10818
                        $mail_content
10819
                    );
10820
                }
10821
            }
10822
        }
10823
    }
10824
10825
    /**
10826
     * Returns an array with the media list.
10827
     *
10828
     * @param array $questionList question list
10829
     *
10830
     * @example there's 1 question with iid 5 that belongs to the media question with iid = 100
10831
     * <code>
10832
     * array (size=2)
10833
     *  999 =>
10834
     *    array (size=3)
10835
     *      0 => int 7
10836
     *      1 => int 6
10837
     *      2 => int 3254
10838
     *  100 =>
10839
     *   array (size=1)
10840
     *      0 => int 5
10841
     *  </code>
10842
     */
10843
    private function setMediaList($questionList)
10844
    {
10845
        $mediaList = [];
10846
        /*
10847
         * Media feature is not activated in 1.11.x
10848
        if (!empty($questionList)) {
10849
            foreach ($questionList as $questionId) {
10850
                $objQuestionTmp = Question::read($questionId, $this->course_id);
10851
                // If a media question exists
10852
                if (isset($objQuestionTmp->parent_id) && $objQuestionTmp->parent_id != 0) {
10853
                    $mediaList[$objQuestionTmp->parent_id][] = $objQuestionTmp->id;
10854
                } else {
10855
                    // Always the last item
10856
                    $mediaList[999][] = $objQuestionTmp->id;
10857
                }
10858
            }
10859
        }*/
10860
10861
        $this->mediaList = $mediaList;
10862
    }
10863
10864
    /**
10865
     * @return HTML_QuickForm_group
10866
     */
10867
    private function setResultDisabledGroup(FormValidator $form)
10868
    {
10869
        $resultDisabledGroup = [];
10870
10871
        $resultDisabledGroup[] = $form->createElement(
10872
            'radio',
10873
            'results_disabled',
10874
            null,
10875
            get_lang('Auto-evaluation mode: show score and expected answers'),
10876
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
10877
            ['id' => 'result_disabled_0']
10878
        );
10879
10880
        $warning = sprintf(
10881
            get_lang("The setting \"%s\" will change to \"%s\""),
10882
            get_lang('Feedback'),
10883
            get_lang('Exam (no feedback)')
10884
        );
10885
        $resultDisabledGroup[] = $form->createElement(
10886
            'radio',
10887
            'results_disabled',
10888
            null,
10889
            get_lang('Exam mode: Do not show score nor answers'),
10890
            RESULT_DISABLE_NO_SCORE_AND_EXPECTED_ANSWERS,
10891
            [
10892
                'id' => 'result_disabled_1',
10893
                //'onclick' => 'check_results_disabled()'
10894
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
10895
            ]
10896
        );
10897
10898
        $resultDisabledGroup[] = $form->createElement(
10899
            'radio',
10900
            'results_disabled',
10901
            null,
10902
            get_lang('Practice mode: Show score only, by category if at least one is used'),
10903
            RESULT_DISABLE_SHOW_SCORE_ONLY,
10904
            [
10905
                'id' => 'result_disabled_2',
10906
                //'onclick' => 'check_results_disabled()'
10907
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
10908
            ]
10909
        );
10910
10911
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
10912
            return $form->addGroup(
10913
                $resultDisabledGroup,
10914
                null,
10915
                get_lang(
10916
                    'Show score to learner'
10917
                )
10918
            );
10919
        }
10920
10921
        $resultDisabledGroup[] = $form->createElement(
10922
            'radio',
10923
            'results_disabled',
10924
            null,
10925
            get_lang(
10926
                'Show score on every attempt, show correct answers only on last attempt (only works with an attempts limit)'
10927
            ),
10928
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
10929
            ['id' => 'result_disabled_4']
10930
        );
10931
10932
        $resultDisabledGroup[] = $form->createElement(
10933
            'radio',
10934
            'results_disabled',
10935
            null,
10936
            get_lang(
10937
                'Do not show the score (only when user finishes all attempts) but show feedback for each attempt.'
10938
            ),
10939
            RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
10940
            [
10941
                'id' => 'result_disabled_5',
10942
                //'onclick' => 'check_results_disabled()'
10943
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
10944
            ]
10945
        );
10946
10947
        $resultDisabledGroup[] = $form->createElement(
10948
            'radio',
10949
            'results_disabled',
10950
            null,
10951
            get_lang(
10952
                'Ranking mode: Do not show results details question by question and show a table with the ranking of all other users.'
10953
            ),
10954
            RESULT_DISABLE_RANKING,
10955
            ['id' => 'result_disabled_6']
10956
        );
10957
10958
        $resultDisabledGroup[] = $form->createElement(
10959
            'radio',
10960
            'results_disabled',
10961
            null,
10962
            get_lang(
10963
                'Show only global score (not question score) and show only the correct answers, do not show incorrect answers at all'
10964
            ),
10965
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
10966
            ['id' => 'result_disabled_7']
10967
        );
10968
10969
        $resultDisabledGroup[] = $form->createElement(
10970
            'radio',
10971
            'results_disabled',
10972
            null,
10973
            get_lang('Auto-evaluation mode and ranking'),
10974
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
10975
            ['id' => 'result_disabled_8']
10976
        );
10977
10978
        $resultDisabledGroup[] = $form->createElement(
10979
            'radio',
10980
            'results_disabled',
10981
            null,
10982
            get_lang('Show score by category on a radar/spiderweb chart'),
10983
            RESULT_DISABLE_RADAR,
10984
            ['id' => 'result_disabled_9']
10985
        );
10986
10987
        $resultDisabledGroup[] = $form->createElement(
10988
            'radio',
10989
            'results_disabled',
10990
            null,
10991
            get_lang('Show the result to the learner: Show the score, the learner\'s choice and his feedback on each attempt, add the correct answer and his feedback when the chosen limit of attempts is reached.'),
10992
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
10993
            ['id' => 'result_disabled_10']
10994
        );
10995
10996
        return $form->addGroup(
10997
            $resultDisabledGroup,
10998
            null,
10999
            get_lang('Show score to learner')
11000
        );
11001
    }
11002
11003
    /**
11004
     * Return the text to display, based on the score and the max score.
11005
     * @param int|float $score
11006
     * @param int|float $maxScore
11007
     * @return string
11008
     */
11009
    public function getFinishText(int|float $score, int|float $maxScore): string
11010
    {
11011
        $passPercentage = $this->selectPassPercentage();
11012
        if (!empty($passPercentage)) {
11013
            $percentage = float_format(
11014
                ($score / (0 != $maxScore ? $maxScore : 1)) * 100,
11015
                1
11016
            );
11017
            if ($percentage >= $passPercentage) {
11018
                return $this->getTextWhenFinished();
11019
            } else {
11020
                return $this->getTextWhenFinishedFailure();
11021
            }
11022
        } else {
11023
            return $this->getTextWhenFinished();
11024
        }
11025
11026
        return '';
0 ignored issues
show
Unused Code introduced by
return '' is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
11027
    }
11028
}
11029