Issues (1868)

public/main/exercise/exercise.class.php (10 issues)

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('RandomCategoriesWithQuestionsOrderedNoQuestionGrouped'),
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('RandomCategoriesWithRandomQuestionsNoQuestionGrouped'),
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('OrderedCategoriesByParentWithQuestionsOrdered'),
2084
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM => get_lang('OrderedCategoriesByParentWithQuestionsRandom'),
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 and feedback 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('Random questionsHelp'),
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. 20 characters, e.g. <i>INNOV21</i> 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('Numeric'), '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('Numeric'), '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('TheSettingXWillChangeToX'),
2541
            get_lang('ShowResultsToStudents'),
2542
            get_lang('ShowScoreAndRightAnswer')
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_map('intval', $questionList);
3147
        $em = Database::getManager();
3148
3149
        $quiz = $em->find(CQuiz::class, $this->getId());
3150
3151
        $trackExercise = (new TrackEExercise())
3152
            ->setSession(api_get_session_entity())
3153
            ->setCourse(api_get_course_entity())
3154
            ->setMaxScore($weight)
3155
            ->setDataTracking(implode(',', $questionList))
3156
            ->setUser(api_get_user_entity())
3157
            ->setUserIp(api_get_real_ip())
3158
            ->setOrigLpId($safe_lp_id)
3159
            ->setOrigLpItemId($safe_lp_item_id)
3160
            ->setOrigLpItemViewId($safe_lp_item_view_id)
3161
            ->setExpiredTimeControl($clock_expired_time)
3162
            ->setQuiz($quiz)
3163
        ;
3164
        $em->persist($trackExercise);
3165
        $em->flush();
3166
3167
        return $trackExercise->getExeId();
3168
    }
3169
3170
    /**
3171
     * @param int    $question_id
3172
     * @param int    $questionNum
3173
     * @param array  $questions_in_media
3174
     * @param string $currentAnswer
3175
     * @param array  $myRemindList
3176
     * @param bool   $showPreviousButton
3177
     *
3178
     * @return string
3179
     */
3180
    public function show_button(
3181
        $question_id,
3182
        $questionNum,
3183
        $questions_in_media = [],
3184
        $currentAnswer = '',
3185
        $myRemindList = [],
3186
        $showPreviousButton = true
3187
    ) {
3188
        global $safe_lp_id, $safe_lp_item_id, $safe_lp_item_view_id;
3189
        $nbrQuestions = $this->countQuestionsInExercise();
3190
        $buttonList = [];
3191
        $html = $label = '';
3192
        $hotspotGet = isset($_POST['hotspot']) ? Security::remove_XSS($_POST['hotspot']) : null;
3193
3194
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]) &&
3195
            ONE_PER_PAGE == $this->type
3196
        ) {
3197
            $urlTitle = get_lang('Proceed with the test');
3198
            if ($questionNum == count($this->questionList)) {
3199
                $urlTitle = get_lang('End test');
3200
            }
3201
3202
            $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit_modal.php?'.api_get_cidreq();
3203
            $url .= '&'.http_build_query(
3204
                    [
3205
                        'learnpath_id' => $safe_lp_id,
3206
                        'learnpath_item_id' => $safe_lp_item_id,
3207
                        'learnpath_item_view_id' => $safe_lp_item_view_id,
3208
                        'hotspot' => $hotspotGet,
3209
                        'nbrQuestions' => $nbrQuestions,
3210
                        'num' => $questionNum,
3211
                        'exerciseType' => $this->type,
3212
                        'exerciseId' => $this->getId(),
3213
                        'reminder' => empty($myRemindList) ? null : 2,
3214
                        'tryagain' => isset($_REQUEST['tryagain']) && 1 === (int) $_REQUEST['tryagain'] ? 1 : 0,
3215
                    ]
3216
                );
3217
3218
            $params = [
3219
                'class' => 'ajax btn btn--plain no-close-button',
3220
                'data-title' => Security::remove_XSS(get_lang('Comment')),
3221
                'data-size' => 'md',
3222
                'id' => "button_$question_id",
3223
            ];
3224
3225
            if (EXERCISE_FEEDBACK_TYPE_POPUP === $this->getFeedbackType()) {
3226
                //$params['data-block-div-after-closing'] = "question_div_$question_id";
3227
                $params['data-block-closing'] = 'true';
3228
                $params['class'] .= ' no-header ';
3229
            }
3230
3231
            $html .= Display::url($urlTitle, $url, $params);
3232
            $html .= '<br />';
3233
3234
            return $html;
3235
        }
3236
3237
        if (!api_is_allowed_to_session_edit()) {
3238
            return '';
3239
        }
3240
3241
        $isReviewingAnswers = isset($_REQUEST['reminder']) && 2 == $_REQUEST['reminder'];
3242
3243
        // User
3244
        $endReminderValue = false;
3245
        if (!empty($myRemindList) && $isReviewingAnswers) {
3246
            $endValue = end($myRemindList);
3247
            if ($endValue == $question_id) {
3248
                $endReminderValue = true;
3249
            }
3250
        }
3251
        $endTest = false;
3252
        if (ALL_ON_ONE_PAGE == $this->type || $nbrQuestions == $questionNum || $endReminderValue) {
3253
            if ($this->review_answers) {
3254
                $label = get_lang('ReviewQuestions');
3255
                $class = 'btn btn--success';
3256
            } else {
3257
                $endTest = true;
3258
                $label = get_lang('End Test');
3259
                $class = 'btn btn--warning';
3260
            }
3261
        } else {
3262
            $label = get_lang('Next question');
3263
            $class = 'btn btn--primary';
3264
        }
3265
        // used to select it with jquery
3266
        $class .= ' question-validate-btn';
3267
        if (ONE_PER_PAGE == $this->type) {
3268
            if (1 != $questionNum && $this->showPreviousButton()) {
3269
                $prev_question = $questionNum - 2;
3270
                $showPreview = true;
3271
                if (!empty($myRemindList) && $isReviewingAnswers) {
3272
                    $beforeId = null;
3273
                    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...
3274
                        if (isset($myRemindList[$i]) && $myRemindList[$i] == $question_id) {
3275
                            $beforeId = isset($myRemindList[$i - 1]) ? $myRemindList[$i - 1] : null;
3276
3277
                            break;
3278
                        }
3279
                    }
3280
3281
                    if (empty($beforeId)) {
3282
                        $showPreview = false;
3283
                    } else {
3284
                        $num = 0;
3285
                        foreach ($this->questionList as $originalQuestionId) {
3286
                            if ($originalQuestionId == $beforeId) {
3287
                                break;
3288
                            }
3289
                            $num++;
3290
                        }
3291
                        $prev_question = $num;
3292
                    }
3293
                }
3294
3295
                if ($showPreviousButton && $showPreview && 0 === $this->getPreventBackwards()) {
3296
                    $buttonList[] = Display::button(
3297
                        'previous_question_and_save',
3298
                        get_lang('Previous question'),
3299
                        [
3300
                            'type' => 'button',
3301
                            'class' => 'btn btn--plain',
3302
                            'data-prev' => $prev_question,
3303
                            'data-question' => $question_id,
3304
                        ]
3305
                    );
3306
                }
3307
            }
3308
3309
            // Next question
3310
            if (!empty($questions_in_media)) {
3311
                $buttonList[] = Display::button(
3312
                    'save_question_list',
3313
                    $label,
3314
                    [
3315
                        'type' => 'button',
3316
                        'class' => $class,
3317
                        'data-list' => implode(',', $questions_in_media),
3318
                    ]
3319
                );
3320
            } else {
3321
                $attributes = ['type' => 'button', 'class' => $class, 'data-question' => $question_id];
3322
                $name = 'save_now';
3323
                if ($endTest && api_get_configuration_value('quiz_check_all_answers_before_end_test')) {
3324
                    $name = 'check_answers';
3325
                }
3326
                $buttonList[] = Display::button(
3327
                    $name,
3328
                    $label,
3329
                    $attributes
3330
                );
3331
            }
3332
            $buttonList[] = '<span id="save_for_now_'.$question_id.'" class="exercise_save_mini_message"></span>';
3333
3334
            $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3335
3336
            return $html;
3337
        }
3338
3339
        if ($this->review_answers) {
3340
            $all_label = get_lang('Review selected questions');
3341
            $class = 'btn btn--success';
3342
        } else {
3343
            $all_label = get_lang('End test');
3344
            $class = 'btn btn--warning';
3345
        }
3346
        // used to select it with jquery
3347
        $class .= ' question-validate-btn';
3348
        $buttonList[] = Display::button(
3349
            'validate_all',
3350
            $all_label,
3351
            ['type' => 'button', 'class' => $class]
3352
        );
3353
        $buttonList[] = Display::span(null, ['id' => 'save_all_response']);
3354
        $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3355
3356
        return $html;
3357
    }
3358
3359
    /**
3360
     * @param int    $timeLeft in seconds
3361
     * @param string $url
3362
     *
3363
     * @return string
3364
     */
3365
    public function showSimpleTimeControl($timeLeft, $url = '')
3366
    {
3367
        $timeLeft = (int) $timeLeft;
3368
3369
        return "<script>
3370
            function openClockWarning() {
3371
                $('#clock_warning').dialog({
3372
                    modal:true,
3373
                    height:320,
3374
                    width:550,
3375
                    closeOnEscape: false,
3376
                    resizable: false,
3377
                    buttons: {
3378
                        '".addslashes(get_lang('Close'))."': function() {
3379
                            $('#clock_warning').dialog('close');
3380
                        }
3381
                    },
3382
                    close: function() {
3383
                        window.location.href = '$url';
3384
                    }
3385
                });
3386
                $('#clock_warning').dialog('open');
3387
                $('#counter_to_redirect').epiclock({
3388
                    mode: $.epiclock.modes.countdown,
3389
                    offset: {seconds: 5},
3390
                    format: 's'
3391
                }).bind('timer', function () {
3392
                    window.location.href = '$url';
3393
                });
3394
            }
3395
3396
            function onExpiredTimeExercise() {
3397
                $('#wrapper-clock').hide();
3398
                $('#expired-message-id').show();
3399
                // Fixes bug #5263
3400
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3401
                openClockWarning();
3402
            }
3403
3404
			$(function() {
3405
				// time in seconds when using minutes there are some seconds lost
3406
                var time_left = parseInt(".$timeLeft.");
3407
                $('#exercise_clock_warning').epiclock({
3408
                    mode: $.epiclock.modes.countdown,
3409
                    offset: {seconds: time_left},
3410
                    format: 'x:i:s',
3411
                    renderer: 'minute'
3412
                }).bind('timer', function () {
3413
                    onExpiredTimeExercise();
3414
                });
3415
	       		$('#submit_save').click(function () {});
3416
	        });
3417
	    </script>";
3418
    }
3419
3420
    /**
3421
     * So the time control will work.
3422
     *
3423
     * @param int    $timeLeft
3424
     * @param string $redirectToUrl
3425
     *
3426
     * @return string
3427
     */
3428
    public function showTimeControlJS($timeLeft, $redirectToUrl = '')
3429
    {
3430
        $timeLeft = (int) $timeLeft;
3431
        $script = 'redirectExerciseToResult();';
3432
        if (ALL_ON_ONE_PAGE == $this->type) {
3433
            $script = "save_now_all('validate');";
3434
        } elseif (ONE_PER_PAGE == $this->type) {
3435
            $script = 'window.quizTimeEnding = true;
3436
                $(\'[name="save_now"]\').trigger(\'click\');';
3437
        }
3438
3439
        $exerciseSubmitRedirect = '';
3440
        if (!empty($redirectToUrl)) {
3441
            $exerciseSubmitRedirect = "window.location = '$redirectToUrl'";
3442
        }
3443
3444
        return "<script>
3445
            function openClockWarning() {
3446
                $('#clock_warning').dialog({
3447
                    modal:true,
3448
                    height:320,
3449
                    width:550,
3450
                    closeOnEscape: false,
3451
                    resizable: false,
3452
                    buttons: {
3453
                        '".addslashes(get_lang('End test'))."': function() {
3454
                            $('#clock_warning').dialog('close');
3455
                        }
3456
                    },
3457
                    close: function() {
3458
                        send_form();
3459
                    }
3460
                });
3461
3462
                $('#clock_warning').dialog('open');
3463
                $('#counter_to_redirect').epiclock({
3464
                    mode: $.epiclock.modes.countdown,
3465
                    offset: {seconds: 5},
3466
                    format: 's'
3467
                }).bind('timer', function () {
3468
                    send_form();
3469
                });
3470
            }
3471
3472
            function send_form() {
3473
                if ($('#exercise_form').length) {
3474
                    $script
3475
                } else {
3476
                    $exerciseSubmitRedirect
3477
                    // In exercise_reminder.php
3478
                    final_submit();
3479
                }
3480
            }
3481
3482
            function onExpiredTimeExercise() {
3483
                $('#wrapper-clock').hide();
3484
                $('#expired-message-id').show();
3485
                // Fixes bug #5263
3486
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3487
                openClockWarning();
3488
            }
3489
3490
			$(function() {
3491
				// time in seconds when using minutes there are some seconds lost
3492
                var time_left = parseInt(".$timeLeft.");
3493
                $('#exercise_clock_warning').epiclock({
3494
                    mode: $.epiclock.modes.countdown,
3495
                    offset: {seconds: time_left},
3496
                    format: 'x:C:s',
3497
                    renderer: 'minute'
3498
                }).bind('timer', function () {
3499
                    onExpiredTimeExercise();
3500
                });
3501
	       		$('#submit_save').click(function () {});
3502
	        });
3503
	    </script>";
3504
    }
3505
3506
    /**
3507
     * This function was originally found in the exercise_show.php.
3508
     *
3509
     * @param int    $exeId
3510
     * @param int    $questionId
3511
     * @param mixed  $choice                                    the user-selected option
3512
     * @param string $from                                      function is called from 'exercise_show' or
3513
     *                                                          'exercise_result'
3514
     * @param array  $exerciseResultCoordinates                 the hotspot coordinates $hotspot[$question_id] =
3515
     *                                                          coordinates
3516
     * @param bool   $save_results                              save results in the DB or just show the response
3517
     * @param bool   $from_database                             gets information from DB or from the current selection
3518
     * @param bool   $show_result                               show results or not
3519
     * @param int    $propagate_neg
3520
     * @param array  $hotspot_delineation_result
3521
     * @param bool   $showTotalScoreAndUserChoicesInLastAttempt
3522
     * @param bool   $updateResults
3523
     * @param bool   $showHotSpotDelineationTable
3524
     * @param int    $questionDuration                          seconds
3525
     *
3526
     * @return string html code
3527
     *
3528
     * @todo    reduce parameters of this function
3529
     */
3530
    public function manage_answer(
3531
        $exeId,
3532
        $questionId,
3533
        $choice,
3534
        $from = 'exercise_show',
3535
        $exerciseResultCoordinates = [],
3536
        $save_results = true,
3537
        $from_database = false,
3538
        $show_result = true,
3539
        $propagate_neg = 0,
3540
        $hotspot_delineation_result = [],
3541
        $showTotalScoreAndUserChoicesInLastAttempt = true,
3542
        $updateResults = false,
3543
        $showHotSpotDelineationTable = false,
3544
        $questionDuration = 0
3545
    ) {
3546
        $debug = false;
3547
        //needed in order to use in the exercise_attempt() for the time
3548
        global $learnpath_id, $learnpath_item_id;
3549
        require_once api_get_path(LIBRARY_PATH).'geometry.lib.php';
3550
        $em = Database::getManager();
3551
        $feedback_type = $this->getFeedbackType();
3552
        $results_disabled = $this->selectResultsDisabled();
3553
        $questionDuration = (int) $questionDuration;
3554
3555
        if ($debug) {
3556
            error_log('<------ manage_answer ------> ');
3557
            error_log('exe_id: '.$exeId);
3558
            error_log('$from:  '.$from);
3559
            error_log('$save_results: '.(int) $save_results);
3560
            error_log('$from_database: '.(int) $from_database);
3561
            error_log('$show_result: '.(int) $show_result);
3562
            error_log('$propagate_neg: '.$propagate_neg);
3563
            error_log('$exerciseResultCoordinates: '.print_r($exerciseResultCoordinates, 1));
3564
            error_log('$hotspot_delineation_result: '.print_r($hotspot_delineation_result, 1));
3565
            error_log('$learnpath_id: '.$learnpath_id);
3566
            error_log('$learnpath_item_id: '.$learnpath_item_id);
3567
            error_log('$choice: '.print_r($choice, 1));
3568
            error_log('-----------------------------');
3569
        }
3570
3571
        $final_overlap = 0;
3572
        $final_missing = 0;
3573
        $final_excess = 0;
3574
        $overlap_color = 0;
3575
        $missing_color = 0;
3576
        $excess_color = 0;
3577
        $threadhold1 = 0;
3578
        $threadhold2 = 0;
3579
        $threadhold3 = 0;
3580
        $arrques = null;
3581
        $arrans = null;
3582
        $studentChoice = null;
3583
        $expectedAnswer = '';
3584
        $calculatedChoice = '';
3585
        $calculatedStatus = '';
3586
        $questionId = (int) $questionId;
3587
        $exeId = (int) $exeId;
3588
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3589
        $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
3590
        $studentChoiceDegree = null;
3591
3592
        // Creates a temporary Question object
3593
        $course_id = $this->course_id;
3594
        $objQuestionTmp = Question::read($questionId, $this->course);
3595
3596
        if (false === $objQuestionTmp) {
3597
            return false;
3598
        }
3599
3600
        $questionName = $objQuestionTmp->selectTitle();
3601
        $questionWeighting = $objQuestionTmp->selectWeighting();
3602
        $answerType = $objQuestionTmp->selectType();
3603
        $quesId = $objQuestionTmp->getId();
3604
        $extra = $objQuestionTmp->extra;
3605
        $next = 1; //not for now
3606
        $totalWeighting = 0;
3607
        $totalScore = 0;
3608
3609
        // Extra information of the question
3610
        if ((
3611
                MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
3612
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
3613
            )
3614
            && !empty($extra)
3615
        ) {
3616
            $extra = explode(':', $extra);
3617
            // Fixes problems with negatives values using intval
3618
            $true_score = (float) trim($extra[0]);
3619
            $false_score = (float) trim($extra[1]);
3620
            $doubt_score = (float) trim($extra[2]);
3621
        }
3622
3623
        // Construction of the Answer object
3624
        $objAnswerTmp = new Answer($questionId, $course_id);
3625
        $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
3626
3627
        if ($debug) {
3628
            error_log('Count of possible answers: '.$nbrAnswers);
3629
            error_log('$answerType: '.$answerType);
3630
        }
3631
3632
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
3633
            $choiceTmp = $choice;
3634
            $choice = isset($choiceTmp['choice']) ? $choiceTmp['choice'] : '';
3635
            $choiceDegreeCertainty = isset($choiceTmp['choiceDegreeCertainty']) ? $choiceTmp['choiceDegreeCertainty'] : '';
3636
        }
3637
3638
        if (FREE_ANSWER == $answerType ||
3639
            ORAL_EXPRESSION == $answerType ||
3640
            CALCULATED_ANSWER == $answerType ||
3641
            ANNOTATION == $answerType
3642
        ) {
3643
            $nbrAnswers = 1;
3644
        }
3645
3646
        $generatedFilesHtml = '';
3647
        if ($answerType == ORAL_EXPRESSION) {
3648
            $generatedFilesHtml = ExerciseLib::getOralFileAudio($exeId, $questionId);
3649
        }
3650
3651
        $user_answer = '';
3652
        // Get answer list for matching
3653
        $sql = "SELECT iid, answer
3654
                FROM $table_ans
3655
                WHERE question_id = $questionId";
3656
        $res_answer = Database::query($sql);
3657
3658
        $answerMatching = [];
3659
        while ($real_answer = Database::fetch_array($res_answer)) {
3660
            $answerMatching[$real_answer['iid']] = $real_answer['answer'];
3661
        }
3662
3663
        // Get first answer needed for global question, no matter the answer shuffle option;
3664
        $firstAnswer = [];
3665
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
3666
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
3667
        ) {
3668
            $sql = "SELECT *
3669
                    FROM $table_ans
3670
                    WHERE question_id = $questionId
3671
                    ORDER BY position
3672
                    LIMIT 1";
3673
            $result = Database::query($sql);
3674
            if (Database::num_rows($result)) {
3675
                $firstAnswer = Database::fetch_array($result);
3676
            }
3677
        }
3678
3679
        $real_answers = [];
3680
        $quiz_question_options = Question::readQuestionOption($questionId, $course_id);
3681
3682
        $organs_at_risk_hit = 0;
3683
        $questionScore = 0;
3684
        $orderedHotSpots = [];
3685
        if (HOT_SPOT == $answerType || ANNOTATION == $answerType) {
3686
            $orderedHotSpots = $em->getRepository(TrackEHotspot::class)->findBy(
3687
                [
3688
                    'hotspotQuestionId' => $questionId,
3689
                    'course' => $course_id,
3690
                    'hotspotExeId' => $exeId,
3691
                ],
3692
                ['hotspotAnswerId' => 'ASC']
3693
            );
3694
        }
3695
3696
        if ($debug) {
3697
            error_log('-- Start answer loop --');
3698
        }
3699
3700
        $answerDestination = null;
3701
        $userAnsweredQuestion = false;
3702
        $correctAnswerId = [];
3703
        for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
3704
            $answer = $objAnswerTmp->selectAnswer($answerId);
3705
            $answerComment = $objAnswerTmp->selectComment($answerId);
3706
            $answerCorrect = $objAnswerTmp->isCorrect($answerId);
3707
            $answerWeighting = (float) $objAnswerTmp->selectWeighting($answerId);
3708
            $answerAutoId = $objAnswerTmp->selectAutoId($answerId);
3709
            $answerIid = isset($objAnswerTmp->iid[$answerId]) ? (int) $objAnswerTmp->iid[$answerId] : 0;
3710
3711
            if ($debug) {
3712
                error_log("c_quiz_answer.id_auto: $answerAutoId ");
3713
                error_log("Answer marked as correct in db (0/1)?: $answerCorrect ");
3714
                error_log("answerWeighting: $answerWeighting");
3715
            }
3716
3717
            // Delineation
3718
            $delineation_cord = $objAnswerTmp->selectHotspotCoordinates(1);
3719
            $answer_delineation_destination = $objAnswerTmp->selectDestination(1);
3720
3721
            switch ($answerType) {
3722
                case UNIQUE_ANSWER:
3723
                case UNIQUE_ANSWER_IMAGE:
3724
                case UNIQUE_ANSWER_NO_OPTION:
3725
                case READING_COMPREHENSION:
3726
                    if ($from_database) {
3727
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3728
                                WHERE
3729
                                    exe_id = $exeId AND
3730
                                    question_id = $questionId";
3731
                        $result = Database::query($sql);
3732
                        $choice = Database::result($result, 0, 'answer');
3733
3734
                        if (false === $userAnsweredQuestion) {
3735
                            $userAnsweredQuestion = !empty($choice);
3736
                        }
3737
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3738
                        if ($studentChoice) {
3739
                            $questionScore += $answerWeighting;
3740
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3741
                            $correctAnswerId[] = $answerId;
3742
                        }
3743
                    } else {
3744
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3745
                        if ($studentChoice) {
3746
                            $questionScore += $answerWeighting;
3747
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3748
                            $correctAnswerId[] = $answerId;
3749
                        }
3750
                    }
3751
3752
                    break;
3753
                case MULTIPLE_ANSWER_TRUE_FALSE:
3754
                    if ($from_database) {
3755
                        $choice = [];
3756
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3757
                                WHERE
3758
                                    exe_id = $exeId AND
3759
                                    question_id = ".$questionId;
3760
3761
                        $result = Database::query($sql);
3762
                        while ($row = Database::fetch_array($result)) {
3763
                            $values = explode(':', $row['answer']);
3764
                            $my_answer_id = isset($values[0]) ? $values[0] : '';
3765
                            $option = isset($values[1]) ? $values[1] : '';
3766
                            $choice[$my_answer_id] = $option;
3767
                        }
3768
                        $userAnsweredQuestion = !empty($choice);
3769
                    }
3770
3771
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3772
                    if (!empty($studentChoice)) {
3773
                        $correctAnswerId[] = $answerAutoId;
3774
                        if ($studentChoice == $answerCorrect) {
3775
                            $questionScore += $true_score;
3776
                        } else {
3777
                            if (isset($quiz_question_options[$studentChoice])
3778
                                && in_array($quiz_question_options[$studentChoice]['name'], ["Don't know", 'DoubtScore'])
3779
                            ) {
3780
                                $questionScore += $doubt_score;
3781
                            } else {
3782
                                $questionScore += $false_score;
3783
                            }
3784
                        }
3785
                    } else {
3786
                        // If no result then the user just hit don't know
3787
                        $studentChoice = 3;
3788
                        $questionScore += $doubt_score;
3789
                    }
3790
                    $totalScore = $questionScore;
3791
3792
                    break;
3793
                case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
3794
                    if ($from_database) {
3795
                        $choice = [];
3796
                        $choiceDegreeCertainty = [];
3797
                        $sql = "SELECT answer
3798
                                FROM $TBL_TRACK_ATTEMPT
3799
                                WHERE exe_id = $exeId AND question_id = $questionId";
3800
3801
                        $result = Database::query($sql);
3802
                        while ($row = Database::fetch_array($result)) {
3803
                            $ind = $row['answer'];
3804
                            $values = explode(':', $ind);
3805
                            $myAnswerId = $values[0] ?? null;
3806
                            $option = $values[1] ?? null;
3807
                            $percent = $values[2] ?? null;
3808
                            $choice[$myAnswerId] = $option;
3809
                            $choiceDegreeCertainty[$myAnswerId] = $percent;
3810
                        }
3811
                    }
3812
3813
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3814
                    $studentChoiceDegree = isset($choiceDegreeCertainty[$answerAutoId]) ? $choiceDegreeCertainty[$answerAutoId] : null;
3815
3816
                    // student score update
3817
                    if (!empty($studentChoice)) {
3818
                        if ($studentChoice == $answerCorrect) {
3819
                            // correct answer and student is Unsure or PrettySur
3820
                            if (isset($quiz_question_options[$studentChoiceDegree]) &&
3821
                                $quiz_question_options[$studentChoiceDegree]['position'] >= 3 &&
3822
                                $quiz_question_options[$studentChoiceDegree]['position'] < 9
3823
                            ) {
3824
                                $questionScore += $true_score;
3825
                            } else {
3826
                                // student ignore correct answer
3827
                                $questionScore += $doubt_score;
3828
                            }
3829
                        } else {
3830
                            // false answer and student is Unsure or PrettySur
3831
                            if (isset($quiz_question_options[$studentChoiceDegree]) && $quiz_question_options[$studentChoiceDegree]['position'] >= 3
3832
                                && $quiz_question_options[$studentChoiceDegree]['position'] < 9) {
3833
                                $questionScore += $false_score;
3834
                            } else {
3835
                                // student ignore correct answer
3836
                                $questionScore += $doubt_score;
3837
                            }
3838
                        }
3839
                    }
3840
                    $totalScore = $questionScore;
3841
3842
                    break;
3843
                case MULTIPLE_ANSWER:
3844
                    if ($from_database) {
3845
                        $choice = [];
3846
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3847
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3848
                        $resultans = Database::query($sql);
3849
                        while ($row = Database::fetch_array($resultans)) {
3850
                            $choice[$row['answer']] = 1;
3851
                        }
3852
3853
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3854
                        $real_answers[$answerId] = (bool) $studentChoice;
3855
3856
                        if ($studentChoice) {
3857
                            $questionScore += $answerWeighting;
3858
                        }
3859
                    } else {
3860
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3861
                        $real_answers[$answerId] = (bool) $studentChoice;
3862
3863
                        if (isset($studentChoice)) {
3864
                            $correctAnswerId[] = $answerAutoId;
3865
                            $questionScore += $answerWeighting;
3866
                        }
3867
                    }
3868
                    $totalScore += $answerWeighting;
3869
3870
                    break;
3871
                case GLOBAL_MULTIPLE_ANSWER:
3872
                    if ($from_database) {
3873
                        $choice = [];
3874
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3875
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3876
                        $resultans = Database::query($sql);
3877
                        while ($row = Database::fetch_array($resultans)) {
3878
                            $choice[$row['answer']] = 1;
3879
                        }
3880
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3881
                        $real_answers[$answerId] = (bool) $studentChoice;
3882
                        if ($studentChoice) {
3883
                            $questionScore += $answerWeighting;
3884
                        }
3885
                    } else {
3886
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3887
                        if (isset($studentChoice)) {
3888
                            $questionScore += $answerWeighting;
3889
                        }
3890
                        $real_answers[$answerId] = (bool) $studentChoice;
3891
                    }
3892
                    $totalScore += $answerWeighting;
3893
                    if ($debug) {
3894
                        error_log("studentChoice: $studentChoice");
3895
                    }
3896
3897
                    break;
3898
                case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
3899
                    if ($from_database) {
3900
                        $choice = [];
3901
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3902
                                WHERE exe_id = $exeId AND question_id = $questionId";
3903
                        $resultans = Database::query($sql);
3904
                        while ($row = Database::fetch_array($resultans)) {
3905
                            $result = explode(':', $row['answer']);
3906
                            if (isset($result[0])) {
3907
                                $my_answer_id = isset($result[0]) ? $result[0] : '';
3908
                                $option = isset($result[1]) ? $result[1] : '';
3909
                                $choice[$my_answer_id] = $option;
3910
                            }
3911
                        }
3912
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3913
3914
                        $real_answers[$answerId] = false;
3915
                        if ($answerCorrect == $studentChoice) {
3916
                            $real_answers[$answerId] = true;
3917
                        }
3918
                    } else {
3919
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3920
                        $real_answers[$answerId] = false;
3921
                        if ($answerCorrect == $studentChoice) {
3922
                            $real_answers[$answerId] = true;
3923
                        }
3924
                    }
3925
3926
                    break;
3927
                case MULTIPLE_ANSWER_COMBINATION:
3928
                    if ($from_database) {
3929
                        $choice = [];
3930
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3931
                                WHERE exe_id = $exeId AND question_id = $questionId";
3932
                        $resultans = Database::query($sql);
3933
                        while ($row = Database::fetch_array($resultans)) {
3934
                            $choice[$row['answer']] = 1;
3935
                        }
3936
3937
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3938
                        if (1 == $answerCorrect) {
3939
                            $real_answers[$answerId] = false;
3940
                            if ($studentChoice) {
3941
                                $real_answers[$answerId] = true;
3942
                            }
3943
                        } else {
3944
                            $real_answers[$answerId] = true;
3945
                            if ($studentChoice) {
3946
                                $real_answers[$answerId] = false;
3947
                            }
3948
                        }
3949
                    } else {
3950
                        $studentChoice = $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
                    }
3963
3964
                    break;
3965
                case FILL_IN_BLANKS:
3966
                    $str = '';
3967
                    $answerFromDatabase = '';
3968
                    if ($from_database) {
3969
                        $sql = "SELECT answer
3970
                                FROM $TBL_TRACK_ATTEMPT
3971
                                WHERE
3972
                                    exe_id = $exeId AND
3973
                                    question_id= $questionId ";
3974
                        $result = Database::query($sql);
3975
                        $str = $answerFromDatabase = Database::result($result, 0, 'answer');
3976
                    }
3977
3978
                    // if ($saved_results == false && strpos($answerFromDatabase, 'font color') !== false) {
3979
                    if (false) {
3980
                        // the question is encoded like this
3981
                        // [A] B [C] D [E] F::10,10,10@1
3982
                        // number 1 before the "@" means that is a switchable fill in blank question
3983
                        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
3984
                        // means that is a normal fill blank question
3985
                        // first we explode the "::"
3986
                        $pre_array = explode('::', $answer);
3987
3988
                        // is switchable fill blank or not
3989
                        $last = count($pre_array) - 1;
3990
                        $is_set_switchable = explode('@', $pre_array[$last]);
3991
                        $switchable_answer_set = false;
3992
                        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
3993
                            $switchable_answer_set = true;
3994
                        }
3995
                        $answer = '';
3996
                        for ($k = 0; $k < $last; $k++) {
3997
                            $answer .= $pre_array[$k];
3998
                        }
3999
                        // splits weightings that are joined with a comma
4000
                        $answerWeighting = explode(',', $is_set_switchable[0]);
4001
                        // we save the answer because it will be modified
4002
                        $temp = $answer;
4003
                        $answer = '';
4004
                        $j = 0;
4005
                        //initialise answer tags
4006
                        $user_tags = $correct_tags = $real_text = [];
4007
                        // the loop will stop at the end of the text
4008
                        while (1) {
4009
                            // quits the loop if there are no more blanks (detect '[')
4010
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
4011
                                // adds the end of the text
4012
                                $answer = $temp;
4013
                                $real_text[] = $answer;
4014
4015
                                break; //no more "blanks", quit the loop
4016
                            }
4017
                            // adds the piece of text that is before the blank
4018
                            //and ends with '[' into a general storage array
4019
                            $real_text[] = api_substr($temp, 0, $pos + 1);
4020
                            $answer .= api_substr($temp, 0, $pos + 1);
4021
                            //take the string remaining (after the last "[" we found)
4022
                            $temp = api_substr($temp, $pos + 1);
4023
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4024
                            if (false === ($pos = api_strpos($temp, ']'))) {
4025
                                // adds the end of the text
4026
                                $answer .= $temp;
4027
4028
                                break;
4029
                            }
4030
                            if ($from_database) {
4031
                                $str = $answerFromDatabase;
4032
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4033
                                $str = str_replace('\r\n', '', $str);
4034
4035
                                $choice = $arr[1];
4036
                                if (isset($choice[$j])) {
4037
                                    $tmp = api_strrpos($choice[$j], ' / ');
4038
                                    $choice[$j] = api_substr($choice[$j], 0, $tmp);
4039
                                    $choice[$j] = trim($choice[$j]);
4040
                                    // Needed to let characters ' and " to work as part of an answer
4041
                                    $choice[$j] = stripslashes($choice[$j]);
4042
                                } else {
4043
                                    $choice[$j] = null;
4044
                                }
4045
                            } else {
4046
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4047
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4048
                            }
4049
4050
                            $user_tags[] = $choice[$j];
4051
                            // Put the contents of the [] answer tag into correct_tags[]
4052
                            $correct_tags[] = api_substr($temp, 0, $pos);
4053
                            $j++;
4054
                            $temp = api_substr($temp, $pos + 1);
4055
                        }
4056
                        $answer = '';
4057
                        $real_correct_tags = $correct_tags;
4058
                        $chosen_list = [];
4059
4060
                        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...
4061
                            if (0 == $i) {
4062
                                $answer .= $real_text[0];
4063
                            }
4064
                            if (!$switchable_answer_set) {
4065
                                // Needed to parse ' and " characters
4066
                                $user_tags[$i] = stripslashes($user_tags[$i]);
4067
                                if ($correct_tags[$i] == $user_tags[$i]) {
4068
                                    // gives the related weighting to the student
4069
                                    $questionScore += $answerWeighting[$i];
4070
                                    // increments total score
4071
                                    $totalScore += $answerWeighting[$i];
4072
                                    // adds the word in green at the end of the string
4073
                                    $answer .= $correct_tags[$i];
4074
                                } elseif (!empty($user_tags[$i])) {
4075
                                    // else if the word entered by the student IS NOT the same as
4076
                                    // the one defined by the professor
4077
                                    // adds the word in red at the end of the string, and strikes it
4078
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4079
                                } else {
4080
                                    // adds a tabulation if no word has been typed by the student
4081
                                    $answer .= ''; // remove &nbsp; that causes issue
4082
                                }
4083
                            } else {
4084
                                // switchable fill in the blanks
4085
                                if (in_array($user_tags[$i], $correct_tags)) {
4086
                                    $chosen_list[] = $user_tags[$i];
4087
                                    $correct_tags = array_diff($correct_tags, $chosen_list);
4088
                                    // gives the related weighting to the student
4089
                                    $questionScore += $answerWeighting[$i];
4090
                                    // increments total score
4091
                                    $totalScore += $answerWeighting[$i];
4092
                                    // adds the word in green at the end of the string
4093
                                    $answer .= $user_tags[$i];
4094
                                } elseif (!empty($user_tags[$i])) {
4095
                                    // else if the word entered by the student IS NOT the same
4096
                                    // as the one defined by the professor
4097
                                    // adds the word in red at the end of the string, and strikes it
4098
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4099
                                } else {
4100
                                    // adds a tabulation if no word has been typed by the student
4101
                                    $answer .= ''; // remove &nbsp; that causes issue
4102
                                }
4103
                            }
4104
4105
                            // adds the correct word, followed by ] to close the blank
4106
                            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4107
                            if (isset($real_text[$i + 1])) {
4108
                                $answer .= $real_text[$i + 1];
4109
                            }
4110
                        }
4111
                    } else {
4112
                        // insert the student result in the track_e_attempt table, field answer
4113
                        // $answer is the answer like in the c_quiz_answer table for the question
4114
                        // student data are choice[]
4115
                        $listCorrectAnswers = FillBlanks::getAnswerInfo($answer);
4116
                        $switchableAnswerSet = $listCorrectAnswers['switchable'];
4117
                        $answerWeighting = $listCorrectAnswers['weighting'];
4118
                        // user choices is an array $choice
4119
4120
                        // get existing user data in n the BDD
4121
                        if ($from_database) {
4122
                            $listStudentResults = FillBlanks::getAnswerInfo(
4123
                                $answerFromDatabase,
4124
                                true
4125
                            );
4126
                            $choice = $listStudentResults['student_answer'];
4127
                        }
4128
4129
                        // loop other all blanks words
4130
                        if (!$switchableAnswerSet) {
4131
                            // not switchable answer, must be in the same place than teacher order
4132
                            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...
4133
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
4134
                                $correctAnswer = $listCorrectAnswers['words'][$i];
4135
4136
                                if ($debug) {
4137
                                    error_log("Student answer: $i");
4138
                                    error_log($studentAnswer);
4139
                                }
4140
4141
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4142
                                // Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
4143
                                // ENT_QUOTES is used in order to transform ' to &#039;
4144
                                if (!$from_database) {
4145
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4146
                                    if ($debug) {
4147
                                        error_log('Student answer cleaned:');
4148
                                        error_log($studentAnswer);
4149
                                    }
4150
                                }
4151
4152
                                $isAnswerCorrect = 0;
4153
                                if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
4154
                                    // gives the related weighting to the student
4155
                                    $questionScore += $answerWeighting[$i];
4156
                                    // increments total score
4157
                                    $totalScore += $answerWeighting[$i];
4158
                                    $isAnswerCorrect = 1;
4159
                                }
4160
                                if ($debug) {
4161
                                    error_log("isAnswerCorrect $i: $isAnswerCorrect");
4162
                                }
4163
4164
                                $studentAnswerToShow = $studentAnswer;
4165
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4166
                                if ($debug) {
4167
                                    error_log("Fill in blank type: $type");
4168
                                }
4169
                                if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4170
                                    $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4171
                                    if ('' != $studentAnswer) {
4172
                                        foreach ($listMenu as $item) {
4173
                                            if (sha1($item) == $studentAnswer) {
4174
                                                $studentAnswerToShow = $item;
4175
                                            }
4176
                                        }
4177
                                    }
4178
                                }
4179
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4180
                                $listCorrectAnswers['student_score'][$i] = $isAnswerCorrect;
4181
                            }
4182
                        } else {
4183
                            // switchable answer
4184
                            $listStudentAnswerTemp = $choice;
4185
                            $listTeacherAnswerTemp = $listCorrectAnswers['words'];
4186
4187
                            // for every teacher answer, check if there is a student answer
4188
                            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...
4189
                                $studentAnswer = trim($listStudentAnswerTemp[$i]);
4190
                                $studentAnswerToShow = $studentAnswer;
4191
4192
                                if (empty($studentAnswer)) {
4193
                                    break;
4194
                                }
4195
4196
                                if ($debug) {
4197
                                    error_log("Student answer: $i");
4198
                                    error_log($studentAnswer);
4199
                                }
4200
4201
                                if (!$from_database) {
4202
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4203
                                    if ($debug) {
4204
                                        error_log("Student answer cleaned:");
4205
                                        error_log($studentAnswer);
4206
                                    }
4207
                                }
4208
4209
                                $found = false;
4210
                                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...
4211
                                    $correctAnswer = $listTeacherAnswerTemp[$j];
4212
4213
                                    if (!$found) {
4214
                                        if (FillBlanks::isStudentAnswerGood(
4215
                                            $studentAnswer,
4216
                                            $correctAnswer,
4217
                                            $from_database
4218
                                        )) {
4219
                                            $questionScore += $answerWeighting[$i];
4220
                                            $totalScore += $answerWeighting[$i];
4221
                                            $listTeacherAnswerTemp[$j] = '';
4222
                                            $found = true;
4223
                                        }
4224
                                    }
4225
4226
                                    $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4227
                                    if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4228
                                        $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4229
                                        if (!empty($studentAnswer)) {
4230
                                            foreach ($listMenu as $key => $item) {
4231
                                                if (sha1($item) === $studentAnswer) {
4232
                                                    $studentAnswerToShow = $item;
4233
                                                    break;
4234
                                                }
4235
                                            }
4236
                                        }
4237
                                    }
4238
                                }
4239
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4240
                                $listCorrectAnswers['student_score'][$i] = $found ? 1 : 0;
4241
                            }
4242
                        }
4243
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
4244
                    }
4245
4246
                    break;
4247
                case CALCULATED_ANSWER:
4248
                    $calculatedAnswerList = Session::read('calculatedAnswerId');
4249
                    if (!empty($calculatedAnswerList)) {
4250
                        $answer = $objAnswerTmp->selectAnswer($calculatedAnswerList[$questionId]);
4251
                        $preArray = explode('@@', $answer);
4252
                        $last = count($preArray) - 1;
4253
                        $answer = '';
4254
                        for ($k = 0; $k < $last; $k++) {
4255
                            $answer .= $preArray[$k];
4256
                        }
4257
                        $answerWeighting = [$answerWeighting];
4258
                        // we save the answer because it will be modified
4259
                        $temp = $answer;
4260
                        $answer = '';
4261
                        $j = 0;
4262
                        // initialise answer tags
4263
                        $userTags = $correctTags = $realText = [];
4264
                        // the loop will stop at the end of the text
4265
                        while (1) {
4266
                            // quits the loop if there are no more blanks (detect '[')
4267
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
4268
                                // adds the end of the text
4269
                                $answer = $temp;
4270
                                $realText[] = $answer;
4271
4272
                                break; //no more "blanks", quit the loop
4273
                            }
4274
                            // adds the piece of text that is before the blank
4275
                            // and ends with '[' into a general storage array
4276
                            $realText[] = api_substr($temp, 0, $pos + 1);
4277
                            $answer .= api_substr($temp, 0, $pos + 1);
4278
                            // take the string remaining (after the last "[" we found)
4279
                            $temp = api_substr($temp, $pos + 1);
4280
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4281
                            if (false === ($pos = api_strpos($temp, ']'))) {
4282
                                // adds the end of the text
4283
                                $answer .= $temp;
4284
4285
                                break;
4286
                            }
4287
4288
                            if ($from_database) {
4289
                                $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4290
                                        WHERE
4291
                                            exe_id = $exeId AND
4292
                                            question_id = $questionId ";
4293
                                $result = Database::query($sql);
4294
                                $str = Database::result($result, 0, 'answer');
4295
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4296
                                $str = str_replace('\r\n', '', $str);
4297
                                $choice = $arr[1];
4298
                                if (isset($choice[$j])) {
4299
                                    $tmp = api_strrpos($choice[$j], ' / ');
4300
                                    if ($tmp) {
4301
                                        $choice[$j] = api_substr($choice[$j], 0, $tmp);
4302
                                    } else {
4303
                                        $tmp = ltrim($tmp, '[');
4304
                                        $tmp = rtrim($tmp, ']');
4305
                                    }
4306
                                    $choice[$j] = trim($choice[$j]);
4307
                                    // Needed to let characters ' and " to work as part of an answer
4308
                                    $choice[$j] = stripslashes($choice[$j]);
4309
                                } else {
4310
                                    $choice[$j] = null;
4311
                                }
4312
                            } else {
4313
                                // This value is the user input not escaped while correct answer is escaped by ckeditor
4314
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4315
                            }
4316
                            $userTags[] = $choice[$j];
4317
                            // put the contents of the [] answer tag into correct_tags[]
4318
                            $correctTags[] = api_substr($temp, 0, $pos);
4319
                            $j++;
4320
                            $temp = api_substr($temp, $pos + 1);
4321
                        }
4322
                        $answer = '';
4323
                        $realCorrectTags = $correctTags;
4324
                        $calculatedStatus = Display::label(get_lang('Incorrect'), 'danger');
4325
                        $expectedAnswer = '';
4326
                        $calculatedChoice = '';
4327
4328
                        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...
4329
                            if (0 == $i) {
4330
                                $answer .= $realText[0];
4331
                            }
4332
                            // Needed to parse ' and " characters
4333
                            $userTags[$i] = stripslashes($userTags[$i]);
4334
                            if ($correctTags[$i] == $userTags[$i]) {
4335
                                // gives the related weighting to the student
4336
                                $questionScore += $answerWeighting[$i];
4337
                                // increments total score
4338
                                $totalScore += $answerWeighting[$i];
4339
                                // adds the word in green at the end of the string
4340
                                $answer .= $correctTags[$i];
4341
                                $calculatedChoice = $correctTags[$i];
4342
                            } elseif (!empty($userTags[$i])) {
4343
                                // else if the word entered by the student IS NOT the same as
4344
                                // the one defined by the professor
4345
                                // adds the word in red at the end of the string, and strikes it
4346
                                $answer .= '<font color="red"><s>'.$userTags[$i].'</s></font>';
4347
                                $calculatedChoice = $userTags[$i];
4348
                            } else {
4349
                                // adds a tabulation if no word has been typed by the student
4350
                                $answer .= ''; // remove &nbsp; that causes issue
4351
                            }
4352
                            // adds the correct word, followed by ] to close the blank
4353
                            if (EXERCISE_FEEDBACK_TYPE_EXAM != $this->results_disabled) {
4354
                                $answer .= ' / <font color="green"><b>'.$realCorrectTags[$i].'</b></font>';
4355
                                $calculatedStatus = Display::label(get_lang('Correct'), 'success');
4356
                                $expectedAnswer = $realCorrectTags[$i];
4357
                            }
4358
                            $answer .= ']';
4359
                            if (isset($realText[$i + 1])) {
4360
                                $answer .= $realText[$i + 1];
4361
                            }
4362
                        }
4363
                    } else {
4364
                        if ($from_database) {
4365
                            $sql = "SELECT *
4366
                                    FROM $TBL_TRACK_ATTEMPT
4367
                                    WHERE
4368
                                        exe_id = $exeId AND
4369
                                        question_id = $questionId ";
4370
                            $result = Database::query($sql);
4371
                            $resultData = Database::fetch_assoc($result);
4372
                            $answer = $resultData['answer'];
4373
                            $questionScore = $resultData['marks'];
4374
                        }
4375
                    }
4376
4377
                    break;
4378
                case FREE_ANSWER:
4379
                    if ($from_database) {
4380
                        $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
4381
                                 WHERE
4382
                                    exe_id = $exeId AND
4383
                                    question_id= ".$questionId;
4384
                        $result = Database::query($sql);
4385
                        $data = Database::fetch_array($result);
4386
                        $choice = '';
4387
                        $questionScore = 0;
4388
                        if ($data) {
4389
                            $choice = $data['answer'];
4390
                            $questionScore = $data['marks'];
4391
                        }
4392
4393
                        $choice = str_replace('\r\n', '', $choice);
4394
                        $choice = stripslashes($choice);
4395
4396
                        if (-1 == $questionScore) {
4397
                            $totalScore += 0;
4398
                        } else {
4399
                            $totalScore += $questionScore;
4400
                        }
4401
                        if ('' == $questionScore) {
4402
                            $questionScore = 0;
4403
                        }
4404
                        $arrques = $questionName;
4405
                        $arrans = $choice;
4406
                    } else {
4407
                        $studentChoice = $choice;
4408
                        if ($studentChoice) {
4409
                            //Fixing negative puntation see #2193
4410
                            $questionScore = 0;
4411
                            $totalScore += 0;
4412
                        }
4413
                    }
4414
4415
                    break;
4416
                case ORAL_EXPRESSION:
4417
                    if ($from_database) {
4418
                        $query = "SELECT answer, marks
4419
                                  FROM $TBL_TRACK_ATTEMPT
4420
                                  WHERE
4421
                                        exe_id = $exeId AND
4422
                                        question_id = $questionId
4423
                                 ";
4424
                        $resq = Database::query($query);
4425
                        $row = Database::fetch_assoc($resq);
4426
                        $choice = '';
4427
                        $questionScore = 0;
4428
4429
                        if (is_array($row)) {
4430
                            $choice = $row['answer'];
4431
                            $choice = str_replace('\r\n', '', $choice);
4432
                            $choice = stripslashes($choice);
4433
                            $questionScore = $row['marks'];
4434
                        }
4435
4436
                        if (-1 == $questionScore) {
4437
                            $totalScore += 0;
4438
                        } else {
4439
                            $totalScore += $questionScore;
4440
                        }
4441
                        $arrques = $questionName;
4442
                        $arrans = $choice;
4443
                    } else {
4444
                        $studentChoice = $choice;
4445
                        if ($studentChoice) {
4446
                            //Fixing negative puntation see #2193
4447
                            $questionScore = 0;
4448
                            $totalScore += 0;
4449
                        }
4450
                    }
4451
4452
                    break;
4453
                case DRAGGABLE:
4454
                case MATCHING_DRAGGABLE:
4455
                case MATCHING:
4456
                    if ($from_database) {
4457
                        $sql = "SELECT iid, answer
4458
                                FROM $table_ans
4459
                                WHERE
4460
                                    question_id = $questionId AND
4461
                                    correct = 0
4462
                                ";
4463
                        $result = Database::query($sql);
4464
                        // Getting the real answer
4465
                        $real_list = [];
4466
                        while ($realAnswer = Database::fetch_array($result)) {
4467
                            $real_list[$realAnswer['iid']] = $realAnswer['answer'];
4468
                        }
4469
4470
                        $orderBy = ' ORDER BY iid ';
4471
                        if (DRAGGABLE == $answerType) {
4472
                            $orderBy = ' ORDER BY correct ';
4473
                        }
4474
4475
                        $sql = "SELECT iid, answer, correct, ponderation
4476
                                FROM $table_ans
4477
                                WHERE
4478
                                    question_id = $questionId AND
4479
                                    correct <> 0
4480
                                $orderBy";
4481
                        $result = Database::query($sql);
4482
                        $options = [];
4483
                        $correctAnswers = [];
4484
                        while ($row = Database::fetch_assoc($result)) {
4485
                            $options[] = $row;
4486
                            $correctAnswers[$row['correct']] = $row['answer'];
4487
                        }
4488
4489
                        $questionScore = 0;
4490
                        $counterAnswer = 1;
4491
                        foreach ($options as $a_answers) {
4492
                            $i_answer_id = $a_answers['iid']; //3
4493
                            $s_answer_label = $a_answers['answer']; // your daddy - your mother
4494
                            $i_answer_correct_answer = $a_answers['correct']; //1 - 2
4495
                            $i_answer_id_auto = $a_answers['iid']; // 3 - 4
4496
4497
                            $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4498
                                    WHERE
4499
                                        exe_id = '$exeId' AND
4500
                                        question_id = '$questionId' AND
4501
                                        position = '$i_answer_id_auto'";
4502
                            $result = Database::query($sql);
4503
                            $s_user_answer = 0;
4504
                            if (Database::num_rows($result) > 0) {
4505
                                //  rich - good looking
4506
                                $s_user_answer = Database::result($result, 0, 0);
4507
                            }
4508
                            $i_answerWeighting = $a_answers['ponderation'];
4509
                            $user_answer = '';
4510
                            $status = Display::label(get_lang('Incorrect'), 'danger');
4511
4512
                            if (!empty($s_user_answer)) {
4513
                                if (DRAGGABLE == $answerType) {
4514
                                    if ($s_user_answer == $i_answer_correct_answer) {
4515
                                        $questionScore += $i_answerWeighting;
4516
                                        $totalScore += $i_answerWeighting;
4517
                                        $user_answer = Display::label(get_lang('Correct'), 'success');
4518
                                        if ($this->showExpectedChoice() && !empty($i_answer_id_auto)) {
4519
                                            $user_answer = $answerMatching[$i_answer_id_auto];
4520
                                        }
4521
                                        $status = Display::label(get_lang('Correct'), 'success');
4522
                                    } else {
4523
                                        $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4524
                                        if ($this->showExpectedChoice() && !empty($s_user_answer)) {
4525
                                            /*$data = $options[$real_list[$s_user_answer] - 1];
4526
                                            $user_answer = $data['answer'];*/
4527
                                            $user_answer = $correctAnswers[$s_user_answer] ?? '';
4528
                                        }
4529
                                    }
4530
                                } else {
4531
                                    if ($s_user_answer == $i_answer_correct_answer) {
4532
                                        $questionScore += $i_answerWeighting;
4533
                                        $totalScore += $i_answerWeighting;
4534
                                        $status = Display::label(get_lang('Correct'), 'success');
4535
4536
                                        // Try with id
4537
                                        if (isset($real_list[$i_answer_id])) {
4538
                                            $user_answer = Display::span(
4539
                                                $real_list[$i_answer_id],
4540
                                                ['style' => 'color: #008000; font-weight: bold;']
4541
                                            );
4542
                                        }
4543
4544
                                        // Try with $i_answer_id_auto
4545
                                        if (empty($user_answer)) {
4546
                                            if (isset($real_list[$i_answer_id_auto])) {
4547
                                                $user_answer = Display::span(
4548
                                                    $real_list[$i_answer_id_auto],
4549
                                                    ['style' => 'color: #008000; font-weight: bold;']
4550
                                                );
4551
                                            }
4552
                                        }
4553
4554
                                        if (isset($real_list[$i_answer_correct_answer])) {
4555
                                            $user_answer = Display::span(
4556
                                                $real_list[$i_answer_correct_answer],
4557
                                                ['style' => 'color: #008000; font-weight: bold;']
4558
                                            );
4559
                                        }
4560
                                    } else {
4561
                                        $user_answer = Display::span(
4562
                                            $real_list[$s_user_answer],
4563
                                            ['style' => 'color: #FF0000; text-decoration: line-through;']
4564
                                        );
4565
                                        if ($this->showExpectedChoice()) {
4566
                                            if (isset($real_list[$s_user_answer])) {
4567
                                                $user_answer = Display::span($real_list[$s_user_answer]);
4568
                                            }
4569
                                        }
4570
                                    }
4571
                                }
4572
                            } elseif (DRAGGABLE == $answerType) {
4573
                                $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4574
                                if ($this->showExpectedChoice()) {
4575
                                    $user_answer = '';
4576
                                }
4577
                            } else {
4578
                                $user_answer = Display::span(
4579
                                    get_lang('Incorrect').' &nbsp;',
4580
                                    ['style' => 'color: #FF0000; text-decoration: line-through;']
4581
                                );
4582
                                if ($this->showExpectedChoice()) {
4583
                                    $user_answer = '';
4584
                                }
4585
                            }
4586
4587
                            if ($show_result) {
4588
                                if (false === $this->showExpectedChoice() &&
4589
                                    false === $showTotalScoreAndUserChoicesInLastAttempt
4590
                                ) {
4591
                                    $user_answer = '';
4592
                                }
4593
                                switch ($answerType) {
4594
                                    case MATCHING:
4595
                                    case MATCHING_DRAGGABLE:
4596
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4597
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4598
                                                break;
4599
                                            }
4600
                                        }
4601
                                        echo '<tr>';
4602
                                        if (!in_array(
4603
                                            $this->results_disabled,
4604
                                            [
4605
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4606
                                            ]
4607
                                        )
4608
                                        ) {
4609
                                            echo '<td>'.$s_answer_label.'</td>';
4610
                                            echo '<td>'.$user_answer.'</td>';
4611
                                        } else {
4612
                                            echo '<td>'.$s_answer_label.'</td>';
4613
                                            $status = Display::label(get_lang('Correct'), 'success');
4614
                                        }
4615
4616
                                        if ($this->showExpectedChoice()) {
4617
                                            if ($this->showExpectedChoiceColumn()) {
4618
                                                echo '<td>';
4619
                                                if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4620
                                                    if (isset($real_list[$i_answer_correct_answer]) &&
4621
                                                        true == $showTotalScoreAndUserChoicesInLastAttempt
4622
                                                    ) {
4623
                                                        echo Display::span(
4624
                                                            $real_list[$i_answer_correct_answer]
4625
                                                        );
4626
                                                    }
4627
                                                }
4628
                                                echo '</td>';
4629
                                            }
4630
                                            echo '<td class="text-center">'.$status.'</td>';
4631
                                        } else {
4632
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4633
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4634
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4635
                                                ) {
4636
                                                    if ($this->showExpectedChoiceColumn()) {
4637
                                                        echo '<td>';
4638
                                                        echo Display::span(
4639
                                                            $real_list[$i_answer_correct_answer],
4640
                                                            ['style' => 'color: #008000; font-weight: bold;']
4641
                                                        );
4642
                                                        echo '</td>';
4643
                                                    }
4644
                                                }
4645
                                            }
4646
                                        }
4647
                                        echo '</tr>';
4648
4649
                                        break;
4650
                                    case DRAGGABLE:
4651
                                        if (false == $showTotalScoreAndUserChoicesInLastAttempt) {
4652
                                            $s_answer_label = '';
4653
                                        }
4654
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4655
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4656
                                                break;
4657
                                            }
4658
                                        }
4659
                                        echo '<tr>';
4660
                                        if ($this->showExpectedChoice()) {
4661
                                            if (!in_array(
4662
                                                $this->results_disabled,
4663
                                                [
4664
                                                    RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4665
                                                    //RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4666
                                                ]
4667
                                            )
4668
                                            ) {
4669
                                                echo '<td>'.$user_answer.'</td>';
4670
                                            } else {
4671
                                                $status = Display::label(get_lang('Correct'), 'success');
4672
                                            }
4673
                                            echo '<td>'.$s_answer_label.'</td>';
4674
                                            echo '<td class="text-center">'.$status.'</td>';
4675
                                        } else {
4676
                                            echo '<td>'.$s_answer_label.'</td>';
4677
                                            echo '<td>'.$user_answer.'</td>';
4678
                                            echo '<td>';
4679
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4680
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4681
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4682
                                                ) {
4683
                                                    echo Display::span(
4684
                                                        $real_list[$i_answer_correct_answer],
4685
                                                        ['style' => 'color: #008000; font-weight: bold;']
4686
                                                    );
4687
                                                }
4688
                                            }
4689
                                            echo '</td>';
4690
                                        }
4691
                                        echo '</tr>';
4692
4693
                                        break;
4694
                                }
4695
                            }
4696
                            $counterAnswer++;
4697
                        }
4698
4699
                        break 2; // break the switch and the "for" condition
4700
                    } else {
4701
                        if ($answerCorrect) {
4702
                            if (isset($choice[$answerAutoId]) &&
4703
                                $answerCorrect == $choice[$answerAutoId]
4704
                            ) {
4705
                                $correctAnswerId[] = $answerAutoId;
4706
                                $questionScore += $answerWeighting;
4707
                                $totalScore += $answerWeighting;
4708
                                $user_answer = Display::span($answerMatching[$choice[$answerAutoId]]);
4709
                            } else {
4710
                                if (isset($answerMatching[$choice[$answerAutoId]])) {
4711
                                    $user_answer = Display::span(
4712
                                        $answerMatching[$choice[$answerAutoId]],
4713
                                        ['style' => 'color: #FF0000; text-decoration: line-through;']
4714
                                    );
4715
                                }
4716
                            }
4717
                            $matching[$answerAutoId] = $choice[$answerAutoId];
4718
                        }
4719
                    }
4720
4721
                    break;
4722
                case HOT_SPOT:
4723
                    if ($from_database) {
4724
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4725
                        // Check auto id
4726
                        $foundAnswerId = $answerAutoId;
4727
                        $sql = "SELECT hotspot_correct
4728
                                FROM $TBL_TRACK_HOTSPOT
4729
                                WHERE
4730
                                    hotspot_exe_id = $exeId AND
4731
                                    hotspot_question_id= $questionId AND
4732
                                    hotspot_answer_id = $answerAutoId
4733
                                ORDER BY hotspot_id ASC";
4734
                        $result = Database::query($sql);
4735
                        if (Database::num_rows($result)) {
4736
                            $studentChoice = Database::result(
4737
                                $result,
4738
                                0,
4739
                                'hotspot_correct'
4740
                            );
4741
4742
                            if ($studentChoice) {
4743
                                $questionScore += $answerWeighting;
4744
                                $totalScore += $answerWeighting;
4745
                            }
4746
                        } else {
4747
                            // If answer.id is different:
4748
                            $sql = "SELECT hotspot_correct
4749
                                FROM $TBL_TRACK_HOTSPOT
4750
                                WHERE
4751
                                    hotspot_exe_id = $exeId AND
4752
                                    hotspot_question_id= $questionId AND
4753
                                    hotspot_answer_id = ".(int) $answerId.'
4754
                                ORDER BY hotspot_id ASC';
4755
                            $result = Database::query($sql);
4756
4757
                            $foundAnswerId = $answerId;
4758
                            if (Database::num_rows($result)) {
4759
                                $studentChoice = Database::result(
4760
                                    $result,
4761
                                    0,
4762
                                    'hotspot_correct'
4763
                                );
4764
4765
                                if ($studentChoice) {
4766
                                    $questionScore += $answerWeighting;
4767
                                    $totalScore += $answerWeighting;
4768
                                }
4769
                            } else {
4770
                                // check answer.iid
4771
                                if (!empty($answerIid)) {
4772
                                    $sql = "SELECT hotspot_correct
4773
                                            FROM $TBL_TRACK_HOTSPOT
4774
                                            WHERE
4775
                                                hotspot_exe_id = $exeId AND
4776
                                                hotspot_question_id= $questionId AND
4777
                                                hotspot_answer_id = $answerIid
4778
                                            ORDER BY hotspot_id ASC";
4779
                                    $result = Database::query($sql);
4780
4781
                                    $foundAnswerId = $answerIid;
4782
                                    $studentChoice = Database::result(
4783
                                        $result,
4784
                                        0,
4785
                                        'hotspot_correct'
4786
                                    );
4787
4788
                                    if ($studentChoice) {
4789
                                        $questionScore += $answerWeighting;
4790
                                        $totalScore += $answerWeighting;
4791
                                    }
4792
                                }
4793
                            }
4794
                        }
4795
                    } else {
4796
                        if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
4797
                            $choice[$answerAutoId] = 0;
4798
                            $choice[$answerIid] = 0;
4799
                        } else {
4800
                            $studentChoice = $choice[$answerAutoId];
4801
                            if (empty($studentChoice)) {
4802
                                $studentChoice = $choice[$answerIid];
4803
                            }
4804
                            $choiceIsValid = false;
4805
                            if (!empty($studentChoice)) {
4806
                                $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
4807
                                $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
4808
                                $choicePoint = Geometry::decodePoint($studentChoice);
4809
4810
                                switch ($hotspotType) {
4811
                                    case 'square':
4812
                                        $hotspotProperties = Geometry::decodeSquare($hotspotCoordinates);
4813
                                        $choiceIsValid = Geometry::pointIsInSquare($hotspotProperties, $choicePoint);
4814
4815
                                        break;
4816
                                    case 'circle':
4817
                                        $hotspotProperties = Geometry::decodeEllipse($hotspotCoordinates);
4818
                                        $choiceIsValid = Geometry::pointIsInEllipse($hotspotProperties, $choicePoint);
4819
4820
                                        break;
4821
                                    case 'poly':
4822
                                        $hotspotProperties = Geometry::decodePolygon($hotspotCoordinates);
4823
                                        $choiceIsValid = Geometry::pointIsInPolygon($hotspotProperties, $choicePoint);
4824
4825
                                        break;
4826
                                }
4827
                            }
4828
4829
                            $choice[$answerAutoId] = 0;
4830
                            if ($choiceIsValid) {
4831
                                $questionScore += $answerWeighting;
4832
                                $totalScore += $answerWeighting;
4833
                                $choice[$answerAutoId] = 1;
4834
                                $choice[$answerIid] = 1;
4835
                            }
4836
                        }
4837
                    }
4838
4839
                    break;
4840
                case HOT_SPOT_ORDER:
4841
                    // @todo never added to chamilo
4842
                    // for hotspot with fixed order
4843
                    $studentChoice = $choice['order'][$answerId];
4844
                    if ($studentChoice == $answerId) {
4845
                        $questionScore += $answerWeighting;
4846
                        $totalScore += $answerWeighting;
4847
                        $studentChoice = true;
4848
                    } else {
4849
                        $studentChoice = false;
4850
                    }
4851
4852
                    break;
4853
                case HOT_SPOT_DELINEATION:
4854
                    // for hotspot with delineation
4855
                    if ($from_database) {
4856
                        // getting the user answer
4857
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4858
                        $query = "SELECT hotspot_correct, hotspot_coordinate
4859
                                    FROM $TBL_TRACK_HOTSPOT
4860
                                    WHERE
4861
                                        hotspot_exe_id = $exeId AND
4862
                                        hotspot_question_id= $questionId AND
4863
                                        hotspot_answer_id = '1'";
4864
                        // By default we take 1 because it's a delineation
4865
                        $resq = Database::query($query);
4866
                        $row = Database::fetch_assoc($resq);
4867
4868
                        if ($row && isset($row['hotspot_correct'], $row['hotspot_coordinate'])) {
4869
                            $choice = $row['hotspot_correct'];
4870
                            $user_answer = $row['hotspot_coordinate'];
4871
                            $coords = explode('/', $user_answer);
4872
                        } else {
4873
                            $choice = '';
4874
                            $user_answer = '';
4875
                            $coords = [];
4876
                        }
4877
4878
                        $user_array = '';
4879
                        foreach ($coords as $coord) {
4880
                            [$x, $y] = explode(';', $coord);
4881
                            $user_array .= round($x).';'.round($y).'/';
4882
                        }
4883
                        $user_array = substr($user_array, 0, -1) ?: '';
4884
                    } else {
4885
                        if (!empty($studentChoice)) {
4886
                            $correctAnswerId[] = $answerAutoId;
4887
                            $newquestionList[] = $questionId;
4888
                        }
4889
4890
                        if (1 === $answerId && isset($choice[$answerId])) {
4891
                            $studentChoice = $choice[$answerId];
4892
                            $questionScore += $answerWeighting;
4893
                        }
4894
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
4895
                            $user_array = $_SESSION['exerciseResultCoordinates'][$questionId];
4896
                        }
4897
                    }
4898
                    $_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
4899
                    $_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
4900
4901
                    break;
4902
                case ANNOTATION:
4903
                    if ($from_database) {
4904
                        $sql = "SELECT answer, marks
4905
                                FROM $TBL_TRACK_ATTEMPT
4906
                                WHERE
4907
                                  exe_id = $exeId AND
4908
                                  question_id = $questionId ";
4909
                        $resq = Database::query($sql);
4910
                        $data = Database::fetch_array($resq);
4911
4912
                        $questionScore = empty($data['marks']) ? 0 : $data['marks'];
4913
                        $arrques = $questionName;
4914
4915
                        break;
4916
                    }
4917
                    $studentChoice = $choice;
4918
                    if ($studentChoice) {
4919
                        $questionScore = 0;
4920
                    }
4921
4922
                    break;
4923
            }
4924
4925
            if ($show_result) {
4926
                if ('exercise_result' === $from) {
4927
                    // Display answers (if not matching type, or if the answer is correct)
4928
                    if (!in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE]) ||
4929
                        $answerCorrect
4930
                    ) {
4931
                        if (in_array(
4932
                            $answerType,
4933
                            [
4934
                                UNIQUE_ANSWER,
4935
                                UNIQUE_ANSWER_IMAGE,
4936
                                UNIQUE_ANSWER_NO_OPTION,
4937
                                MULTIPLE_ANSWER,
4938
                                MULTIPLE_ANSWER_COMBINATION,
4939
                                GLOBAL_MULTIPLE_ANSWER,
4940
                                READING_COMPREHENSION,
4941
                            ]
4942
                        )) {
4943
                            ExerciseShowFunctions::display_unique_or_multiple_answer(
4944
                                $this,
4945
                                $feedback_type,
4946
                                $answerType,
4947
                                $studentChoice,
4948
                                $answer,
4949
                                $answerComment,
4950
                                $answerCorrect,
4951
                                0,
4952
                                0,
4953
                                0,
4954
                                $results_disabled,
4955
                                $showTotalScoreAndUserChoicesInLastAttempt,
4956
                                $this->export
4957
                            );
4958
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
4959
                            ExerciseShowFunctions::display_multiple_answer_true_false(
4960
                                $this,
4961
                                $feedback_type,
4962
                                $answerType,
4963
                                $studentChoice,
4964
                                $answer,
4965
                                $answerComment,
4966
                                $answerCorrect,
4967
                                0,
4968
                                $questionId,
4969
                                0,
4970
                                $results_disabled,
4971
                                $showTotalScoreAndUserChoicesInLastAttempt
4972
                            );
4973
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
4974
                            ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
4975
                                $this,
4976
                                $feedback_type,
4977
                                $studentChoice,
4978
                                $studentChoiceDegree,
4979
                                $answer,
4980
                                $answerComment,
4981
                                $answerCorrect,
4982
                                $questionId,
4983
                                $results_disabled
4984
                            );
4985
                        } elseif (MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType) {
4986
                            ExerciseShowFunctions::display_multiple_answer_combination_true_false(
4987
                                $this,
4988
                                $feedback_type,
4989
                                $answerType,
4990
                                $studentChoice,
4991
                                $answer,
4992
                                $answerComment,
4993
                                $answerCorrect,
4994
                                0,
4995
                                0,
4996
                                0,
4997
                                $results_disabled,
4998
                                $showTotalScoreAndUserChoicesInLastAttempt
4999
                            );
5000
                        } elseif (FILL_IN_BLANKS == $answerType) {
5001
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5002
                                $this,
5003
                                $feedback_type,
5004
                                $answer,
5005
                                0,
5006
                                0,
5007
                                $results_disabled,
5008
                                $showTotalScoreAndUserChoicesInLastAttempt,
5009
                                ''
5010
                            );
5011
                        } elseif (CALCULATED_ANSWER == $answerType) {
5012
                            ExerciseShowFunctions::display_calculated_answer(
5013
                                $this,
5014
                                $feedback_type,
5015
                                $answer,
5016
                                0,
5017
                                0,
5018
                                $results_disabled,
5019
                                $showTotalScoreAndUserChoicesInLastAttempt,
5020
                                $expectedAnswer,
5021
                                $calculatedChoice,
5022
                                $calculatedStatus
5023
                            );
5024
                        } elseif (FREE_ANSWER == $answerType) {
5025
                            ExerciseShowFunctions::display_free_answer(
5026
                                $feedback_type,
5027
                                $choice,
5028
                                $exeId,
5029
                                $questionId,
5030
                                $questionScore,
5031
                                $results_disabled
5032
                            );
5033
                        } elseif (ORAL_EXPRESSION == $answerType) {
5034
                            // to store the details of open questions in an array to be used in mail
5035
                            /** @var OralExpression $objQuestionTmp */
5036
                            ExerciseShowFunctions::display_oral_expression_answer(
5037
                                $feedback_type,
5038
                                $choice,
5039
                                $exeId,
5040
                                $questionId,
5041
                                $results_disabled,
5042
                                $questionScore,
5043
                                true
5044
                            );
5045
                        } elseif (HOT_SPOT == $answerType) {
5046
                            $correctAnswerId = 0;
5047
                            /** @var TrackEHotspot $hotspot */
5048
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5049
                                if ($hotspot->getHotspotAnswerId() == $answerAutoId) {
5050
                                    break;
5051
                                }
5052
                            }
5053
5054
                            // force to show whether the choice is correct or not
5055
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
5056
                            ExerciseShowFunctions::display_hotspot_answer(
5057
                                $this,
5058
                                $feedback_type,
5059
                                $answerId,
5060
                                $answer,
5061
                                $studentChoice,
5062
                                $answerComment,
5063
                                $results_disabled,
5064
                                $answerId,
5065
                                $showTotalScoreAndUserChoicesInLastAttempt
5066
                            );
5067
                        } elseif (HOT_SPOT_ORDER == $answerType) {
5068
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5069
                                $feedback_type,
5070
                                $answerId,
5071
                                $answer,
5072
                                $studentChoice,
5073
                                $answerComment
5074
                            );*/
5075
                        } elseif (HOT_SPOT_DELINEATION == $answerType && isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
5076
                            $user_answer = $_SESSION['exerciseResultCoordinates'][$questionId];
5077
5078
                            // Round-up the coordinates
5079
                            $coords = explode('/', $user_answer);
5080
                            $coords = array_filter($coords);
5081
                            $user_array = '';
5082
                            foreach ($coords as $coord) {
5083
                                if (!empty($coord)) {
5084
                                    $parts = explode(';', $coord);
5085
                                    if (!empty($parts)) {
5086
                                        $user_array .= round($parts[0]).';'.round($parts[1]).'/';
5087
                                    }
5088
                                }
5089
                            }
5090
                            $user_array = substr($user_array, 0, -1) ?: '';
5091
                            if ($next) {
5092
                                $user_answer = $user_array;
5093
                                // We compare only the delineation not the other points
5094
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5095
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5096
5097
                                // Calculating the area
5098
                                $poly_user = convert_coordinates($user_answer, '/');
5099
                                $poly_answer = convert_coordinates($answer_question, '|');
5100
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5101
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5102
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5103
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5104
5105
                                $overlap = $poly_results['both'];
5106
                                $poly_answer_area = $poly_results['s1'];
5107
                                $poly_user_area = $poly_results['s2'];
5108
                                $missing = $poly_results['s1Only'];
5109
                                $excess = $poly_results['s2Only'];
5110
5111
                                // //this is an area in pixels
5112
                                if ($debug > 0) {
5113
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5114
                                }
5115
5116
                                if ($overlap < 1) {
5117
                                    // Shortcut to avoid complicated calculations
5118
                                    $final_overlap = 0;
5119
                                    $final_missing = 100;
5120
                                    $final_excess = 100;
5121
                                } else {
5122
                                    // the final overlap is the percentage of the initial polygon
5123
                                    // that is overlapped by the user's polygon
5124
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5125
                                    if ($debug > 1) {
5126
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap, 0);
5127
                                    }
5128
                                    // the final missing area is the percentage of the initial polygon
5129
                                    // that is not overlapped by the user's polygon
5130
                                    $final_missing = 100 - $final_overlap;
5131
                                    if ($debug > 1) {
5132
                                        error_log(__LINE__.' - Final missing is '.$final_missing, 0);
5133
                                    }
5134
                                    // the final excess area is the percentage of the initial polygon's size
5135
                                    // that is covered by the user's polygon outside of the initial polygon
5136
                                    $final_excess = round(
5137
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5138
                                    );
5139
                                    if ($debug > 1) {
5140
                                        error_log(__LINE__.' - Final excess is '.$final_excess, 0);
5141
                                    }
5142
                                }
5143
5144
                                // Checking the destination parameters parsing the "@@"
5145
                                $destination_items = explode('@@', $answerDestination);
5146
                                $threadhold_total = $destination_items[0];
5147
                                $threadhold_items = explode(';', $threadhold_total);
5148
                                $threadhold1 = $threadhold_items[0]; // overlap
5149
                                $threadhold2 = $threadhold_items[1]; // excess
5150
                                $threadhold3 = $threadhold_items[2]; // missing
5151
5152
                                // if is delineation
5153
                                if (1 === $answerId) {
5154
                                    //setting colors
5155
                                    if ($final_overlap >= $threadhold1) {
5156
                                        $overlap_color = true;
5157
                                    }
5158
                                    if ($final_excess <= $threadhold2) {
5159
                                        $excess_color = true;
5160
                                    }
5161
                                    if ($final_missing <= $threadhold3) {
5162
                                        $missing_color = true;
5163
                                    }
5164
5165
                                    // if pass
5166
                                    if ($final_overlap >= $threadhold1 &&
5167
                                        $final_missing <= $threadhold3 &&
5168
                                        $final_excess <= $threadhold2
5169
                                    ) {
5170
                                        $next = 1; //go to the oars
5171
                                        $result_comment = get_lang('Acceptable');
5172
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5173
                                    } else {
5174
                                        $next = 0;
5175
                                        $result_comment = get_lang('Unacceptable');
5176
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5177
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5178
                                        // checking the destination parameters parsing the "@@"
5179
                                        $destination_items = explode('@@', $answerDestination);
5180
                                    }
5181
                                } elseif ($answerId > 1) {
5182
                                    if ('noerror' == $objAnswerTmp->selectHotspotType($answerId)) {
5183
                                        if ($debug > 0) {
5184
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5185
                                        }
5186
                                        //type no error shouldn't be treated
5187
                                        $next = 1;
5188
5189
                                        continue;
5190
                                    }
5191
                                    if ($debug > 0) {
5192
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5193
                                    }
5194
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5195
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5196
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5197
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5198
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5199
5200
                                    if (false == $overlap) {
5201
                                        //all good, no overlap
5202
                                        $next = 1;
5203
5204
                                        continue;
5205
                                    } else {
5206
                                        if ($debug > 0) {
5207
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5208
                                        }
5209
                                        $organs_at_risk_hit++;
5210
                                        //show the feedback
5211
                                        $next = 0;
5212
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5213
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5214
5215
                                        $destination_items = explode('@@', $answerDestination);
5216
                                        $try_hotspot = $destination_items[1];
5217
                                        $lp_hotspot = $destination_items[2];
5218
                                        $select_question_hotspot = $destination_items[3];
5219
                                        $url_hotspot = $destination_items[4];
5220
                                    }
5221
                                }
5222
                            } else {
5223
                                // the first delineation feedback
5224
                                if ($debug > 0) {
5225
                                    error_log(__LINE__.' first', 0);
5226
                                }
5227
                            }
5228
                        } elseif (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
5229
                            echo '<tr>';
5230
                            echo Display::tag('td', $answerMatching[$answerId]);
5231
                            echo Display::tag(
5232
                                'td',
5233
                                "$user_answer / ".Display::tag(
5234
                                    'strong',
5235
                                    $answerMatching[$answerCorrect],
5236
                                    ['style' => 'color: #008000; font-weight: bold;']
5237
                                )
5238
                            );
5239
                            echo '</tr>';
5240
                        } elseif (ANNOTATION == $answerType) {
5241
                            ExerciseShowFunctions::displayAnnotationAnswer(
5242
                                $feedback_type,
5243
                                $exeId,
5244
                                $questionId,
5245
                                $questionScore,
5246
                                $results_disabled
5247
                            );
5248
                        }
5249
                    }
5250
                } else {
5251
                    if ($debug) {
5252
                        error_log('Showing questions $from '.$from);
5253
                    }
5254
5255
                    switch ($answerType) {
5256
                        case UNIQUE_ANSWER:
5257
                        case UNIQUE_ANSWER_IMAGE:
5258
                        case UNIQUE_ANSWER_NO_OPTION:
5259
                        case MULTIPLE_ANSWER:
5260
                        case GLOBAL_MULTIPLE_ANSWER:
5261
                        case MULTIPLE_ANSWER_COMBINATION:
5262
                        case READING_COMPREHENSION:
5263
                            if (1 == $answerId) {
5264
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5265
                                    $this,
5266
                                    $feedback_type,
5267
                                    $answerType,
5268
                                    $studentChoice,
5269
                                    $answer,
5270
                                    $answerComment,
5271
                                    $answerCorrect,
5272
                                    $exeId,
5273
                                    $questionId,
5274
                                    $answerId,
5275
                                    $results_disabled,
5276
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5277
                                    $this->export
5278
                                );
5279
                            } else {
5280
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5281
                                    $this,
5282
                                    $feedback_type,
5283
                                    $answerType,
5284
                                    $studentChoice,
5285
                                    $answer,
5286
                                    $answerComment,
5287
                                    $answerCorrect,
5288
                                    $exeId,
5289
                                    $questionId,
5290
                                    '',
5291
                                    $results_disabled,
5292
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5293
                                    $this->export
5294
                                );
5295
                            }
5296
5297
                            break;
5298
                        case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
5299
                            if (1 == $answerId) {
5300
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5301
                                    $this,
5302
                                    $feedback_type,
5303
                                    $answerType,
5304
                                    $studentChoice,
5305
                                    $answer,
5306
                                    $answerComment,
5307
                                    $answerCorrect,
5308
                                    $exeId,
5309
                                    $questionId,
5310
                                    $answerId,
5311
                                    $results_disabled,
5312
                                    $showTotalScoreAndUserChoicesInLastAttempt
5313
                                );
5314
                            } else {
5315
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5316
                                    $this,
5317
                                    $feedback_type,
5318
                                    $answerType,
5319
                                    $studentChoice,
5320
                                    $answer,
5321
                                    $answerComment,
5322
                                    $answerCorrect,
5323
                                    $exeId,
5324
                                    $questionId,
5325
                                    '',
5326
                                    $results_disabled,
5327
                                    $showTotalScoreAndUserChoicesInLastAttempt
5328
                                );
5329
                            }
5330
5331
                            break;
5332
                        case MULTIPLE_ANSWER_TRUE_FALSE:
5333
                            if (1 == $answerId) {
5334
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5335
                                    $this,
5336
                                    $feedback_type,
5337
                                    $answerType,
5338
                                    $studentChoice,
5339
                                    $answer,
5340
                                    $answerComment,
5341
                                    $answerCorrect,
5342
                                    $exeId,
5343
                                    $questionId,
5344
                                    $answerId,
5345
                                    $results_disabled,
5346
                                    $showTotalScoreAndUserChoicesInLastAttempt
5347
                                );
5348
                            } else {
5349
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5350
                                    $this,
5351
                                    $feedback_type,
5352
                                    $answerType,
5353
                                    $studentChoice,
5354
                                    $answer,
5355
                                    $answerComment,
5356
                                    $answerCorrect,
5357
                                    $exeId,
5358
                                    $questionId,
5359
                                    '',
5360
                                    $results_disabled,
5361
                                    $showTotalScoreAndUserChoicesInLastAttempt
5362
                                );
5363
                            }
5364
5365
                            break;
5366
                        case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
5367
                            if (1 == $answerId) {
5368
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5369
                                    $this,
5370
                                    $feedback_type,
5371
                                    $studentChoice,
5372
                                    $studentChoiceDegree,
5373
                                    $answer,
5374
                                    $answerComment,
5375
                                    $answerCorrect,
5376
                                    $questionId,
5377
                                    $results_disabled
5378
                                );
5379
                            } else {
5380
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5381
                                    $this,
5382
                                    $feedback_type,
5383
                                    $studentChoice,
5384
                                    $studentChoiceDegree,
5385
                                    $answer,
5386
                                    $answerComment,
5387
                                    $answerCorrect,
5388
                                    $questionId,
5389
                                    $results_disabled
5390
                                );
5391
                            }
5392
5393
                            break;
5394
                        case FILL_IN_BLANKS:
5395
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5396
                                $this,
5397
                                $feedback_type,
5398
                                $answer,
5399
                                $exeId,
5400
                                $questionId,
5401
                                $results_disabled,
5402
                                $showTotalScoreAndUserChoicesInLastAttempt,
5403
                                $str
5404
                            );
5405
5406
                            break;
5407
                        case CALCULATED_ANSWER:
5408
                            ExerciseShowFunctions::display_calculated_answer(
5409
                                $this,
5410
                                $feedback_type,
5411
                                $answer,
5412
                                $exeId,
5413
                                $questionId,
5414
                                $results_disabled,
5415
                                '',
5416
                                $showTotalScoreAndUserChoicesInLastAttempt
5417
                            );
5418
5419
                            break;
5420
                        case FREE_ANSWER:
5421
                            echo ExerciseShowFunctions::display_free_answer(
5422
                                $feedback_type,
5423
                                $choice,
5424
                                $exeId,
5425
                                $questionId,
5426
                                $questionScore,
5427
                                $results_disabled
5428
                            );
5429
5430
                            break;
5431
                        case ORAL_EXPRESSION:
5432
                            /** @var OralExpression $objQuestionTmp */
5433
                            echo '<tr>
5434
                                <td valign="top">'.
5435
                                ExerciseShowFunctions::display_oral_expression_answer(
5436
                                    $feedback_type,
5437
                                    $choice,
5438
                                    $exeId,
5439
                                    $questionId,
5440
                                    $results_disabled,
5441
                                    $questionScore
5442
                                ).'</td>
5443
                                </tr>
5444
                                </table>';
5445
                            break;
5446
                        case HOT_SPOT:
5447
                            $correctAnswerId = 0;
5448
5449
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5450
                                if ($hotspot->getHotspotAnswerId() == $foundAnswerId) {
5451
                                    break;
5452
                                }
5453
                            }
5454
                            ExerciseShowFunctions::display_hotspot_answer(
5455
                                $this,
5456
                                $feedback_type,
5457
                                $answerId,
5458
                                $answer,
5459
                                $studentChoice,
5460
                                $answerComment,
5461
                                $results_disabled,
5462
                                $answerId,
5463
                                $showTotalScoreAndUserChoicesInLastAttempt
5464
                            );
5465
5466
                            break;
5467
                        case HOT_SPOT_DELINEATION:
5468
                            $user_answer = $user_array;
5469
                            if ($next) {
5470
                                $user_answer = $user_array;
5471
                                // we compare only the delineation not the other points
5472
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5473
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5474
5475
                                // calculating the area
5476
                                $poly_user = convert_coordinates($user_answer, '/');
5477
                                $poly_answer = convert_coordinates($answer_question, '|');
5478
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5479
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5480
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5481
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5482
5483
                                $overlap = $poly_results['both'];
5484
                                $poly_answer_area = $poly_results['s1'];
5485
                                $poly_user_area = $poly_results['s2'];
5486
                                $missing = $poly_results['s1Only'];
5487
                                $excess = $poly_results['s2Only'];
5488
                                if ($debug > 0) {
5489
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5490
                                }
5491
                                if ($overlap < 1) {
5492
                                    //shortcut to avoid complicated calculations
5493
                                    $final_overlap = 0;
5494
                                    $final_missing = 100;
5495
                                    $final_excess = 100;
5496
                                } else {
5497
                                    // the final overlap is the percentage of the initial polygon
5498
                                    // that is overlapped by the user's polygon
5499
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5500
5501
                                    // the final missing area is the percentage of the initial polygon that
5502
                                    // is not overlapped by the user's polygon
5503
                                    $final_missing = 100 - $final_overlap;
5504
                                    // the final excess area is the percentage of the initial polygon's size that is
5505
                                    // covered by the user's polygon outside of the initial polygon
5506
                                    $final_excess = round(
5507
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5508
                                    );
5509
5510
                                    if ($debug > 1) {
5511
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap);
5512
                                        error_log(__LINE__.' - Final excess is '.$final_excess);
5513
                                        error_log(__LINE__.' - Final missing is '.$final_missing);
5514
                                    }
5515
                                }
5516
5517
                                // Checking the destination parameters parsing the "@@"
5518
                                $destination_items = explode('@@', $answerDestination);
5519
                                $threadhold_total = $destination_items[0];
5520
                                $threadhold_items = explode(';', $threadhold_total);
5521
                                $threadhold1 = $threadhold_items[0]; // overlap
5522
                                $threadhold2 = $threadhold_items[1]; // excess
5523
                                $threadhold3 = $threadhold_items[2]; //missing
5524
                                // if is delineation
5525
                                if (1 === $answerId) {
5526
                                    //setting colors
5527
                                    if ($final_overlap >= $threadhold1) {
5528
                                        $overlap_color = true;
5529
                                    }
5530
                                    if ($final_excess <= $threadhold2) {
5531
                                        $excess_color = true;
5532
                                    }
5533
                                    if ($final_missing <= $threadhold3) {
5534
                                        $missing_color = true;
5535
                                    }
5536
5537
                                    // if pass
5538
                                    if ($final_overlap >= $threadhold1 &&
5539
                                        $final_missing <= $threadhold3 &&
5540
                                        $final_excess <= $threadhold2
5541
                                    ) {
5542
                                        $next = 1; //go to the oars
5543
                                        $result_comment = get_lang('Acceptable');
5544
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5545
                                    } else {
5546
                                        $next = 0;
5547
                                        $result_comment = get_lang('Unacceptable');
5548
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5549
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5550
                                        //checking the destination parameters parsing the "@@"
5551
                                        $destination_items = explode('@@', $answerDestination);
5552
                                    }
5553
                                } elseif ($answerId > 1) {
5554
                                    if ('noerror' === $objAnswerTmp->selectHotspotType($answerId)) {
5555
                                        if ($debug > 0) {
5556
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5557
                                        }
5558
                                        //type no error shouldn't be treated
5559
                                        $next = 1;
5560
5561
                                        break;
5562
                                    }
5563
                                    if ($debug > 0) {
5564
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5565
                                    }
5566
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5567
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5568
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5569
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5570
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5571
5572
                                    if (false == $overlap) {
5573
                                        //all good, no overlap
5574
                                        $next = 1;
5575
5576
                                        break;
5577
                                    } else {
5578
                                        if ($debug > 0) {
5579
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5580
                                        }
5581
                                        $organs_at_risk_hit++;
5582
                                        //show the feedback
5583
                                        $next = 0;
5584
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5585
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5586
5587
                                        $destination_items = explode('@@', $answerDestination);
5588
                                        $try_hotspot = $destination_items[1];
5589
                                        $lp_hotspot = $destination_items[2];
5590
                                        $select_question_hotspot = $destination_items[3];
5591
                                        $url_hotspot = $destination_items[4];
5592
                                    }
5593
                                }
5594
                            }
5595
5596
                            break;
5597
                        case HOT_SPOT_ORDER:
5598
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5599
                                $feedback_type,
5600
                                $answerId,
5601
                                $answer,
5602
                                $studentChoice,
5603
                                $answerComment
5604
                            );*/
5605
5606
                            break;
5607
                        case DRAGGABLE:
5608
                        case MATCHING_DRAGGABLE:
5609
                        case MATCHING:
5610
                            echo '<tr>';
5611
                            echo Display::tag('td', $answerMatching[$answerId]);
5612
                            echo Display::tag(
5613
                                'td',
5614
                                "$user_answer / ".Display::tag(
5615
                                    'strong',
5616
                                    $answerMatching[$answerCorrect],
5617
                                    ['style' => 'color: #008000; font-weight: bold;']
5618
                                )
5619
                            );
5620
                            echo '</tr>';
5621
5622
                            break;
5623
                        case ANNOTATION:
5624
                            ExerciseShowFunctions::displayAnnotationAnswer(
5625
                                $feedback_type,
5626
                                $exeId,
5627
                                $questionId,
5628
                                $questionScore,
5629
                                $results_disabled
5630
                            );
5631
5632
                            break;
5633
                    }
5634
                }
5635
            }
5636
        } // end for that loops over all answers of the current question
5637
5638
        if ($debug) {
5639
            error_log('-- End answer loop --');
5640
        }
5641
5642
        $final_answer = true;
5643
5644
        foreach ($real_answers as $my_answer) {
5645
            if (!$my_answer) {
5646
                $final_answer = false;
5647
            }
5648
        }
5649
5650
        //we add the total score after dealing with the answers
5651
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
5652
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
5653
        ) {
5654
            if ($final_answer) {
5655
                //getting only the first score where we save the weight of all the question
5656
                $answerWeighting = $objAnswerTmp->selectWeighting(1);
5657
                if (empty($answerWeighting) && !empty($firstAnswer) && isset($firstAnswer['ponderation'])) {
5658
                    $answerWeighting = $firstAnswer['ponderation'];
5659
                }
5660
                $questionScore += $answerWeighting;
5661
            }
5662
        }
5663
5664
        $extra_data = [
5665
            'final_overlap' => $final_overlap,
5666
            'final_missing' => $final_missing,
5667
            'final_excess' => $final_excess,
5668
            'overlap_color' => $overlap_color,
5669
            'missing_color' => $missing_color,
5670
            'excess_color' => $excess_color,
5671
            'threadhold1' => $threadhold1,
5672
            'threadhold2' => $threadhold2,
5673
            'threadhold3' => $threadhold3,
5674
        ];
5675
5676
        if ('exercise_result' === $from) {
5677
            // if answer is hotspot. To the difference of exercise_show.php,
5678
            //  we use the results from the session (from_db=0)
5679
            // TODO Change this, because it is wrong to show the user
5680
            //  some results that haven't been stored in the database yet
5681
            if (HOT_SPOT == $answerType || HOT_SPOT_ORDER == $answerType || HOT_SPOT_DELINEATION == $answerType) {
5682
                if ($debug) {
5683
                    error_log('$from AND this is a hotspot kind of question ');
5684
                }
5685
                if (HOT_SPOT_DELINEATION === $answerType) {
5686
                    if ($showHotSpotDelineationTable) {
5687
                        if (!is_numeric($final_overlap)) {
5688
                            $final_overlap = 0;
5689
                        }
5690
                        if (!is_numeric($final_missing)) {
5691
                            $final_missing = 0;
5692
                        }
5693
                        if (!is_numeric($final_excess)) {
5694
                            $final_excess = 0;
5695
                        }
5696
5697
                        if ($final_overlap > 100) {
5698
                            $final_overlap = 100;
5699
                        }
5700
5701
                        $overlap = 0;
5702
                        if ($final_overlap > 0) {
5703
                            $overlap = (int) $final_overlap;
5704
                        }
5705
5706
                        $excess = 0;
5707
                        if ($final_excess > 0) {
5708
                            $excess = (int) $final_excess;
5709
                        }
5710
5711
                        $missing = 0;
5712
                        if ($final_missing > 0) {
5713
                            $missing = (int) $final_missing;
5714
                        }
5715
5716
                        $table_resume = '<table class="table table-hover table-striped data_table">
5717
                                <tr class="row_odd" >
5718
                                    <td></td>
5719
                                    <td ><b>'.get_lang('Requirements').'</b></td>
5720
                                    <td><b>'.get_lang('Your answer').'</b></td>
5721
                                </tr>
5722
                                <tr class="row_even">
5723
                                    <td><b>'.get_lang('Overlapping areaping area').'</b></td>
5724
                                    <td>'.get_lang('Minimum').' '.$threadhold1.'</td>
5725
                                    <td class="text-right '.($overlap_color ? 'text-success' : 'text-error').'">'
5726
                                    .$overlap.'</td>
5727
                                </tr>
5728
                                <tr>
5729
                                    <td><b>'.get_lang('Excessive areaive area').'</b></td>
5730
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold2.'</td>
5731
                                    <td class="text-right '.($excess_color ? 'text-success' : 'text-error').'">'
5732
                                    .$excess.'</td>
5733
                                </tr>
5734
                                <tr class="row_even">
5735
                                    <td><b>'.get_lang('Missing area area').'</b></td>
5736
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold3.'</td>
5737
                                    <td class="text-right '.($missing_color ? 'text-success' : 'text-error').'">'
5738
                                    .$missing.'</td>
5739
                                </tr>
5740
                            </table>';
5741
                        if (0 == $next) {
5742
                        } else {
5743
                            $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
5744
                            $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
5745
                        }
5746
5747
                        $message = '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>
5748
                                    <p style="text-align:center">';
5749
                        $message .= '<p>'.get_lang('Your delineation :').'</p>';
5750
                        $message .= $table_resume;
5751
                        $message .= '<br />'.get_lang('Your result is :').' '.$result_comment.'<br />';
5752
                        if ($organs_at_risk_hit > 0) {
5753
                            $message .= '<p><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
5754
                        }
5755
                        $message .= '<p>'.$comment.'</p>';
5756
                        echo $message;
5757
5758
                        $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][0] = $message;
5759
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
5760
                            $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][1] = $_SESSION['exerciseResultCoordinates'][$questionId];
5761
                        }
5762
                    } else {
5763
                        echo $hotspot_delineation_result[0] ?? '';
5764
                    }
5765
5766
                    // Save the score attempts
5767
                    if (1) {
5768
                        //getting the answer 1 or 0 comes from exercise_submit_modal.php
5769
                        $final_answer = $hotspot_delineation_result[1] ?? '';
5770
                        if (0 == $final_answer) {
5771
                            $questionScore = 0;
5772
                        }
5773
                        // we always insert the answer_id 1 = delineation
5774
                        Event::saveQuestionAttempt($this, $questionScore, 1, $quesId, $exeId, 0);
5775
                        //in delineation mode, get the answer from $hotspot_delineation_result[1]
5776
                        $hotspotValue = isset($hotspot_delineation_result[1]) ? 1 === (int) $hotspot_delineation_result[1] ? 1 : 0 : 0;
5777
                        Event::saveExerciseAttemptHotspot(
5778
                            $this,
5779
                            $exeId,
5780
                            $quesId,
5781
                            1,
5782
                            $hotspotValue,
5783
                            $exerciseResultCoordinates[$quesId] ?? '',
5784
                            false,
5785
                            0,
5786
                            $learnpath_id,
5787
                            $learnpath_item_id
5788
                        );
5789
                    } else {
5790
                        if (0 == $final_answer) {
5791
                            $questionScore = 0;
5792
                            $answer = 0;
5793
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
5794
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5795
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5796
                                    Event::saveExerciseAttemptHotspot(
5797
                                        $this,
5798
                                        $exeId,
5799
                                        $quesId,
5800
                                        $idx,
5801
                                        0,
5802
                                        $val,
5803
                                        false,
5804
                                        0,
5805
                                        $learnpath_id,
5806
                                        $learnpath_item_id
5807
                                    );
5808
                                }
5809
                            }
5810
                        } else {
5811
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
5812
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5813
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5814
                                    $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
5815
                                    Event::saveExerciseAttemptHotspot(
5816
                                        $this,
5817
                                        $exeId,
5818
                                        $quesId,
5819
                                        $idx,
5820
                                        $hotspotValue,
5821
                                        $val,
5822
                                        false,
5823
                                        0,
5824
                                        $learnpath_id,
5825
                                        $learnpath_item_id
5826
                                    );
5827
                                }
5828
                            }
5829
                        }
5830
                    }
5831
                }
5832
            }
5833
5834
            $relPath = api_get_path(WEB_CODE_PATH);
5835
5836
            if (HOT_SPOT == $answerType || HOT_SPOT_ORDER == $answerType) {
5837
                // We made an extra table for the answers
5838
                if ($show_result) {
5839
                    echo '</table></td></tr>';
5840
                    echo '
5841
                        <tr>
5842
                            <td colspan="2">
5843
                                <p><em>'.get_lang('Image zones')."</em></p>
5844
                                <div id=\"hotspot-solution-$questionId\"></div>
5845
                                <script>
5846
                                    $(function() {
5847
                                        new HotspotQuestion({
5848
                                            questionId: $questionId,
5849
                                            exerciseId: {$this->getId()},
5850
                                            exeId: $exeId,
5851
                                            selector: '#hotspot-solution-$questionId',
5852
                                            for: 'solution',
5853
                                            relPath: '$relPath'
5854
                                        });
5855
                                    });
5856
                                </script>
5857
                            </td>
5858
                        </tr>
5859
                    ";
5860
                }
5861
            } elseif (ANNOTATION == $answerType) {
5862
                if ($show_result) {
5863
                    echo '
5864
                        <p><em>'.get_lang('Annotation').'</em></p>
5865
                        <div id="annotation-canvas-'.$questionId.'"></div>
5866
                        <script>
5867
                            AnnotationQuestion({
5868
                                questionId: parseInt('.$questionId.'),
5869
                                exerciseId: parseInt('.$exeId.'),
5870
                                relPath: \''.$relPath.'\',
5871
                                courseId: parseInt('.$course_id.')
5872
                            });
5873
                        </script>
5874
                    ';
5875
                }
5876
            }
5877
5878
            if ($show_result && ANNOTATION != $answerType) {
5879
                echo '</table>';
5880
            }
5881
        }
5882
        unset($objAnswerTmp);
5883
5884
        $totalWeighting += $questionWeighting;
5885
        // Store results directly in the database
5886
        // For all in one page exercises, the results will be
5887
        // stored by exercise_results.php (using the session)
5888
        if ($save_results) {
5889
            if ($debug) {
5890
                error_log("Save question results $save_results");
5891
                error_log("Question score: $questionScore");
5892
                error_log('choice: ');
5893
                error_log(print_r($choice, 1));
5894
            }
5895
5896
            if (empty($choice)) {
5897
                $choice = 0;
5898
            }
5899
            // with certainty degree
5900
            if (empty($choiceDegreeCertainty)) {
5901
                $choiceDegreeCertainty = 0;
5902
            }
5903
            if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
5904
                MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType ||
5905
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
5906
            ) {
5907
                if (0 != $choice) {
5908
                    $reply = array_keys($choice);
5909
                    $countReply = count($reply);
5910
                    for ($i = 0; $i < $countReply; $i++) {
5911
                        $chosenAnswer = $reply[$i];
5912
                        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
5913
                            if (0 != $choiceDegreeCertainty) {
5914
                                $replyDegreeCertainty = array_keys($choiceDegreeCertainty);
5915
                                $answerDegreeCertainty = isset($replyDegreeCertainty[$i]) ? $replyDegreeCertainty[$i] : '';
5916
                                $answerValue = isset($choiceDegreeCertainty[$answerDegreeCertainty]) ? $choiceDegreeCertainty[$answerDegreeCertainty] : '';
5917
                                Event::saveQuestionAttempt(
5918
                                    $this,
5919
                                    $questionScore,
5920
                                    $chosenAnswer.':'.$choice[$chosenAnswer].':'.$answerValue,
5921
                                    $quesId,
5922
                                    $exeId,
5923
                                    $i,
5924
                                    $this->getId(),
5925
                                    $updateResults,
5926
                                    $questionDuration
5927
                                );
5928
                            }
5929
                        } else {
5930
                            Event::saveQuestionAttempt(
5931
                                $this,
5932
                                $questionScore,
5933
                                $chosenAnswer.':'.$choice[$chosenAnswer],
5934
                                $quesId,
5935
                                $exeId,
5936
                                $i,
5937
                                $this->getId(),
5938
                                $updateResults,
5939
                                $questionDuration
5940
                            );
5941
                        }
5942
                        if ($debug) {
5943
                            error_log('result =>'.$questionScore.' '.$chosenAnswer.':'.$choice[$chosenAnswer]);
5944
                        }
5945
                    }
5946
                } else {
5947
                    Event::saveQuestionAttempt(
5948
                        $this,
5949
                        $questionScore,
5950
                        0,
5951
                        $quesId,
5952
                        $exeId,
5953
                        0,
5954
                        $this->getId(),
5955
                        false,
5956
                        $questionDuration
5957
                    );
5958
                }
5959
            } elseif (MULTIPLE_ANSWER == $answerType || GLOBAL_MULTIPLE_ANSWER == $answerType) {
5960
                if (0 != $choice) {
5961
                    $reply = array_keys($choice);
5962
                    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...
5963
                        $ans = $reply[$i];
5964
                        Event::saveQuestionAttempt(
5965
                            $this,
5966
                            $questionScore,
5967
                            $ans,
5968
                            $quesId,
5969
                            $exeId,
5970
                            $i,
5971
                            $this->id,
5972
                            false,
5973
                            $questionDuration
5974
                        );
5975
                    }
5976
                } else {
5977
                    Event::saveQuestionAttempt(
5978
                        $this,
5979
                        $questionScore,
5980
                        0,
5981
                        $quesId,
5982
                        $exeId,
5983
                        0,
5984
                        $this->id,
5985
                        false,
5986
                        $questionDuration
5987
                    );
5988
                }
5989
            } elseif (MULTIPLE_ANSWER_COMBINATION == $answerType) {
5990
                if (0 != $choice) {
5991
                    $reply = array_keys($choice);
5992
                    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...
5993
                        $ans = $reply[$i];
5994
                        Event::saveQuestionAttempt(
5995
                            $this,
5996
                            $questionScore,
5997
                            $ans,
5998
                            $quesId,
5999
                            $exeId,
6000
                            $i,
6001
                            $this->id,
6002
                            false,
6003
                            $questionDuration
6004
                        );
6005
                    }
6006
                } else {
6007
                    Event::saveQuestionAttempt(
6008
                        $this,
6009
                        $questionScore,
6010
                        0,
6011
                        $quesId,
6012
                        $exeId,
6013
                        0,
6014
                        $this->id,
6015
                        false,
6016
                        $questionDuration
6017
                    );
6018
                }
6019
            } elseif (in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE])) {
6020
                if (isset($matching)) {
6021
                    foreach ($matching as $j => $val) {
6022
                        Event::saveQuestionAttempt(
6023
                            $this,
6024
                            $questionScore,
6025
                            $val,
6026
                            $quesId,
6027
                            $exeId,
6028
                            $j,
6029
                            $this->id,
6030
                            false,
6031
                            $questionDuration
6032
                        );
6033
                    }
6034
                }
6035
            } elseif (FREE_ANSWER == $answerType) {
6036
                $answer = $choice;
6037
                Event::saveQuestionAttempt(
6038
                    $this,
6039
                    $questionScore,
6040
                    $answer,
6041
                    $quesId,
6042
                    $exeId,
6043
                    0,
6044
                    $this->id,
6045
                    false,
6046
                    $questionDuration
6047
                );
6048
            } elseif (ORAL_EXPRESSION == $answerType) {
6049
                $answer = $choice;
6050
                /** @var OralExpression $objQuestionTmp */
6051
                $questionAttemptId = Event::saveQuestionAttempt(
6052
                    $this,
6053
                    $questionScore,
6054
                    $answer,
6055
                    $quesId,
6056
                    $exeId,
6057
                    0,
6058
                    $this->id,
6059
                    false,
6060
                    $questionDuration
6061
                );
6062
6063
                if (false !== $questionAttemptId) {
6064
                    OralExpression::saveAssetInQuestionAttempt($questionAttemptId);
6065
                }
6066
            } elseif (
6067
            in_array(
6068
                $answerType,
6069
                [UNIQUE_ANSWER, UNIQUE_ANSWER_IMAGE, UNIQUE_ANSWER_NO_OPTION, READING_COMPREHENSION]
6070
            )
6071
            ) {
6072
                $answer = $choice;
6073
                Event::saveQuestionAttempt(
6074
                    $this,
6075
                    $questionScore,
6076
                    $answer,
6077
                    $quesId,
6078
                    $exeId,
6079
                    0,
6080
                    $this->id,
6081
                    false,
6082
                    $questionDuration
6083
                );
6084
            } elseif (HOT_SPOT == $answerType || ANNOTATION == $answerType) {
6085
                $answer = [];
6086
                if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) {
6087
                    if ($debug) {
6088
                        error_log('Checking result coordinates');
6089
                    }
6090
                    Database::delete(
6091
                        Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
6092
                        [
6093
                            'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
6094
                                $exeId,
6095
                                $questionId,
6096
                                api_get_course_int_id(),
6097
                            ],
6098
                        ]
6099
                    );
6100
6101
                    foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
6102
                        $answer[] = $val;
6103
                        $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
6104
                        if ($debug) {
6105
                            error_log('Hotspot value: '.$hotspotValue);
6106
                        }
6107
                        Event::saveExerciseAttemptHotspot(
6108
                            $this,
6109
                            $exeId,
6110
                            $quesId,
6111
                            $idx,
6112
                            $hotspotValue,
6113
                            $val,
6114
                            false,
6115
                            $this->id,
6116
                            $learnpath_id,
6117
                            $learnpath_item_id
6118
                        );
6119
                    }
6120
                } else {
6121
                    if ($debug) {
6122
                        error_log('Empty: exerciseResultCoordinates');
6123
                    }
6124
                }
6125
                Event::saveQuestionAttempt(
6126
                    $this,
6127
                    $questionScore,
6128
                    implode('|', $answer),
6129
                    $quesId,
6130
                    $exeId,
6131
                    0,
6132
                    $this->id,
6133
                    false,
6134
                    $questionDuration
6135
                );
6136
            } else {
6137
                Event::saveQuestionAttempt(
6138
                    $this,
6139
                    $questionScore,
6140
                    $answer,
6141
                    $quesId,
6142
                    $exeId,
6143
                    0,
6144
                    $this->id,
6145
                    false,
6146
                    $questionDuration
6147
                );
6148
            }
6149
        }
6150
6151
        if (0 == $propagate_neg && $questionScore < 0) {
6152
            $questionScore = 0;
6153
        }
6154
6155
        if ($save_results) {
6156
            $statsTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6157
            $sql = "UPDATE $statsTable SET
6158
                        score = score + ".(float) $questionScore."
6159
                    WHERE exe_id = $exeId";
6160
            Database::query($sql);
6161
        }
6162
6163
        return [
6164
            'score' => $questionScore,
6165
            'weight' => $questionWeighting,
6166
            'extra' => $extra_data,
6167
            'open_question' => $arrques,
6168
            'open_answer' => $arrans,
6169
            'answer_type' => $answerType,
6170
            'generated_oral_file' => $generatedFilesHtml,
6171
            'user_answered' => $userAnsweredQuestion,
6172
            'correct_answer_id' => $correctAnswerId,
6173
            'answer_destination' => $answerDestination,
6174
        ];
6175
    }
6176
6177
    /**
6178
     * Sends a notification when a user ends an examn.
6179
     *
6180
     * @param string $type                  'start' or 'end' of an exercise
6181
     * @param array  $question_list_answers
6182
     * @param string $origin
6183
     * @param int    $exe_id
6184
     * @param float  $score
6185
     * @param float  $weight
6186
     *
6187
     * @return bool
6188
     */
6189
    public function send_mail_notification_for_exam(
6190
        $type,
6191
        $question_list_answers,
6192
        $origin,
6193
        $exe_id,
6194
        $score = null,
6195
        $weight = null
6196
    ) {
6197
        $setting = api_get_course_setting('email_alert_manager_on_new_quiz');
6198
6199
        if ((empty($setting) || !is_array($setting)) && empty($this->getNotifications())) {
6200
            return false;
6201
        }
6202
6203
        $settingFromExercise = $this->getNotifications();
6204
        if (!empty($settingFromExercise)) {
6205
            $setting = $settingFromExercise;
6206
        }
6207
6208
        // Email configuration settings
6209
        $courseCode = api_get_course_id();
6210
        $courseInfo = api_get_course_info($courseCode);
6211
6212
        if (empty($courseInfo)) {
6213
            return false;
6214
        }
6215
6216
        $sessionId = api_get_session_id();
6217
6218
        $sessionData = '';
6219
        if (!empty($sessionId)) {
6220
            $sessionInfo = api_get_session_info($sessionId);
6221
            if (!empty($sessionInfo)) {
6222
                $sessionData = '<tr>'
6223
                    .'<td>'.get_lang('Session name').'</td>'
6224
                    .'<td>'.$sessionInfo['name'].'</td>'
6225
                    .'</tr>';
6226
            }
6227
        }
6228
6229
        $sendStart = false;
6230
        $sendEnd = false;
6231
        $sendEndOpenQuestion = false;
6232
        $sendEndOralQuestion = false;
6233
6234
        foreach ($setting as $option) {
6235
            switch ($option) {
6236
                case 0:
6237
                    return false;
6238
6239
                    break;
0 ignored issues
show
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...
6240
                case 1: // End
6241
                    if ('end' == $type) {
6242
                        $sendEnd = true;
6243
                    }
6244
6245
                    break;
6246
                case 2: // start
6247
                    if ('start' == $type) {
6248
                        $sendStart = true;
6249
                    }
6250
6251
                    break;
6252
                case 3: // end + open
6253
                    if ('end' == $type) {
6254
                        $sendEndOpenQuestion = true;
6255
                    }
6256
6257
                    break;
6258
                case 4: // end + oral
6259
                    if ('end' == $type) {
6260
                        $sendEndOralQuestion = true;
6261
                    }
6262
6263
                    break;
6264
            }
6265
        }
6266
6267
        $user_info = api_get_user_info(api_get_user_id());
6268
        $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_show.php?'.
6269
            api_get_cidreq(true, true, 'qualify').'&id='.$exe_id.'&action=qualify';
6270
6271
        if (!empty($sessionId)) {
6272
            $addGeneralCoach = true;
6273
            $setting = ('true' === api_get_setting('exercise.block_quiz_mail_notification_general_coach'));
6274
            if (true === $setting) {
6275
                $addGeneralCoach = false;
6276
            }
6277
            $teachers = CourseManager::get_coach_list_from_course_code(
6278
                $courseCode,
6279
                $sessionId,
6280
                $addGeneralCoach
6281
            );
6282
        } else {
6283
            $teachers = CourseManager::get_teacher_list_from_course_code($courseCode);
6284
        }
6285
6286
        if ($sendEndOpenQuestion) {
6287
            $this->sendNotificationForOpenQuestions(
6288
                $question_list_answers,
6289
                $origin,
6290
                $user_info,
6291
                $url,
6292
                $teachers
6293
            );
6294
        }
6295
6296
        if ($sendEndOralQuestion) {
6297
            $this->sendNotificationForOralQuestions(
6298
                $question_list_answers,
6299
                $origin,
6300
                $exe_id,
6301
                $user_info,
6302
                $url,
6303
                $teachers
6304
            );
6305
        }
6306
6307
        if (!$sendEnd && !$sendStart) {
6308
            return false;
6309
        }
6310
6311
        $scoreLabel = '';
6312
        if ($sendEnd &&
6313
            ('true' === api_get_setting('exercise.send_score_in_exam_notification_mail_to_manager'))
6314
        ) {
6315
            $notificationPercentage = ('true' === api_get_setting('mail.send_notification_score_in_percentage'));
6316
            $scoreLabel = ExerciseLib::show_score($score, $weight, $notificationPercentage, true);
6317
            $scoreLabel = '<tr>
6318
                            <td>'.get_lang('Score')."</td>
6319
                            <td>&nbsp;$scoreLabel</td>
6320
                        </tr>";
6321
        }
6322
6323
        if ($sendEnd) {
6324
            $msg = get_lang('A learner attempted an exercise').'<br /><br />';
6325
        } else {
6326
            $msg = get_lang('Student just started an exercise').'<br /><br />';
6327
        }
6328
6329
        $msg .= get_lang('Attempt details').' : <br /><br />
6330
                    <table>
6331
                        <tr>
6332
                            <td>'.get_lang('Course name').'</td>
6333
                            <td>#course#</td>
6334
                        </tr>
6335
                        '.$sessionData.'
6336
                        <tr>
6337
                            <td>'.get_lang('Test').'</td>
6338
                            <td>&nbsp;#exercise#</td>
6339
                        </tr>
6340
                        <tr>
6341
                            <td>'.get_lang('Learner name').'</td>
6342
                            <td>&nbsp;#student_complete_name#</td>
6343
                        </tr>
6344
                        <tr>
6345
                            <td>'.get_lang('Learner e-mail').'</td>
6346
                            <td>&nbsp;#email#</td>
6347
                        </tr>
6348
                        '.$scoreLabel.'
6349
                    </table>';
6350
6351
        $variables = [
6352
            '#email#' => $user_info['email'],
6353
            '#exercise#' => $this->exercise,
6354
            '#student_complete_name#' => $user_info['complete_name'],
6355
            '#course#' => Display::url(
6356
                $courseInfo['title'],
6357
                $courseInfo['course_public_url'].'?sid='.$sessionId
6358
            ),
6359
        ];
6360
6361
        if ($sendEnd) {
6362
            $msg .= '<br /><a href="#url#">'.get_lang(
6363
                    'Click this link to check the answer and/or give feedback'
6364
                ).'</a>';
6365
            $variables['#url#'] = $url;
6366
        }
6367
6368
        $content = str_replace(array_keys($variables), array_values($variables), $msg);
6369
6370
        if ($sendEnd) {
6371
            $subject = get_lang('A learner attempted an exercise');
6372
        } else {
6373
            $subject = get_lang('Student just started an exercise');
6374
        }
6375
6376
        if (!empty($teachers)) {
6377
            foreach ($teachers as $user_id => $teacher_data) {
6378
                MessageManager::send_message_simple(
6379
                    $user_id,
6380
                    $subject,
6381
                    $content
6382
                );
6383
            }
6384
        }
6385
    }
6386
6387
    /**
6388
     * @param array $user_data         result of api_get_user_info()
6389
     * @param array $trackExerciseInfo result of get_stat_track_exercise_info
6390
     * @param bool  $saveUserResult
6391
     * @param bool  $allowSignature
6392
     * @param bool  $allowExportPdf
6393
     *
6394
     * @return string
6395
     */
6396
    public function showExerciseResultHeader(
6397
        $user_data,
6398
        $trackExerciseInfo,
6399
        $saveUserResult,
6400
        $allowSignature = false,
6401
        $allowExportPdf = false
6402
    ) {
6403
        if ('true' === api_get_setting('exercise.hide_user_info_in_quiz_result')) {
6404
            return '';
6405
        }
6406
6407
        $start_date = null;
6408
        if (isset($trackExerciseInfo['start_date'])) {
6409
            $start_date = api_convert_and_format_date($trackExerciseInfo['start_date']);
6410
        }
6411
        $duration = isset($trackExerciseInfo['duration_formatted']) ? $trackExerciseInfo['duration_formatted'] : null;
6412
        $ip = isset($trackExerciseInfo['user_ip']) ? $trackExerciseInfo['user_ip'] : null;
6413
6414
        if (!empty($user_data)) {
6415
            $userFullName = $user_data['complete_name'];
6416
            if (api_is_teacher() || api_is_platform_admin(true, true)) {
6417
                $userFullName = '<a href="'.$user_data['profile_url'].'" title="'.get_lang('GoToStudentDetails').'">'.
6418
                    $user_data['complete_name'].'</a>';
6419
            }
6420
6421
            $data = [
6422
                'name_url' => $userFullName,
6423
                'complete_name' => $user_data['complete_name'],
6424
                'username' => $user_data['username'],
6425
                'avatar' => $user_data['avatar_medium'],
6426
                'url' => $user_data['profile_url'],
6427
            ];
6428
6429
            if (!empty($user_data['official_code'])) {
6430
                $data['code'] = $user_data['official_code'];
6431
            }
6432
        }
6433
        // Description can be very long and is generally meant to explain
6434
        //   rules *before* the exam. Leaving here to make display easier if
6435
        //   necessary
6436
        /*
6437
        if (!empty($this->description)) {
6438
            $array[] = array('title' => get_lang("Description"), 'content' => $this->description);
6439
        }
6440
        */
6441
6442
        $data['start_date'] = $start_date;
6443
        $data['duration'] = $duration;
6444
        $data['ip'] = $ip;
6445
6446
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
6447
            $data['title'] = $this->get_formated_title().get_lang('Result');
6448
        } else {
6449
            $data['title'] = PHP_EOL.$this->exercise.' : '.get_lang('Result');
6450
        }
6451
6452
        $questionsCount = count(explode(',', $trackExerciseInfo['data_tracking']));
6453
        $savedAnswersCount = $this->countUserAnswersSavedInExercise($trackExerciseInfo['exe_id']);
6454
6455
        $data['number_of_answers'] = $questionsCount;
6456
        $data['number_of_answers_saved'] = $savedAnswersCount;
6457
        $exeId = $trackExerciseInfo['exe_id'];
6458
6459
        if ('true' === api_get_setting('exercise.quiz_confirm_saved_answers')) {
6460
            $em = Database::getManager();
6461
6462
            if ($saveUserResult) {
6463
                $trackConfirmation = new TrackEExerciseConfirmation();
6464
                $trackConfirmation
6465
                    ->setUser(api_get_user_entity($trackExerciseInfo['exe_user_id']))
6466
                    ->setQuizId($trackExerciseInfo['exe_exo_id'])
6467
                    ->setAttemptId($trackExerciseInfo['exe_id'])
6468
                    ->setQuestionsCount($questionsCount)
6469
                    ->setSavedAnswersCount($savedAnswersCount)
6470
                    ->setCourseId($trackExerciseInfo['c_id'])
6471
                    ->setSessionId($trackExerciseInfo['session_id'])
6472
                    ->setCreatedAt(api_get_utc_datetime(null, false, true));
6473
6474
                $em->persist($trackConfirmation);
6475
                $em->flush();
6476
            } else {
6477
                $trackConfirmation = $em
6478
                    ->getRepository(TrackEExerciseConfirmation::class)
6479
                    ->findOneBy(
6480
                        [
6481
                            'attemptId' => $trackExerciseInfo['exe_id'],
6482
                            'quizId' => $trackExerciseInfo['exe_exo_id'],
6483
                            'courseId' => $trackExerciseInfo['c_id'],
6484
                            'sessionId' => $trackExerciseInfo['session_id'],
6485
                        ]
6486
                    );
6487
            }
6488
6489
            $data['track_confirmation'] = $trackConfirmation;
6490
        }
6491
6492
        $signature = '';
6493
        if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($this)) {
6494
            $signature = ExerciseSignaturePlugin::getSignature($trackExerciseInfo['exe_user_id'], $trackExerciseInfo);
6495
        }
6496
        $tpl = new Template(null, false, false, false, false, false, false);
6497
        $tpl->assign('data', $data);
6498
        $tpl->assign('allow_signature', $allowSignature);
6499
        $tpl->assign('signature', $signature);
6500
        $tpl->assign('allow_export_pdf', $allowExportPdf);
6501
        $tpl->assign(
6502
            'export_url',
6503
            api_get_path(WEB_CODE_PATH).'exercise/result.php?action=export&id='.$exeId.'&'.api_get_cidreq()
6504
        );
6505
        $layoutTemplate = $tpl->get_template('exercise/partials/result_exercise.tpl');
6506
6507
        return $tpl->fetch($layoutTemplate);
6508
    }
6509
6510
    /**
6511
     * Returns the exercise result.
6512
     *
6513
     * @param int        attempt id
6514
     *
6515
     * @return array
6516
     */
6517
    public function get_exercise_result($exe_id)
6518
    {
6519
        $result = [];
6520
        $track_exercise_info = ExerciseLib::get_exercise_track_exercise_info($exe_id);
6521
6522
        if (!empty($track_exercise_info)) {
6523
            $totalScore = 0;
6524
            $objExercise = new self();
6525
            $objExercise->read($track_exercise_info['exe_exo_id']);
6526
            if (!empty($track_exercise_info['data_tracking'])) {
6527
                $question_list = explode(',', $track_exercise_info['data_tracking']);
6528
            }
6529
            foreach ($question_list as $questionId) {
6530
                $question_result = $objExercise->manage_answer(
6531
                    $exe_id,
6532
                    $questionId,
6533
                    '',
6534
                    'exercise_show',
6535
                    [],
6536
                    false,
6537
                    true,
6538
                    false,
6539
                    $objExercise->selectPropagateNeg()
6540
                );
6541
                $totalScore += $question_result['score'];
6542
            }
6543
6544
            if (0 == $objExercise->selectPropagateNeg() && $totalScore < 0) {
6545
                $totalScore = 0;
6546
            }
6547
            $result = [
6548
                'score' => $totalScore,
6549
                'weight' => $track_exercise_info['max_score'],
6550
            ];
6551
        }
6552
6553
        return $result;
6554
    }
6555
6556
    /**
6557
     * Checks if the exercise is visible due a lot of conditions
6558
     * visibility, time limits, student attempts
6559
     * Return associative array
6560
     * value : true if exercise visible
6561
     * message : HTML formatted message
6562
     * rawMessage : text message.
6563
     *
6564
     * @param int  $lpId
6565
     * @param int  $lpItemId
6566
     * @param int  $lpItemViewId
6567
     * @param bool $filterByAdmin
6568
     *
6569
     * @return array
6570
     */
6571
    public function is_visible(
6572
        $lpId = 0,
6573
        $lpItemId = 0,
6574
        $lpItemViewId = 0,
6575
        $filterByAdmin = true
6576
    ) {
6577
        // 1. By default the exercise is visible
6578
        $isVisible = true;
6579
        $message = null;
6580
6581
        // 1.1 Admins and teachers can access to the exercise
6582
        if ($filterByAdmin) {
6583
            if (api_is_platform_admin() || api_is_course_admin() || api_is_course_tutor()) {
6584
                return ['value' => true, 'message' => ''];
6585
            }
6586
        }
6587
6588
        // Deleted exercise.
6589
        if (-1 == $this->active) {
6590
            return [
6591
                'value' => false,
6592
                'message' => Display::return_message(
6593
                    get_lang('TestNotFound'),
6594
                    'warning',
6595
                    false
6596
                ),
6597
                'rawMessage' => get_lang('TestNotFound'),
6598
            ];
6599
        }
6600
6601
        $repo = Container::getQuizRepository();
6602
        $exercise = $repo->find($this->iId);
6603
6604
        if (null === $exercise) {
6605
            return [];
6606
        }
6607
6608
        $course = api_get_course_entity($this->course_id);
6609
        $link = $exercise->getFirstResourceLinkFromCourseSession($course);
6610
6611
        if ($link && $link->isDraft()) {
6612
            $this->active = 0;
6613
        }
6614
6615
        // 2. If the exercise is not active.
6616
        if (empty($lpId)) {
6617
            // 2.1 LP is OFF
6618
            if (0 == $this->active) {
6619
                return [
6620
                    'value' => false,
6621
                    'message' => Display::return_message(
6622
                        get_lang('TestNotFound'),
6623
                        'warning',
6624
                        false
6625
                    ),
6626
                    'rawMessage' => get_lang('TestNotFound'),
6627
                ];
6628
            }
6629
        } else {
6630
            $lp = Container::getLpRepository()->find($lpId);
6631
            // 2.1 LP is loaded
6632
            if ($lp && 0 == $this->active &&
6633
                !learnpath::is_lp_visible_for_student($lp, api_get_user_id(), $course)
6634
            ) {
6635
                return [
6636
                    'value' => false,
6637
                    'message' => Display::return_message(
6638
                        get_lang('TestNotFound'),
6639
                        'warning',
6640
                        false
6641
                    ),
6642
                    'rawMessage' => get_lang('TestNotFound'),
6643
                ];
6644
            }
6645
        }
6646
6647
        // 3. We check if the time limits are on
6648
        $limitTimeExists = false;
6649
        if (!empty($this->start_time) || !empty($this->end_time)) {
6650
            $limitTimeExists = true;
6651
        }
6652
6653
        if ($limitTimeExists) {
6654
            $timeNow = time();
6655
            $existsStartDate = false;
6656
            $nowIsAfterStartDate = true;
6657
            $existsEndDate = false;
6658
            $nowIsBeforeEndDate = true;
6659
6660
            if (!empty($this->start_time)) {
6661
                $existsStartDate = true;
6662
            }
6663
6664
            if (!empty($this->end_time)) {
6665
                $existsEndDate = true;
6666
            }
6667
6668
            // check if we are before-or-after end-or-start date
6669
            if ($existsStartDate && $timeNow < api_strtotime($this->start_time, 'UTC')) {
6670
                $nowIsAfterStartDate = false;
6671
            }
6672
6673
            if ($existsEndDate & $timeNow >= api_strtotime($this->end_time, 'UTC')) {
6674
                $nowIsBeforeEndDate = false;
6675
            }
6676
6677
            // lets check all cases
6678
            if ($existsStartDate && !$existsEndDate) {
6679
                // exists start date and dont exists end date
6680
                if ($nowIsAfterStartDate) {
6681
                    // after start date, no end date
6682
                    $isVisible = true;
6683
                    $message = sprintf(
6684
                        get_lang('TestAvailableSinceX'),
6685
                        api_convert_and_format_date($this->start_time)
6686
                    );
6687
                } else {
6688
                    // before start date, no end date
6689
                    $isVisible = false;
6690
                    $message = sprintf(
6691
                        get_lang('TestAvailableFromX'),
6692
                        api_convert_and_format_date($this->start_time)
6693
                    );
6694
                }
6695
            } elseif (!$existsStartDate && $existsEndDate) {
6696
                // doesnt exist start date, exists end date
6697
                if ($nowIsBeforeEndDate) {
6698
                    // before end date, no start date
6699
                    $isVisible = true;
6700
                    $message = sprintf(
6701
                        get_lang('TestAvailableUntilX'),
6702
                        api_convert_and_format_date($this->end_time)
6703
                    );
6704
                } else {
6705
                    // after end date, no start date
6706
                    $isVisible = false;
6707
                    $message = sprintf(
6708
                        get_lang('TestAvailableUntilX'),
6709
                        api_convert_and_format_date($this->end_time)
6710
                    );
6711
                }
6712
            } elseif ($existsStartDate && $existsEndDate) {
6713
                // exists start date and end date
6714
                if ($nowIsAfterStartDate) {
6715
                    if ($nowIsBeforeEndDate) {
6716
                        // after start date and before end date
6717
                        $isVisible = true;
6718
                        $message = sprintf(
6719
                            get_lang('TestIsActivatedFromXToY'),
6720
                            api_convert_and_format_date($this->start_time),
6721
                            api_convert_and_format_date($this->end_time)
6722
                        );
6723
                    } else {
6724
                        // after start date and after end date
6725
                        $isVisible = false;
6726
                        $message = sprintf(
6727
                            get_lang('TestWasActivatedFromXToY'),
6728
                            api_convert_and_format_date($this->start_time),
6729
                            api_convert_and_format_date($this->end_time)
6730
                        );
6731
                    }
6732
                } else {
6733
                    if ($nowIsBeforeEndDate) {
6734
                        // before start date and before end date
6735
                        $isVisible = false;
6736
                        $message = sprintf(
6737
                            get_lang('TestWillBeActivatedFromXToY'),
6738
                            api_convert_and_format_date($this->start_time),
6739
                            api_convert_and_format_date($this->end_time)
6740
                        );
6741
                    }
6742
                    // case before start date and after end date is impossible
6743
                }
6744
            } elseif (!$existsStartDate && !$existsEndDate) {
6745
                // doesnt exist start date nor end date
6746
                $isVisible = true;
6747
                $message = '';
6748
            }
6749
        }
6750
6751
        // 4. We check if the student have attempts
6752
        if ($isVisible) {
6753
            $exerciseAttempts = $this->selectAttempts();
6754
6755
            if ($exerciseAttempts > 0) {
6756
                $attemptCount = Event::get_attempt_count(
6757
                    api_get_user_id(),
6758
                    $this->getId(),
6759
                    (int) $lpId,
6760
                    (int) $lpItemId,
6761
                    (int) $lpItemViewId
6762
                );
6763
6764
                if ($attemptCount >= $exerciseAttempts) {
6765
                    $message = sprintf(
6766
                        get_lang('You cannot take test <b>%s</b> because you have already reached the maximum of %s attempts.'),
6767
                        $this->name,
6768
                        $exerciseAttempts
6769
                    );
6770
                    $isVisible = false;
6771
                } else {
6772
                    // Check blocking exercise.
6773
                    $extraFieldValue = new ExtraFieldValue('exercise');
6774
                    $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
6775
                        $this->iId,
6776
                        'blocking_percentage'
6777
                    );
6778
                    if ($blockExercise && isset($blockExercise['value']) && !empty($blockExercise['value'])) {
6779
                        $blockPercentage = (int) $blockExercise['value'];
6780
                        $userAttempts = Event::getExerciseResultsByUser(
6781
                            api_get_user_id(),
6782
                            $this->iId,
6783
                            $this->course_id,
6784
                            $this->sessionId,
6785
                            $lpId,
6786
                            $lpItemId
6787
                        );
6788
6789
                        if (!empty($userAttempts)) {
6790
                            $currentAttempt = current($userAttempts);
6791
                            if ($currentAttempt['total_percentage'] <= $blockPercentage) {
6792
                                $message = sprintf(
6793
                                    get_lang('ExerciseBlockBecausePercentageX'),
6794
                                    $blockPercentage
6795
                                );
6796
                                $isVisible = false;
6797
                            }
6798
                        }
6799
                    }
6800
                }
6801
            }
6802
        }
6803
6804
        $rawMessage = '';
6805
        if (!empty($message)) {
6806
            $rawMessage = $message;
6807
            $message = Display::return_message($message, 'warning', false);
6808
        }
6809
6810
        return [
6811
            'value' => $isVisible,
6812
            'message' => $message,
6813
            'rawMessage' => $rawMessage,
6814
        ];
6815
    }
6816
6817
    /**
6818
     * @return bool
6819
     */
6820
    public function added_in_lp()
6821
    {
6822
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
6823
        $sql = "SELECT max_score FROM $TBL_LP_ITEM
6824
                WHERE
6825
                    item_type = '".TOOL_QUIZ."' AND
6826
                    path = '{$this->getId()}'";
6827
        $result = Database::query($sql);
6828
        if (Database::num_rows($result) > 0) {
6829
            return true;
6830
        }
6831
6832
        return false;
6833
    }
6834
6835
    /**
6836
     * Returns an array with this form.
6837
     *
6838
     * @return array
6839
     *
6840
     * @example
6841
     * <code>
6842
     * array (size=3)
6843
     * 999 =>
6844
     * array (size=3)
6845
     * 0 => int 3422
6846
     * 1 => int 3423
6847
     * 2 => int 3424
6848
     * 100 =>
6849
     * array (size=2)
6850
     * 0 => int 3469
6851
     * 1 => int 3470
6852
     * 101 =>
6853
     * array (size=1)
6854
     * 0 => int 3482
6855
     * </code>
6856
     * The array inside the key 999 means the question list that belongs to the media id = 999,
6857
     * this case is special because 999 means "no media".
6858
     */
6859
    public function getMediaList()
6860
    {
6861
        return $this->mediaList;
6862
    }
6863
6864
    /**
6865
     * Is media question activated?
6866
     *
6867
     * @return bool
6868
     */
6869
    public function mediaIsActivated()
6870
    {
6871
        $mediaQuestions = $this->getMediaList();
6872
        $active = false;
6873
        if (isset($mediaQuestions) && !empty($mediaQuestions)) {
6874
            $media_count = count($mediaQuestions);
6875
            if ($media_count > 1) {
6876
                return true;
6877
            } elseif (1 == $media_count) {
6878
                if (isset($mediaQuestions[999])) {
6879
                    return false;
6880
                } else {
6881
                    return true;
6882
                }
6883
            }
6884
        }
6885
6886
        return $active;
6887
    }
6888
6889
    /**
6890
     * Gets question list from the exercise.
6891
     *
6892
     * @return array
6893
     */
6894
    public function getQuestionList()
6895
    {
6896
        return $this->questionList;
6897
    }
6898
6899
    /**
6900
     * Question list with medias compressed like this.
6901
     *
6902
     * @return array
6903
     *
6904
     * @example
6905
     *      <code>
6906
     *      array(
6907
     *      question_id_1,
6908
     *      question_id_2,
6909
     *      media_id, <- this media id contains question ids
6910
     *      question_id_3,
6911
     *      )
6912
     *      </code>
6913
     */
6914
    public function getQuestionListWithMediasCompressed()
6915
    {
6916
        return $this->questionList;
6917
    }
6918
6919
    /**
6920
     * Question list with medias uncompressed like this.
6921
     *
6922
     * @return array
6923
     *
6924
     * @example
6925
     *      <code>
6926
     *      array(
6927
     *      question_id,
6928
     *      question_id,
6929
     *      question_id, <- belongs to a media id
6930
     *      question_id, <- belongs to a media id
6931
     *      question_id,
6932
     *      )
6933
     *      </code>
6934
     */
6935
    public function getQuestionListWithMediasUncompressed()
6936
    {
6937
        return $this->questionListUncompressed;
6938
    }
6939
6940
    /**
6941
     * Sets the question list when the exercise->read() is executed.
6942
     *
6943
     * @param bool $adminView Whether to view the set the list of *all* questions or just the normal student view
6944
     */
6945
    public function setQuestionList($adminView = false)
6946
    {
6947
        // Getting question list.
6948
        $questionList = $this->selectQuestionList(true, $adminView);
6949
        $this->setMediaList($questionList);
6950
        $this->questionList = $this->transformQuestionListWithMedias($questionList, false);
6951
        $this->questionListUncompressed = $this->transformQuestionListWithMedias(
6952
            $questionList,
6953
            true
6954
        );
6955
    }
6956
6957
    /**
6958
     * @params array question list
6959
     * @params bool expand or not question list (true show all questions,
6960
     * false show media question id instead of the question ids)
6961
     */
6962
    public function transformQuestionListWithMedias(
6963
        $question_list,
6964
        $expand_media_questions = false
6965
    ) {
6966
        $new_question_list = [];
6967
        if (!empty($question_list)) {
6968
            $media_questions = $this->getMediaList();
6969
            $media_active = $this->mediaIsActivated($media_questions);
6970
6971
            if ($media_active) {
6972
                $counter = 1;
6973
                foreach ($question_list as $question_id) {
6974
                    $add_question = true;
6975
                    foreach ($media_questions as $media_id => $question_list_in_media) {
6976
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
6977
                            $add_question = false;
6978
                            if (!in_array($media_id, $new_question_list)) {
6979
                                $new_question_list[$counter] = $media_id;
6980
                                $counter++;
6981
                            }
6982
6983
                            break;
6984
                        }
6985
                    }
6986
                    if ($add_question) {
6987
                        $new_question_list[$counter] = $question_id;
6988
                        $counter++;
6989
                    }
6990
                }
6991
                if ($expand_media_questions) {
6992
                    $media_key_list = array_keys($media_questions);
6993
                    foreach ($new_question_list as &$question_id) {
6994
                        if (in_array($question_id, $media_key_list)) {
6995
                            $question_id = $media_questions[$question_id];
6996
                        }
6997
                    }
6998
                    $new_question_list = array_flatten($new_question_list);
6999
                }
7000
            } else {
7001
                $new_question_list = $question_list;
7002
            }
7003
        }
7004
7005
        return $new_question_list;
7006
    }
7007
7008
    /**
7009
     * Get question list depend on the random settings.
7010
     *
7011
     * @return array
7012
     */
7013
    public function get_validated_question_list()
7014
    {
7015
        $isRandomByCategory = $this->isRandomByCat();
7016
        if (0 == $isRandomByCategory) {
7017
            if ($this->isRandom()) {
7018
                return $this->getRandomList();
7019
            }
7020
7021
            return $this->selectQuestionList();
7022
        }
7023
7024
        if ($this->isRandom()) {
7025
            // USE question categories
7026
            // get questions by category for this exercise
7027
            // we have to choice $objExercise->random question in each array values of $tabCategoryQuestions
7028
            // key of $tabCategoryQuestions are the categopy id (0 for not in a category)
7029
            // value is the array of question id of this category
7030
            $questionList = [];
7031
            $categoryQuestions = TestCategory::getQuestionsByCat($this->id);
7032
            $isRandomByCategory = $this->getRandomByCategory();
7033
            // We sort categories based on the term between [] in the head
7034
            // of the category's description
7035
            /* examples of categories :
7036
             * [biologie] Maitriser les mecanismes de base de la genetique
7037
             * [biologie] Relier les moyens de depenses et les agents infectieux
7038
             * [biologie] Savoir ou est produite l'enrgie dans les cellules et sous quelle forme
7039
             * [chimie] Classer les molles suivant leur pouvoir oxydant ou reacteur
7040
             * [chimie] Connaître la denition de la theoie acide/base selon Brönsted
7041
             * [chimie] Connaître les charges des particules
7042
             * We want that in the order of the groups defined by the term
7043
             * between brackets at the beginning of the category title
7044
            */
7045
            // If test option is Grouped By Categories
7046
            if (2 == $isRandomByCategory) {
7047
                $categoryQuestions = TestCategory::sortTabByBracketLabel($categoryQuestions);
7048
            }
7049
            foreach ($categoryQuestions as $question) {
7050
                $number_of_random_question = $this->random;
7051
                if (-1 == $this->random) {
7052
                    $number_of_random_question = count($this->questionList);
7053
                }
7054
                $questionList = array_merge(
7055
                    $questionList,
7056
                    TestCategory::getNElementsFromArray(
7057
                        $question,
7058
                        $number_of_random_question
7059
                    )
7060
                );
7061
            }
7062
            // shuffle the question list if test is not grouped by categories
7063
            if (1 == $isRandomByCategory) {
7064
                shuffle($questionList); // or not
7065
            }
7066
7067
            return $questionList;
7068
        }
7069
7070
        // Problem, random by category has been selected and
7071
        // we have no $this->isRandom number of question selected
7072
        // Should not happened
7073
7074
        return [];
7075
    }
7076
7077
    public function get_question_list($expand_media_questions = false)
7078
    {
7079
        $question_list = $this->get_validated_question_list();
7080
        $question_list = $this->transform_question_list_with_medias($question_list, $expand_media_questions);
7081
7082
        return $question_list;
7083
    }
7084
7085
    public function transform_question_list_with_medias($question_list, $expand_media_questions = false)
7086
    {
7087
        $new_question_list = [];
7088
        if (!empty($question_list)) {
7089
            $media_questions = $this->getMediaList();
7090
            $media_active = $this->mediaIsActivated($media_questions);
7091
7092
            if ($media_active) {
7093
                $counter = 1;
7094
                foreach ($question_list as $question_id) {
7095
                    $add_question = true;
7096
                    foreach ($media_questions as $media_id => $question_list_in_media) {
7097
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
7098
                            $add_question = false;
7099
                            if (!in_array($media_id, $new_question_list)) {
7100
                                $new_question_list[$counter] = $media_id;
7101
                                $counter++;
7102
                            }
7103
7104
                            break;
7105
                        }
7106
                    }
7107
                    if ($add_question) {
7108
                        $new_question_list[$counter] = $question_id;
7109
                        $counter++;
7110
                    }
7111
                }
7112
                if ($expand_media_questions) {
7113
                    $media_key_list = array_keys($media_questions);
7114
                    foreach ($new_question_list as &$question_id) {
7115
                        if (in_array($question_id, $media_key_list)) {
7116
                            $question_id = $media_questions[$question_id];
7117
                        }
7118
                    }
7119
                    $new_question_list = array_flatten($new_question_list);
7120
                }
7121
            } else {
7122
                $new_question_list = $question_list;
7123
            }
7124
        }
7125
7126
        return $new_question_list;
7127
    }
7128
7129
    /**
7130
     * @param int $exe_id
7131
     *
7132
     * @return array
7133
     */
7134
    public function get_stat_track_exercise_info_by_exe_id($exe_id)
7135
    {
7136
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7137
        $exe_id = (int) $exe_id;
7138
        $sql_track = "SELECT * FROM $table WHERE exe_id = $exe_id ";
7139
        $result = Database::query($sql_track);
7140
        $new_array = [];
7141
        if (Database::num_rows($result) > 0) {
7142
            $new_array = Database::fetch_assoc($result);
7143
            $start_date = api_get_utc_datetime($new_array['start_date'], true);
7144
            $end_date = api_get_utc_datetime($new_array['exe_date'], true);
7145
            $new_array['duration_formatted'] = '';
7146
            if (!empty($new_array['exe_duration']) && !empty($start_date) && !empty($end_date)) {
7147
                $time = api_format_time($new_array['exe_duration'], 'js');
7148
                $new_array['duration_formatted'] = $time;
7149
            }
7150
        }
7151
7152
        return $new_array;
7153
    }
7154
7155
    /**
7156
     * @param int $exeId
7157
     *
7158
     * @return bool
7159
     */
7160
    public function removeAllQuestionToRemind($exeId)
7161
    {
7162
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7163
        $exeId = (int) $exeId;
7164
        if (empty($exeId)) {
7165
            return false;
7166
        }
7167
        $sql = "UPDATE $table
7168
                SET questions_to_check = ''
7169
                WHERE exe_id = $exeId ";
7170
        Database::query($sql);
7171
7172
        return true;
7173
    }
7174
7175
    /**
7176
     * @param int   $exeId
7177
     * @param array $questionList
7178
     *
7179
     * @return bool
7180
     */
7181
    public function addAllQuestionToRemind($exeId, $questionList = [])
7182
    {
7183
        $exeId = (int) $exeId;
7184
        if (empty($questionList)) {
7185
            return false;
7186
        }
7187
7188
        $questionListToString = implode(',', $questionList);
7189
        $questionListToString = Database::escape_string($questionListToString);
7190
7191
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7192
        $sql = "UPDATE $table
7193
                SET questions_to_check = '$questionListToString'
7194
                WHERE exe_id = $exeId";
7195
        Database::query($sql);
7196
7197
        return true;
7198
    }
7199
7200
    /**
7201
     * @param int    $exeId
7202
     * @param int    $questionId
7203
     * @param string $action
7204
     */
7205
    public function editQuestionToRemind($exeId, $questionId, $action = 'add')
7206
    {
7207
        $exercise_info = self::get_stat_track_exercise_info_by_exe_id($exeId);
7208
        $questionId = (int) $questionId;
7209
        $exeId = (int) $exeId;
7210
7211
        if ($exercise_info) {
7212
            $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7213
            if (empty($exercise_info['questions_to_check'])) {
7214
                if ('add' == $action) {
7215
                    $sql = "UPDATE $track_exercises
7216
                            SET questions_to_check = '$questionId'
7217
                            WHERE exe_id = $exeId ";
7218
                    Database::query($sql);
7219
                }
7220
            } else {
7221
                $remind_list = explode(',', $exercise_info['questions_to_check']);
7222
                $remind_list_string = '';
7223
                if ('add' === $action) {
7224
                    if (!in_array($questionId, $remind_list)) {
7225
                        $newRemindList = [];
7226
                        $remind_list[] = $questionId;
7227
                        $questionListInSession = Session::read('questionList');
7228
                        if (!empty($questionListInSession)) {
7229
                            foreach ($questionListInSession as $originalQuestionId) {
7230
                                if (in_array($originalQuestionId, $remind_list)) {
7231
                                    $newRemindList[] = $originalQuestionId;
7232
                                }
7233
                            }
7234
                        }
7235
                        $remind_list_string = implode(',', $newRemindList);
7236
                    }
7237
                } elseif ('delete' == $action) {
7238
                    if (!empty($remind_list)) {
7239
                        if (in_array($questionId, $remind_list)) {
7240
                            $remind_list = array_flip($remind_list);
7241
                            unset($remind_list[$questionId]);
7242
                            $remind_list = array_flip($remind_list);
7243
7244
                            if (!empty($remind_list)) {
7245
                                sort($remind_list);
7246
                                array_filter($remind_list);
7247
                                $remind_list_string = implode(',', $remind_list);
7248
                            }
7249
                        }
7250
                    }
7251
                }
7252
                $value = Database::escape_string($remind_list_string);
7253
                $sql = "UPDATE $track_exercises
7254
                        SET questions_to_check = '$value'
7255
                        WHERE exe_id = $exeId ";
7256
                Database::query($sql);
7257
            }
7258
        }
7259
    }
7260
7261
    /**
7262
     * @param string $answer
7263
     */
7264
    public function fill_in_blank_answer_to_array($answer)
7265
    {
7266
        $list = null;
7267
        api_preg_match_all('/\[[^]]+\]/', $answer, $list);
7268
7269
        if (empty($list)) {
7270
            return '';
7271
        }
7272
7273
        return $list[0];
7274
    }
7275
7276
    /**
7277
     * @param string $answer
7278
     *
7279
     * @return string
7280
     */
7281
    public function fill_in_blank_answer_to_string($answer)
7282
    {
7283
        $teacher_answer_list = $this->fill_in_blank_answer_to_array($answer);
7284
        $result = '';
7285
        if (!empty($teacher_answer_list)) {
7286
            foreach ($teacher_answer_list as $teacher_item) {
7287
                //Cleaning student answer list
7288
                $value = strip_tags($teacher_item);
7289
                $value = api_substr($value, 1, api_strlen($value) - 2);
7290
                $value = explode('/', $value);
7291
                if (!empty($value[0])) {
7292
                    $value = trim($value[0]);
7293
                    $value = str_replace('&nbsp;', '', $value);
7294
                    $result .= $value;
7295
                }
7296
            }
7297
        }
7298
7299
        return $result;
7300
    }
7301
7302
    /**
7303
     * @return string
7304
     */
7305
    public function returnTimeLeftDiv()
7306
    {
7307
        $html = '<div id="clock_warning" style="display:none">';
7308
        $html .= Display::return_message(
7309
            get_lang('Time limit reached'),
7310
            'warning'
7311
        );
7312
        $html .= ' ';
7313
        $html .= sprintf(
7314
            get_lang('Just a moment, please. You will be redirected in %s seconds...'),
7315
            '<span id="counter_to_redirect" class="red_alert"></span>'
7316
        );
7317
        $html .= '</div>';
7318
        $icon = Display::getMdiIcon('clock-outline', 'ch-tool-icon');
7319
        $html .= '<div class="count_down">
7320
                    '.get_lang('RemainingTimeToFinishExercise').'
7321
                    '.$icon.'<span id="exercise_clock_warning"></span>
7322
                </div>';
7323
7324
        return $html;
7325
    }
7326
7327
    /**
7328
     * Get categories added in the exercise--category matrix.
7329
     *
7330
     * @return array
7331
     */
7332
    public function getCategoriesInExercise()
7333
    {
7334
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7335
        if (!empty($this->getId())) {
7336
            $sql = "SELECT * FROM $table
7337
                    WHERE exercise_id = {$this->getId()} ";
7338
            $result = Database::query($sql);
7339
            $list = [];
7340
            if (Database::num_rows($result)) {
7341
                while ($row = Database::fetch_assoc($result)) {
7342
                    $list[$row['category_id']] = $row;
7343
                }
7344
7345
                return $list;
7346
            }
7347
        }
7348
7349
        return [];
7350
    }
7351
7352
    /**
7353
     * Get total number of question that will be parsed when using the category/exercise.
7354
     *
7355
     * @return int
7356
     */
7357
    public function getNumberQuestionExerciseCategory()
7358
    {
7359
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7360
        if (!empty($this->getId())) {
7361
            $sql = "SELECT SUM(count_questions) count_questions
7362
                    FROM $table
7363
                    WHERE exercise_id = {$this->getId()}";
7364
            $result = Database::query($sql);
7365
            if (Database::num_rows($result)) {
7366
                $row = Database::fetch_array($result);
7367
7368
                return (int) $row['count_questions'];
7369
            }
7370
        }
7371
7372
        return 0;
7373
    }
7374
7375
    /**
7376
     * Save categories in the TABLE_QUIZ_REL_CATEGORY table
7377
     *
7378
     * @param array $categories
7379
     */
7380
    public function saveCategoriesInExercise($categories)
7381
    {
7382
        if (!empty($categories) && !empty($this->getId())) {
7383
            $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7384
            $sql = "DELETE FROM $table
7385
                    WHERE exercise_id = {$this->getId()}";
7386
            Database::query($sql);
7387
            foreach ($categories as $categoryId => $countQuestions) {
7388
                if ($categoryId !== 0) {
7389
                    $params = [
7390
                        'exercise_id' => $this->getId(),
7391
                        'category_id' => $categoryId,
7392
                        'count_questions' => $countQuestions,
7393
                    ];
7394
                    Database::insert($table, $params);
7395
                }
7396
            }
7397
        }
7398
    }
7399
7400
    /**
7401
     * @param array  $questionList
7402
     * @param int    $currentQuestion
7403
     * @param array  $conditions
7404
     * @param string $link
7405
     *
7406
     * @return string
7407
     */
7408
    public function progressExercisePaginationBar(
7409
        $questionList,
7410
        $currentQuestion,
7411
        $conditions,
7412
        $link
7413
    ) {
7414
        $mediaQuestions = $this->getMediaList();
7415
7416
        $html = '<div class="exercise_pagination pagination pagination-mini"><ul>';
7417
        $counter = 0;
7418
        $nextValue = 0;
7419
        $wasMedia = false;
7420
        $before = 0;
7421
        $counterNoMedias = 0;
7422
        foreach ($questionList as $questionId) {
7423
            $isCurrent = $currentQuestion == $counterNoMedias + 1 ? true : false;
7424
7425
            if (!empty($nextValue)) {
7426
                if ($wasMedia) {
7427
                    $nextValue = $nextValue - $before + 1;
7428
                }
7429
            }
7430
7431
            if (isset($mediaQuestions) && isset($mediaQuestions[$questionId])) {
7432
                $fixedValue = $counterNoMedias;
7433
7434
                $html .= Display::progressPaginationBar(
7435
                    $nextValue,
7436
                    $mediaQuestions[$questionId],
7437
                    $currentQuestion,
7438
                    $fixedValue,
7439
                    $conditions,
7440
                    $link,
7441
                    true,
7442
                    true
7443
                );
7444
7445
                $counter += count($mediaQuestions[$questionId]) - 1;
7446
                $before = count($questionList);
7447
                $wasMedia = true;
7448
                $nextValue += count($questionList);
7449
            } else {
7450
                $html .= Display::parsePaginationItem(
7451
                    $questionId,
7452
                    $isCurrent,
7453
                    $conditions,
7454
                    $link,
7455
                    $counter
7456
                );
7457
                $counter++;
7458
                $nextValue++;
7459
                $wasMedia = false;
7460
            }
7461
            $counterNoMedias++;
7462
        }
7463
        $html .= '</ul></div>';
7464
7465
        return $html;
7466
    }
7467
7468
    /**
7469
     *  Shows a list of numbers that represents the question to answer in a exercise.
7470
     *
7471
     * @param array  $categories
7472
     * @param int    $current
7473
     * @param array  $conditions
7474
     * @param string $link
7475
     *
7476
     * @return string
7477
     */
7478
    public function progressExercisePaginationBarWithCategories(
7479
        $categories,
7480
        $current,
7481
        $conditions = [],
7482
        $link = null
7483
    ) {
7484
        $html = null;
7485
        $counterNoMedias = 0;
7486
        $nextValue = 0;
7487
        $wasMedia = false;
7488
        $before = 0;
7489
7490
        if (!empty($categories)) {
7491
            $selectionType = $this->getQuestionSelectionType();
7492
            $useRootAsCategoryTitle = false;
7493
7494
            // Grouping questions per parent category see BT#6540
7495
            if (in_array(
7496
                $selectionType,
7497
                [
7498
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED,
7499
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM,
7500
                ]
7501
            )) {
7502
                $useRootAsCategoryTitle = true;
7503
            }
7504
7505
            // If the exercise is set to only show the titles of the categories
7506
            // at the root of the tree, then pre-order the categories tree by
7507
            // removing children and summing their questions into the parent
7508
            // categories
7509
            if ($useRootAsCategoryTitle) {
7510
                // The new categories list starts empty
7511
                $newCategoryList = [];
7512
                foreach ($categories as $category) {
7513
                    $rootElement = $category['root'];
7514
7515
                    if (isset($category['parent_info'])) {
7516
                        $rootElement = $category['parent_info']['id'];
7517
                    }
7518
7519
                    //$rootElement = $category['id'];
7520
                    // If the current category's ancestor was never seen
7521
                    // before, then declare it and assign the current
7522
                    // category to it.
7523
                    if (!isset($newCategoryList[$rootElement])) {
7524
                        $newCategoryList[$rootElement] = $category;
7525
                    } else {
7526
                        // If it was already seen, then merge the previous with
7527
                        // the current category
7528
                        $oldQuestionList = $newCategoryList[$rootElement]['question_list'];
7529
                        $category['question_list'] = array_merge($oldQuestionList, $category['question_list']);
7530
                        $newCategoryList[$rootElement] = $category;
7531
                    }
7532
                }
7533
                // Now use the newly built categories list, with only parents
7534
                $categories = $newCategoryList;
7535
            }
7536
7537
            foreach ($categories as $category) {
7538
                $questionList = $category['question_list'];
7539
                // Check if in this category there questions added in a media
7540
                $mediaQuestionId = $category['media_question'];
7541
                $isMedia = false;
7542
                $fixedValue = null;
7543
7544
                // Media exists!
7545
                if (999 != $mediaQuestionId) {
7546
                    $isMedia = true;
7547
                    $fixedValue = $counterNoMedias;
7548
                }
7549
7550
                //$categoryName = $category['path']; << show the path
7551
                $categoryName = $category['name'];
7552
7553
                if ($useRootAsCategoryTitle) {
7554
                    if (isset($category['parent_info'])) {
7555
                        $categoryName = $category['parent_info']['title'];
7556
                    }
7557
                }
7558
                $html .= '<div class="row">';
7559
                $html .= '<div class="span2">'.$categoryName.'</div>';
7560
                $html .= '<div class="span8">';
7561
7562
                if (!empty($nextValue)) {
7563
                    if ($wasMedia) {
7564
                        $nextValue = $nextValue - $before + 1;
7565
                    }
7566
                }
7567
                $html .= Display::progressPaginationBar(
7568
                    $nextValue,
7569
                    $questionList,
7570
                    $current,
7571
                    $fixedValue,
7572
                    $conditions,
7573
                    $link,
7574
                    $isMedia,
7575
                    true
7576
                );
7577
                $html .= '</div>';
7578
                $html .= '</div>';
7579
7580
                if (999 == $mediaQuestionId) {
7581
                    $counterNoMedias += count($questionList);
7582
                } else {
7583
                    $counterNoMedias++;
7584
                }
7585
7586
                $nextValue += count($questionList);
7587
                $before = count($questionList);
7588
7589
                if (999 != $mediaQuestionId) {
7590
                    $wasMedia = true;
7591
                } else {
7592
                    $wasMedia = false;
7593
                }
7594
            }
7595
        }
7596
7597
        return $html;
7598
    }
7599
7600
    /**
7601
     * Renders a question list.
7602
     *
7603
     * @param array $questionList    (with media questions compressed)
7604
     * @param int   $currentQuestion
7605
     * @param array $exerciseResult
7606
     * @param array $attemptList
7607
     * @param array $remindList
7608
     */
7609
    public function renderQuestionList(
7610
        $questionList,
7611
        $currentQuestion,
7612
        $exerciseResult,
7613
        $attemptList,
7614
        $remindList
7615
    ) {
7616
        $mediaQuestions = $this->getMediaList();
7617
        $i = 0;
7618
7619
        // Normal question list render (medias compressed)
7620
        foreach ($questionList as $questionId) {
7621
            $i++;
7622
            // For sequential exercises
7623
7624
            if (ONE_PER_PAGE == $this->type) {
7625
                // If it is not the right question, goes to the next loop iteration
7626
                if ($currentQuestion != $i) {
7627
                    continue;
7628
                } else {
7629
                    if (!in_array(
7630
                        $this->getFeedbackType(),
7631
                        [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
7632
                    )) {
7633
                        // if the user has already answered this question
7634
                        if (isset($exerciseResult[$questionId])) {
7635
                            echo Display::return_message(
7636
                                get_lang('You already answered the question'),
7637
                                'normal'
7638
                            );
7639
7640
                            break;
7641
                        }
7642
                    }
7643
                }
7644
            }
7645
7646
            // The $questionList contains the media id we check
7647
            // if this questionId is a media question type
7648
            if (isset($mediaQuestions[$questionId]) &&
7649
                999 != $mediaQuestions[$questionId]
7650
            ) {
7651
                // The question belongs to a media
7652
                $mediaQuestionList = $mediaQuestions[$questionId];
7653
                $objQuestionTmp = Question::read($questionId);
7654
7655
                $counter = 1;
7656
                if (MEDIA_QUESTION == $objQuestionTmp->type) {
7657
                    echo $objQuestionTmp->show_media_content();
7658
7659
                    $countQuestionsInsideMedia = count($mediaQuestionList);
7660
7661
                    // Show questions that belongs to a media
7662
                    if (!empty($mediaQuestionList)) {
7663
                        // In order to parse media questions we use letters a, b, c, etc.
7664
                        $letterCounter = 97;
7665
                        foreach ($mediaQuestionList as $questionIdInsideMedia) {
7666
                            $isLastQuestionInMedia = false;
7667
                            if ($counter == $countQuestionsInsideMedia) {
7668
                                $isLastQuestionInMedia = true;
7669
                            }
7670
                            $this->renderQuestion(
7671
                                $questionIdInsideMedia,
7672
                                $attemptList,
7673
                                $remindList,
7674
                                chr($letterCounter),
7675
                                $currentQuestion,
7676
                                $mediaQuestionList,
7677
                                $isLastQuestionInMedia,
7678
                                $questionList
7679
                            );
7680
                            $letterCounter++;
7681
                            $counter++;
7682
                        }
7683
                    }
7684
                } else {
7685
                    $this->renderQuestion(
7686
                        $questionId,
7687
                        $attemptList,
7688
                        $remindList,
7689
                        $i,
7690
                        $currentQuestion,
7691
                        null,
7692
                        null,
7693
                        $questionList
7694
                    );
7695
                    $i++;
7696
                }
7697
            } else {
7698
                // Normal question render.
7699
                $this->renderQuestion(
7700
                    $questionId,
7701
                    $attemptList,
7702
                    $remindList,
7703
                    $i,
7704
                    $currentQuestion,
7705
                    null,
7706
                    null,
7707
                    $questionList
7708
                );
7709
            }
7710
7711
            // For sequential exercises.
7712
            if (ONE_PER_PAGE == $this->type) {
7713
                // quits the loop
7714
                break;
7715
            }
7716
        }
7717
        // end foreach()
7718
7719
        if (ALL_ON_ONE_PAGE == $this->type) {
7720
            $exercise_actions = $this->show_button($questionId, $currentQuestion);
7721
            echo Display::div($exercise_actions, ['class' => 'exercise_actions']);
7722
        }
7723
    }
7724
7725
    /**
7726
     * Not implemented in 1.11.x.
7727
     *
7728
     * @param int   $questionId
7729
     * @param array $attemptList
7730
     * @param array $remindList
7731
     * @param int   $i
7732
     * @param int   $current_question
7733
     * @param array $questions_in_media
7734
     * @param bool  $last_question_in_media
7735
     * @param array $realQuestionList
7736
     * @param bool  $generateJS
7737
     */
7738
    public function renderQuestion(
7739
        $questionId,
7740
        $attemptList,
7741
        $remindList,
7742
        $i,
7743
        $current_question,
7744
        $questions_in_media = [],
7745
        $last_question_in_media = false,
7746
        $realQuestionList = [],
7747
        $generateJS = true
7748
    ) {
7749
        // With this option on the question is loaded via AJAX
7750
        //$generateJS = true;
7751
        //$this->loadQuestionAJAX = true;
7752
7753
        if ($generateJS && $this->loadQuestionAJAX) {
7754
            $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?a=get_question&id='.$questionId.'&'.api_get_cidreq();
7755
            $params = [
7756
                'questionId' => $questionId,
7757
                'attemptList' => $attemptList,
7758
                'remindList' => $remindList,
7759
                'i' => $i,
7760
                'current_question' => $current_question,
7761
                'questions_in_media' => $questions_in_media,
7762
                'last_question_in_media' => $last_question_in_media,
7763
            ];
7764
            $params = json_encode($params);
7765
7766
            $script = '<script>
7767
            $(function(){
7768
                var params = '.$params.';
7769
                $.ajax({
7770
                    type: "GET",
7771
                    data: params,
7772
                    url: "'.$url.'",
7773
                    success: function(return_value) {
7774
                        $("#ajaxquestiondiv'.$questionId.'").html(return_value);
7775
                    }
7776
                });
7777
            });
7778
            </script>
7779
            <div id="ajaxquestiondiv'.$questionId.'"></div>';
7780
            echo $script;
7781
        } else {
7782
            $origin = api_get_origin();
7783
            $question_obj = Question::read($questionId);
7784
            $user_choice = isset($attemptList[$questionId]) ? $attemptList[$questionId] : null;
7785
            $remind_highlight = null;
7786
7787
            // Hides questions when reviewing a ALL_ON_ONE_PAGE exercise
7788
            // see #4542 no_remind_highlight class hide with jquery
7789
            if (ALL_ON_ONE_PAGE == $this->type && isset($_GET['reminder']) && 2 == $_GET['reminder']) {
7790
                $remind_highlight = 'no_remind_highlight';
7791
                // @todo not implemented in 1.11.x
7792
                /*if (in_array($question_obj->type, Question::question_type_no_review())) {
7793
                    return null;
7794
                }*/
7795
            }
7796
7797
            $attributes = ['id' => 'remind_list['.$questionId.']'];
7798
7799
            // Showing the question
7800
            $exercise_actions = null;
7801
            echo '<a id="questionanchor'.$questionId.'"></a><br />';
7802
            echo '<div id="question_div_'.$questionId.'" class="main_question '.$remind_highlight.'" >';
7803
7804
            // Shows the question + possible answers
7805
            $showTitle = 1 == $this->getHideQuestionTitle() ? false : true;
7806
            // @todo not implemented in 1.11.x
7807
            /*echo $this->showQuestion(
7808
                $question_obj,
7809
                false,
7810
                $origin,
7811
                $i,
7812
                $showTitle,
7813
                false,
7814
                $user_choice,
7815
                false,
7816
                null,
7817
                false,
7818
                $this->getModelType(),
7819
                $this->categoryMinusOne
7820
            );*/
7821
7822
            // Button save and continue
7823
            switch ($this->type) {
7824
                case ONE_PER_PAGE:
7825
                    $exercise_actions .= $this->show_button(
7826
                        $questionId,
7827
                        $current_question,
7828
                        null,
7829
                        $remindList
7830
                    );
7831
7832
                    break;
7833
                case ALL_ON_ONE_PAGE:
7834
                    if (api_is_allowed_to_session_edit()) {
7835
                        $button = [
7836
                            Display::button(
7837
                                'save_now',
7838
                                get_lang('Save and continue'),
7839
                                ['type' => 'button', 'class' => 'btn btn--primary', 'data-question' => $questionId]
7840
                            ),
7841
                            '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>',
7842
                        ];
7843
                        $exercise_actions .= Display::div(
7844
                            implode(PHP_EOL, $button),
7845
                            ['class' => 'exercise_save_now_button mb-4']
7846
                        );
7847
                    }
7848
7849
                    break;
7850
            }
7851
7852
            if (!empty($questions_in_media)) {
7853
                $count_of_questions_inside_media = count($questions_in_media);
7854
                if ($count_of_questions_inside_media > 1 && 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>&nbsp;',
7862
                    ];
7863
                    $exercise_actions = Display::div(
7864
                        implode(PHP_EOL, $button),
7865
                        ['class' => 'exercise_save_now_button mb-4']
7866
                    );
7867
                }
7868
7869
                if ($last_question_in_media && ONE_PER_PAGE == $this->type) {
7870
                    $exercise_actions = $this->show_button($questionId, $current_question, $questions_in_media);
7871
                }
7872
            }
7873
7874
            // Checkbox review answers. Not implemented.
7875
            /*if ($this->review_answers &&
7876
                !in_array($question_obj->type, Question::question_type_no_review())
7877
            ) {
7878
                $remind_question_div = Display::tag(
7879
                    'label',
7880
                    Display::input(
7881
                        'checkbox',
7882
                        'remind_list['.$questionId.']',
7883
                        '',
7884
                        $attributes
7885
                    ).get_lang('Revise question later'),
7886
                    [
7887
                        'class' => 'checkbox',
7888
                        'for' => 'remind_list['.$questionId.']',
7889
                    ]
7890
                );
7891
                $exercise_actions .= Display::div(
7892
                    $remind_question_div,
7893
                    ['class' => 'exercise_save_now_button']
7894
                );
7895
            }*/
7896
7897
            echo Display::div(' ', ['class' => 'clear']);
7898
7899
            $paginationCounter = null;
7900
            if (ONE_PER_PAGE == $this->type) {
7901
                if (empty($questions_in_media)) {
7902
                    $paginationCounter = Display::paginationIndicator(
7903
                        $current_question,
7904
                        count($realQuestionList)
7905
                    );
7906
                } else {
7907
                    if ($last_question_in_media) {
7908
                        $paginationCounter = Display::paginationIndicator(
7909
                            $current_question,
7910
                            count($realQuestionList)
7911
                        );
7912
                    }
7913
                }
7914
            }
7915
7916
            echo '<div class="row"><div class="pull-right">'.$paginationCounter.'</div></div>';
7917
            echo Display::div($exercise_actions, ['class' => 'form-actions']);
7918
            echo '</div>';
7919
        }
7920
    }
7921
7922
    /**
7923
     * Returns an array of categories details for the questions of the current
7924
     * exercise.
7925
     *
7926
     * @return array
7927
     */
7928
    public function getQuestionWithCategories()
7929
    {
7930
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
7931
        $categoryRelTable = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
7932
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
7933
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
7934
        $sql = "SELECT DISTINCT cat.*
7935
                FROM $TBL_EXERCICE_QUESTION e
7936
                INNER JOIN $TBL_QUESTIONS q
7937
                ON (e.question_id = q.iid)
7938
                INNER JOIN $categoryRelTable catRel
7939
                ON (catRel.question_id = e.question_id)
7940
                INNER JOIN $categoryTable cat
7941
                ON (cat.iid = catRel.category_id)
7942
                WHERE
7943
                  e.quiz_id	= ".(int) ($this->getId());
7944
7945
        $result = Database::query($sql);
7946
        $categoriesInExercise = [];
7947
        if (Database::num_rows($result)) {
7948
            $categoriesInExercise = Database::store_result($result, 'ASSOC');
7949
        }
7950
7951
        return $categoriesInExercise;
7952
    }
7953
7954
    /**
7955
     * Calculate the max_score of the quiz, depending of question inside, and quiz advanced option.
7956
     */
7957
    public function getMaxScore()
7958
    {
7959
        $outMaxScore = 0;
7960
        // list of question's id !!! the array key start at 1 !!!
7961
        $questionList = $this->selectQuestionList(true);
7962
7963
        if ($this->random > 0 && $this->randomByCat > 0) {
7964
            // test is random by category
7965
            // get the $numberRandomQuestions best score question of each category
7966
            $numberRandomQuestions = $this->random;
7967
            $tabCategoriesScores = [];
7968
            foreach ($questionList as $questionId) {
7969
                $questionCategoryId = TestCategory::getCategoryForQuestion($questionId);
7970
                if (!is_array($tabCategoriesScores[$questionCategoryId])) {
7971
                    $tabCategoriesScores[$questionCategoryId] = [];
7972
                }
7973
                $tmpObjQuestion = Question::read($questionId);
7974
                if (is_object($tmpObjQuestion)) {
7975
                    $tabCategoriesScores[$questionCategoryId][] = $tmpObjQuestion->weighting;
7976
                }
7977
            }
7978
7979
            // here we've got an array with first key, the category_id, second key, score of question for this cat
7980
            foreach ($tabCategoriesScores as $tabScores) {
7981
                rsort($tabScores);
7982
                $tabScoresCount = count($tabScores);
7983
                for ($i = 0; $i < min($numberRandomQuestions, $tabScoresCount); $i++) {
7984
                    $outMaxScore += $tabScores[$i];
7985
                }
7986
            }
7987
7988
            return $outMaxScore;
7989
        }
7990
7991
        // standard test, just add each question score
7992
        foreach ($questionList as $questionId) {
7993
            $question = Question::read($questionId, $this->course);
7994
            $outMaxScore += $question->weighting;
7995
        }
7996
7997
        return $outMaxScore;
7998
    }
7999
8000
    /**
8001
     * @return string
8002
     */
8003
    public function get_formated_title()
8004
    {
8005
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
8006
        }
8007
8008
        return api_html_entity_decode($this->selectTitle());
8009
    }
8010
8011
    /**
8012
     * @param string $title
8013
     *
8014
     * @return string
8015
     */
8016
    public static function get_formated_title_variable($title)
8017
    {
8018
        return api_html_entity_decode($title);
8019
    }
8020
8021
    /**
8022
     * @return string
8023
     */
8024
    public function format_title()
8025
    {
8026
        return api_htmlentities($this->title);
8027
    }
8028
8029
    /**
8030
     * @param string $title
8031
     *
8032
     * @return string
8033
     */
8034
    public static function format_title_variable($title)
8035
    {
8036
        return api_htmlentities($title);
8037
    }
8038
8039
    /**
8040
     * @param int $courseId
8041
     * @param int $sessionId
8042
     *
8043
     * @return array exercises
8044
     */
8045
    public function getExercisesByCourseSession($courseId, $sessionId)
8046
    {
8047
        $courseId = (int) $courseId;
8048
        $sessionId = (int) $sessionId;
8049
8050
        $tbl_quiz = Database::get_course_table(TABLE_QUIZ_TEST);
8051
        $sql = "SELECT * FROM $tbl_quiz cq
8052
                WHERE
8053
                    cq.c_id = %s AND
8054
                    (cq.session_id = %s OR cq.session_id = 0) AND
8055
                    cq.active = 0
8056
                ORDER BY cq.iid";
8057
        $sql = sprintf($sql, $courseId, $sessionId);
8058
8059
        $result = Database::query($sql);
8060
8061
        $rows = [];
8062
        while ($row = Database::fetch_assoc($result)) {
8063
            $rows[] = $row;
8064
        }
8065
8066
        return $rows;
8067
    }
8068
8069
    /**
8070
     * @param int   $courseId
8071
     * @param int   $sessionId
8072
     * @param array $quizId
8073
     *
8074
     * @return array exercises
8075
     */
8076
    public function getExerciseAndResult($courseId, $sessionId, $quizId = [])
8077
    {
8078
        if (empty($quizId)) {
8079
            return [];
8080
        }
8081
8082
        $sessionId = (int) $sessionId;
8083
        $courseId = (int) $courseId;
8084
8085
        $ids = is_array($quizId) ? $quizId : [$quizId];
8086
        $ids = array_map('intval', $ids);
8087
        $ids = implode(',', $ids);
8088
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8089
        if (0 != $sessionId) {
8090
            $sql = "SELECT * FROM $track_exercises te
8091
              INNER JOIN c_quiz cq
8092
              ON cq.iid = te.exe_exo_id
8093
              WHERE
8094
              te.c_id = %d AND
8095
              te.session_id = %s AND
8096
              cq.iid IN (%s)
8097
              ORDER BY cq.iid";
8098
8099
            $sql = sprintf($sql, $courseId, $sessionId, $ids);
8100
        } else {
8101
            $sql = "SELECT * FROM $track_exercises te
8102
              INNER JOIN c_quiz cq ON cq.iid = te.exe_exo_id
8103
              WHERE
8104
              te.c_id = %d AND
8105
              cq.iid IN (%s)
8106
              ORDER BY cq.iid";
8107
            $sql = sprintf($sql, $courseId, $ids);
8108
        }
8109
        $result = Database::query($sql);
8110
        $rows = [];
8111
        while ($row = Database::fetch_assoc($result)) {
8112
            $rows[] = $row;
8113
        }
8114
8115
        return $rows;
8116
    }
8117
8118
    /**
8119
     * @param $exeId
8120
     * @param $exercise_stat_info
8121
     * @param $remindList
8122
     * @param $currentQuestion
8123
     *
8124
     * @return int|null
8125
     */
8126
    public static function getNextQuestionId(
8127
        $exeId,
8128
        $exercise_stat_info,
8129
        $remindList,
8130
        $currentQuestion
8131
    ) {
8132
        $result = Event::get_exercise_results_by_attempt($exeId, 'incomplete');
8133
8134
        if (isset($result[$exeId])) {
8135
            $result = $result[$exeId];
8136
        } else {
8137
            return null;
8138
        }
8139
8140
        $data_tracking = $exercise_stat_info['data_tracking'];
8141
        $data_tracking = explode(',', $data_tracking);
8142
8143
        // if this is the final question do nothing.
8144
        if ($currentQuestion == count($data_tracking)) {
8145
            return null;
8146
        }
8147
8148
        $currentQuestion--;
8149
8150
        if (!empty($result['question_list'])) {
8151
            $answeredQuestions = [];
8152
            foreach ($result['question_list'] as $question) {
8153
                if (!empty($question['answer'])) {
8154
                    $answeredQuestions[] = $question['question_id'];
8155
                }
8156
            }
8157
8158
            // Checking answered questions
8159
            $counterAnsweredQuestions = 0;
8160
            foreach ($data_tracking as $questionId) {
8161
                if (!in_array($questionId, $answeredQuestions)) {
8162
                    if ($currentQuestion != $counterAnsweredQuestions) {
8163
                        break;
8164
                    }
8165
                }
8166
                $counterAnsweredQuestions++;
8167
            }
8168
8169
            $counterRemindListQuestions = 0;
8170
            // Checking questions saved in the reminder list
8171
            if (!empty($remindList)) {
8172
                foreach ($data_tracking as $questionId) {
8173
                    if (in_array($questionId, $remindList)) {
8174
                        // Skip the current question
8175
                        if ($currentQuestion != $counterRemindListQuestions) {
8176
                            break;
8177
                        }
8178
                    }
8179
                    $counterRemindListQuestions++;
8180
                }
8181
8182
                if ($counterRemindListQuestions < $currentQuestion) {
8183
                    return null;
8184
                }
8185
8186
                if (!empty($counterRemindListQuestions)) {
8187
                    if ($counterRemindListQuestions > $counterAnsweredQuestions) {
8188
                        return $counterAnsweredQuestions;
8189
                    } else {
8190
                        return $counterRemindListQuestions;
8191
                    }
8192
                }
8193
            }
8194
8195
            return $counterAnsweredQuestions;
8196
        }
8197
    }
8198
8199
    /**
8200
     * Gets the position of a questionId in the question list.
8201
     *
8202
     * @param $questionId
8203
     *
8204
     * @return int
8205
     */
8206
    public function getPositionInCompressedQuestionList($questionId)
8207
    {
8208
        $questionList = $this->getQuestionListWithMediasCompressed();
8209
        $mediaQuestions = $this->getMediaList();
8210
        $position = 1;
8211
        foreach ($questionList as $id) {
8212
            if (isset($mediaQuestions[$id]) && in_array($questionId, $mediaQuestions[$id])) {
8213
                $mediaQuestionList = $mediaQuestions[$id];
8214
                if (in_array($questionId, $mediaQuestionList)) {
8215
                    return $position;
8216
                } else {
8217
                    $position++;
8218
                }
8219
            } else {
8220
                if ($id == $questionId) {
8221
                    return $position;
8222
                } else {
8223
                    $position++;
8224
                }
8225
            }
8226
        }
8227
8228
        return 1;
8229
    }
8230
8231
    /**
8232
     * Get the correct answers in all attempts.
8233
     *
8234
     * @param int  $learnPathId
8235
     * @param int  $learnPathItemId
8236
     * @param bool $onlyCorrect
8237
     *
8238
     * @return array
8239
     */
8240
    public function getAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0, $onlyCorrect = true)
8241
    {
8242
        $attempts = Event::getExerciseResultsByUser(
8243
            api_get_user_id(),
8244
            $this->getId(),
8245
            api_get_course_int_id(),
8246
            api_get_session_id(),
8247
            $learnPathId,
8248
            $learnPathItemId,
8249
            'DESC'
8250
        );
8251
8252
        $list = [];
8253
        foreach ($attempts as $attempt) {
8254
            foreach ($attempt['question_list'] as $answers) {
8255
                foreach ($answers as $answer) {
8256
                    $objAnswer = new Answer($answer['question_id']);
8257
                    if ($onlyCorrect) {
8258
                        switch ($objAnswer->getQuestionType()) {
8259
                            case FILL_IN_BLANKS:
8260
                                $isCorrect = FillBlanks::isCorrect($answer['answer']);
8261
8262
                                break;
8263
                            case MATCHING:
8264
                            case DRAGGABLE:
8265
                            case MATCHING_DRAGGABLE:
8266
                                $isCorrect = Matching::isCorrect(
8267
                                    $answer['position'],
8268
                                    $answer['answer'],
8269
                                    $answer['question_id']
8270
                                );
8271
8272
                                break;
8273
                            case ORAL_EXPRESSION:
8274
                                $isCorrect = false;
8275
8276
                                break;
8277
                            default:
8278
                                $isCorrect = $objAnswer->isCorrectByAutoId($answer['answer']);
8279
                        }
8280
                        if ($isCorrect) {
8281
                            $list[$answer['question_id']][] = $answer;
8282
                        }
8283
                    } else {
8284
                        $list[$answer['question_id']][] = $answer;
8285
                    }
8286
                }
8287
            }
8288
8289
            if (false === $onlyCorrect) {
8290
                // Only take latest attempt
8291
                break;
8292
            }
8293
        }
8294
8295
        return $list;
8296
    }
8297
8298
    /**
8299
     * Get the correct answers in all attempts.
8300
     *
8301
     * @param int $learnPathId
8302
     * @param int $learnPathItemId
8303
     *
8304
     * @return array
8305
     */
8306
    public function getCorrectAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0)
8307
    {
8308
        return $this->getAnswersInAllAttempts($learnPathId, $learnPathItemId);
8309
    }
8310
8311
    /**
8312
     * @return bool
8313
     */
8314
    public function showPreviousButton()
8315
    {
8316
        $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
8317
        if (false === $allow) {
8318
            return true;
8319
        }
8320
8321
        return $this->showPreviousButton;
8322
    }
8323
8324
    public function getPreventBackwards()
8325
    {
8326
        return (int) $this->preventBackwards;
8327
    }
8328
8329
    /**
8330
     * @return int
8331
     */
8332
    public function getQuizCategoryId(): ?int
8333
    {
8334
        if (empty($this->quizCategoryId)) {
8335
            return null;
8336
        }
8337
8338
        return (int) $this->quizCategoryId;
8339
    }
8340
8341
    /**
8342
     * @param int $value
8343
     */
8344
    public function setQuizCategoryId($value): void
8345
    {
8346
        if (!empty($value)) {
8347
            $this->quizCategoryId = (int) $value;
8348
        }
8349
    }
8350
8351
    /**
8352
     * Set the value to 1 to hide the question number.
8353
     *
8354
     * @param int $value
8355
     */
8356
    public function setHideQuestionNumber($value = 0)
8357
    {
8358
        $this->hideQuestionNumber = (int) $value;
8359
    }
8360
8361
    /**
8362
     * Gets the value to hide or show the question number. If it does not exist, it is set to 0.
8363
     *
8364
     * @return int 1 if the question number must be hidden
8365
     */
8366
    public function getHideQuestionNumber()
8367
    {
8368
        return (int) $this->hideQuestionNumber;
8369
    }
8370
8371
    public function setPageResultConfiguration(array $values)
8372
    {
8373
        $pageConfig = ('true' === api_get_setting('exercise.allow_quiz_results_page_config'));
8374
        if ($pageConfig) {
8375
            $params = [
8376
                'hide_expected_answer' => $values['hide_expected_answer'] ?? '',
8377
                'hide_question_score' => $values['hide_question_score'] ?? '',
8378
                'hide_total_score' => $values['hide_total_score'] ?? '',
8379
                'hide_category_table' => $values['hide_category_table'] ?? '',
8380
                'hide_correct_answered_questions' => $values['hide_correct_answered_questions'] ?? '',
8381
            ];
8382
            $this->pageResultConfiguration = $params;
8383
        }
8384
    }
8385
8386
    /**
8387
     * @param array $defaults
8388
     */
8389
    public function setPageResultConfigurationDefaults(&$defaults)
8390
    {
8391
        $configuration = $this->getPageResultConfiguration();
8392
        if (!empty($configuration) && !empty($defaults)) {
8393
            $defaults = array_merge($defaults, $configuration);
8394
        }
8395
    }
8396
8397
    /**
8398
     * @return array
8399
     */
8400
    public function getPageResultConfiguration()
8401
    {
8402
        $pageConfig = ('true' === api_get_setting('exercise.allow_quiz_results_page_config'));
8403
        if ($pageConfig) {
8404
            return $this->pageResultConfiguration;
8405
        }
8406
8407
        return [];
8408
    }
8409
8410
    /**
8411
     * @param string $attribute
8412
     *
8413
     * @return mixed|null
8414
     */
8415
    public function getPageConfigurationAttribute($attribute)
8416
    {
8417
        $result = $this->getPageResultConfiguration();
8418
8419
        if (!empty($result)) {
8420
            return $result[$attribute] ?? null;
8421
        }
8422
8423
        return null;
8424
    }
8425
8426
    /**
8427
     * @param bool $showPreviousButton
8428
     *
8429
     * @return Exercise
8430
     */
8431
    public function setShowPreviousButton($showPreviousButton)
8432
    {
8433
        $this->showPreviousButton = $showPreviousButton;
8434
8435
        return $this;
8436
    }
8437
8438
    /**
8439
     * @param array $notifications
8440
     */
8441
    public function setNotifications($notifications)
8442
    {
8443
        $this->notifications = $notifications;
8444
    }
8445
8446
    /**
8447
     * @return array
8448
     */
8449
    public function getNotifications()
8450
    {
8451
        return $this->notifications;
8452
    }
8453
8454
    /**
8455
     * @return bool
8456
     */
8457
    public function showExpectedChoice()
8458
    {
8459
        return ('true' === api_get_setting('exercise.show_exercise_expected_choice'));
8460
    }
8461
8462
    /**
8463
     * @return bool
8464
     */
8465
    public function showExpectedChoiceColumn()
8466
    {
8467
        if (true === $this->forceShowExpectedChoiceColumn) {
8468
            return true;
8469
        }
8470
        if ($this->hideExpectedAnswer) {
8471
            return false;
8472
        }
8473
        if (!in_array(
8474
            $this->results_disabled,
8475
            [
8476
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
8477
            ]
8478
        )
8479
        ) {
8480
            $hide = (int) $this->getPageConfigurationAttribute('hide_expected_answer');
8481
            if (1 === $hide) {
8482
                return false;
8483
            }
8484
8485
            return true;
8486
        }
8487
8488
        return false;
8489
    }
8490
8491
    public function getQuestionRibbon(string $class, string $scoreLabel, ?string $result, array $array): string
8492
    {
8493
        $hide = (int) $this->getPageConfigurationAttribute('hide_question_score');
8494
        if (1 === $hide) {
8495
            return '';
8496
        }
8497
8498
        $ribbon = '<div class="question-answer-result__header-ribbon-title question-answer-result__header-ribbon-title--'.$class.'">'.$scoreLabel.'</div>';
8499
        if (!empty($result)) {
8500
            $ribbon .= '<div class="question-answer-result__header-ribbon-detail">'
8501
                .get_lang('Score').': '.$result
8502
                .'</div>';
8503
        }
8504
8505
        $ribbonClassModifier = '';
8506
8507
        if ($this->showExpectedChoice()) {
8508
            $hideLabel = ('true' === api_get_setting('exercise.exercise_hide_label'));
8509
            if (true === $hideLabel) {
8510
                $ribbonClassModifier = 'question-answer-result__header-ribbon--no-ribbon';
8511
                $html = '';
8512
                $answerUsed = (int) $array['used'];
8513
                $answerMissing = (int) $array['missing'] - $answerUsed;
8514
                for ($i = 1; $i <= $answerUsed; $i++) {
8515
                    $html .= Display::getMdiIcon(StateIcon::COMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL);
8516
                }
8517
                for ($i = 1; $i <= $answerMissing; $i++) {
8518
                    $html .= Display::getMdiIcon(StateIcon::INCOMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL);
8519
                }
8520
                $ribbon = '<div class="question-answer-result__header-ribbon-title hide-label-title">'
8521
                    .get_lang('Correct answers').': '.$result.'</div>'
8522
                    .'<div class="question-answer-result__header-ribbon-detail">'.$html.'</div>';
8523
            }
8524
        }
8525
8526
        return Display::div(
8527
            $ribbon,
8528
            ['class' => "question-answer-result__header-ribbon $ribbonClassModifier"]
8529
        );
8530
    }
8531
8532
    /**
8533
     * @return int
8534
     */
8535
    public function getAutoLaunch()
8536
    {
8537
        return $this->autolaunch;
8538
    }
8539
8540
    /**
8541
     * Clean auto launch settings for all exercise in course/course-session.
8542
     */
8543
    public function enableAutoLaunch()
8544
    {
8545
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8546
        $sql = "UPDATE $table SET autolaunch = 1
8547
                WHERE iid = ".$this->iId;
8548
        Database::query($sql);
8549
    }
8550
8551
    /**
8552
     * Clean auto launch settings for all exercise in course/course-session.
8553
     */
8554
    public function cleanCourseLaunchSettings()
8555
    {
8556
        $em = Database::getManager();
8557
8558
        $repo = Container::getQuizRepository();
8559
8560
        $session = api_get_session_entity();
8561
        $course = api_get_course_entity();
8562
8563
        $qb = $repo->getResourcesByCourse($course, $session);
8564
        $quizzes = $qb->getQuery()->getResult();
8565
8566
        foreach ($quizzes as $quiz) {
8567
            $quiz->setAutoLaunch(false);
8568
            $em->persist($quiz);
8569
        }
8570
8571
        $em->flush();
8572
    }
8573
8574
    /**
8575
     * Get the title without HTML tags.
8576
     *
8577
     * @return string
8578
     */
8579
    public function getUnformattedTitle()
8580
    {
8581
        return strip_tags(api_html_entity_decode($this->title));
8582
    }
8583
8584
    /**
8585
     * Get the question IDs from quiz_rel_question for the current quiz,
8586
     * using the parameters as the arguments to the SQL's LIMIT clause.
8587
     * Because the exercise_id is known, it also comes with a filter on
8588
     * the session, so sessions are not specified here.
8589
     *
8590
     * @param int $start  At which question do we want to start the list
8591
     * @param int $length Up to how many results we want
8592
     *
8593
     * @return array A list of question IDs
8594
     */
8595
    public function getQuestionForTeacher($start = 0, $length = 10)
8596
    {
8597
        $start = (int) $start;
8598
        if ($start < 0) {
8599
            $start = 0;
8600
        }
8601
8602
        $length = (int) $length;
8603
8604
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8605
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
8606
        $sql = "SELECT DISTINCT e.question_id
8607
                FROM $quizRelQuestion e
8608
                INNER JOIN $question q
8609
                ON (e.question_id = q.iid)
8610
                WHERE
8611
8612
                    e.quiz_id = '".$this->getId()."'
8613
                ORDER BY question_order
8614
                LIMIT $start, $length
8615
            ";
8616
        $result = Database::query($sql);
8617
        $questionList = [];
8618
        while ($object = Database::fetch_object($result)) {
8619
            $questionList[] = $object->question_id;
8620
        }
8621
8622
        return $questionList;
8623
    }
8624
8625
    /**
8626
     * @param int   $exerciseId
8627
     * @param array $courseInfo
8628
     * @param int   $sessionId
8629
     *
8630
     * @return bool
8631
     */
8632
    public function generateStats($exerciseId, $courseInfo, $sessionId)
8633
    {
8634
        $allowStats = ('true' === api_get_setting('gradebook.allow_gradebook_stats'));
8635
        if (!$allowStats) {
8636
            return false;
8637
        }
8638
8639
        if (empty($courseInfo)) {
8640
            return false;
8641
        }
8642
8643
        $courseId = $courseInfo['real_id'];
8644
8645
        $sessionId = (int) $sessionId;
8646
        $exerciseId = (int) $exerciseId;
8647
8648
        $result = $this->read($exerciseId);
8649
8650
        if (empty($result)) {
8651
            api_not_allowed(true);
8652
        }
8653
8654
        $statusToFilter = empty($sessionId) ? STUDENT : 0;
8655
8656
        $studentList = CourseManager::get_user_list_from_course_code(
8657
            $courseInfo['code'],
8658
            $sessionId,
8659
            null,
8660
            null,
8661
            $statusToFilter
8662
        );
8663
8664
        if (empty($studentList)) {
8665
            Display::addFlash(Display::return_message(get_lang('No users in course')));
8666
            header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
8667
            exit;
8668
        }
8669
8670
        $tblStats = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8671
8672
        $studentIdList = [];
8673
        if (!empty($studentList)) {
8674
            $studentIdList = array_column($studentList, 'user_id');
8675
        }
8676
8677
        $sessionCondition = api_get_session_condition($sessionId);
8678
        if (false == $this->exercise_was_added_in_lp) {
8679
            $sql = "SELECT * FROM $tblStats
8680
                        WHERE
8681
                            exe_exo_id = $exerciseId AND
8682
                            orig_lp_id = 0 AND
8683
                            orig_lp_item_id = 0 AND
8684
                            status <> 'incomplete' AND
8685
                            c_id = $courseId
8686
                            $sessionCondition
8687
                        ";
8688
        } else {
8689
            $lpId = null;
8690
            if (!empty($this->lpList)) {
8691
                // Taking only the first LP
8692
                $lpId = $this->getLpBySession($sessionId);
8693
                $lpId = $lpId['lp_id'];
8694
            }
8695
8696
            $sql = "SELECT *
8697
                        FROM $tblStats
8698
                        WHERE
8699
                            exe_exo_id = $exerciseId AND
8700
                            orig_lp_id = $lpId AND
8701
                            status <> 'incomplete' AND
8702
                            session_id = $sessionId AND
8703
                            c_id = $courseId ";
8704
        }
8705
8706
        $sql .= ' ORDER BY exe_id DESC';
8707
8708
        $studentCount = 0;
8709
        $sum = 0;
8710
        $bestResult = 0;
8711
        $sumResult = 0;
8712
        $result = Database::query($sql);
8713
        while ($data = Database::fetch_assoc($result)) {
8714
            // Only take into account users in the current student list.
8715
            if (!empty($studentIdList)) {
8716
                if (!in_array($data['exe_user_id'], $studentIdList)) {
8717
                    continue;
8718
                }
8719
            }
8720
8721
            if (!isset($students[$data['exe_user_id']])) {
8722
                if (0 != $data['max_score']) {
8723
                    $students[$data['exe_user_id']] = $data['score'];
8724
                    if ($data['score'] > $bestResult) {
8725
                        $bestResult = $data['score'];
8726
                    }
8727
                    $sumResult += $data['score'];
8728
                }
8729
            }
8730
        }
8731
8732
        $count = count($studentList);
8733
        $average = $sumResult / $count;
8734
        $em = Database::getManager();
8735
8736
        $links = AbstractLink::getGradebookLinksFromItem(
8737
            $this->getId(),
8738
            LINK_EXERCISE,
8739
            $courseInfo['real_id'],
8740
            $sessionId
8741
        );
8742
8743
        if (empty($links)) {
8744
            $links = AbstractLink::getGradebookLinksFromItem(
8745
                $this->iId,
8746
                LINK_EXERCISE,
8747
                $courseInfo['real_id'],
8748
                $sessionId
8749
            );
8750
        }
8751
8752
        if (!empty($links)) {
8753
            $repo = $em->getRepository(GradebookLink::class);
8754
8755
            foreach ($links as $link) {
8756
                $linkId = $link['id'];
8757
                /** @var GradebookLink $exerciseLink */
8758
                $exerciseLink = $repo->find($linkId);
8759
                if ($exerciseLink) {
8760
                    $exerciseLink
8761
                        ->setUserScoreList($students)
8762
                        ->setBestScore($bestResult)
8763
                        ->setAverageScore($average)
8764
                        ->setScoreWeight($this->getMaxScore());
8765
                    $em->persist($exerciseLink);
8766
                    $em->flush();
8767
                }
8768
            }
8769
        }
8770
    }
8771
8772
    /**
8773
     * Return an HTML table of exercises for on-screen printing, including
8774
     * action icons. If no exercise is present and the user can edit the
8775
     * course, show a "create test" button.
8776
     *
8777
     * @param int    $categoryId
8778
     * @param string $keyword
8779
     * @param int    $userId
8780
     * @param int    $courseId
8781
     * @param int    $sessionId
8782
     * @param bool   $returnData
8783
     * @param int    $minCategoriesInExercise
8784
     * @param int    $filterByResultDisabled
8785
     * @param int    $filterByAttempt
8786
     *
8787
     * @return string|SortableTableFromArrayConfig
8788
     */
8789
    public static function exerciseGridResource(
8790
        $categoryId,
8791
        $keyword = '',
8792
        $userId = 0,
8793
        $courseId = 0,
8794
        $sessionId = 0,
8795
        $returnData = false,
8796
        $minCategoriesInExercise = 0,
8797
        $filterByResultDisabled = 0,
8798
        $filterByAttempt = 0,
8799
        $myActions = null,
8800
        $returnTable = false
8801
    ) {
8802
        $is_allowedToEdit = api_is_allowed_to_edit(null, true);
8803
        $courseId = $courseId ? (int) $courseId : api_get_course_int_id();
8804
        $sessionId = $sessionId ? (int) $sessionId : api_get_session_id();
8805
8806
        $course = api_get_course_entity($courseId);
8807
        $session = api_get_session_entity($sessionId);
8808
8809
        $userId = $userId ? (int) $userId : api_get_user_id();
8810
        $user = api_get_user_entity($userId);
8811
8812
        $repo = Container::getQuizRepository();
8813
8814
        $trackEExerciseRepo = Container::getTrackEExerciseRepository();
8815
        $pendingCorrections = $trackEExerciseRepo->getPendingCorrectionsByExercise($courseId);
8816
        $pendingAttempts = [];
8817
        foreach ($pendingCorrections as $correction) {
8818
            $pendingAttempts[$correction['exerciseId']] = $correction['pendingCount'];
8819
        }
8820
8821
        // 2. Get query builder from repo.
8822
        $qb = $repo->getResourcesByCourse($course, $session);
8823
8824
        if (!empty($categoryId)) {
8825
            $qb->andWhere($qb->expr()->eq('resource.quizCategory', $categoryId));
8826
        } else {
8827
            $qb->andWhere($qb->expr()->isNull('resource.quizCategory'));
8828
        }
8829
8830
        $allowDelete = self::allowAction('delete');
8831
        $allowClean = self::allowAction('clean_results');
8832
8833
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8834
8835
        $categoryId = (int) $categoryId;
8836
        $keyword = Database::escape_string($keyword);
8837
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
8838
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
8839
8840
        $courseId = $course->getId();
8841
        $tableRows = [];
8842
        $origin = api_get_origin();
8843
        $charset = 'utf-8';
8844
        $token = Security::get_token();
8845
        $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh($userId, ['real_id' => $courseId]);
8846
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
8847
        $content = '';
8848
        $column = 0;
8849
        if ($is_allowedToEdit) {
8850
            $column = 1;
8851
        }
8852
8853
        $table = new SortableTableFromArrayConfig(
8854
            [],
8855
            $column,
8856
            self::PAGINATION_ITEMS_PER_PAGE,
8857
            'exercises_cat_'.$categoryId.'_'.api_get_course_int_id().'_'.api_get_session_id()
8858
        );
8859
8860
        $limit = $table->per_page;
8861
        $page = $table->page_nr;
8862
        $from = $limit * ($page - 1);
8863
8864
        $categoryCondition = '';
8865
        if ('true' === api_get_setting('exercise.allow_exercise_categories')) {
8866
            if (!empty($categoryId)) {
8867
                $categoryCondition = " AND quiz_category_id = $categoryId ";
8868
            } else {
8869
                $categoryCondition = ' AND quiz_category_id IS NULL ';
8870
            }
8871
        }
8872
8873
        if (!empty($keyword)) {
8874
            $qb->andWhere($qb->expr()->like('resource.title', ':keyword'));
8875
            $qb->setParameter('keyword', '%'.$keyword.'%');
8876
        }
8877
8878
        // Only for administrators
8879
        if ($is_allowedToEdit) {
8880
            $qb->andWhere($qb->expr()->neq('resource.active', -1));
8881
        } else {
8882
            $qb->andWhere($qb->expr()->eq('resource.active', 1));
8883
        }
8884
8885
        $qb->setFirstResult($from);
8886
        $qb->setMaxResults($limit);
8887
8888
        $filterByResultDisabledCondition = '';
8889
        $filterByResultDisabled = (int) $filterByResultDisabled;
8890
        if (!empty($filterByResultDisabled)) {
8891
            $filterByResultDisabledCondition = ' AND e.results_disabled = '.$filterByResultDisabled;
8892
        }
8893
        $filterByAttemptCondition = '';
8894
        $filterByAttempt = (int) $filterByAttempt;
8895
        if (!empty($filterByAttempt)) {
8896
            $filterByAttemptCondition = ' AND e.max_attempt = '.$filterByAttempt;
8897
        }
8898
8899
        $exerciseList = $qb->getQuery()->getResult();
8900
8901
        $total = $repo->getCount($qb);
8902
8903
        $webPath = api_get_path(WEB_CODE_PATH);
8904
        if (!empty($exerciseList)) {
8905
            $visibilitySetting = ('true' === api_get_setting('lp.show_hidden_exercise_added_to_lp'));
8906
            //avoid sending empty parameters
8907
            $mylpid = empty($learnpath_id) ? '' : '&learnpath_id='.$learnpath_id;
8908
            $mylpitemid = empty($learnpath_item_id) ? '' : '&learnpath_item_id='.$learnpath_item_id;
8909
8910
            /** @var CQuiz $exerciseEntity */
8911
            foreach ($exerciseList as $exerciseEntity) {
8912
                $currentRow = [];
8913
                $exerciseId = $exerciseEntity->getIid();
8914
                $actions = '';
8915
                $attempt_text = '';
8916
                $exercise = new Exercise($courseId);
8917
                $exercise->read($exerciseId, false);
8918
8919
                if (empty($exercise->iId)) {
8920
                    continue;
8921
                }
8922
8923
                $sessionId = api_get_session_id();
8924
                $allowToEditBaseCourse = true;
8925
                $visibility = $visibilityInCourse = $exerciseEntity->isVisible($course);
8926
                $visibilityInSession = false;
8927
                if (!empty($sessionId)) {
8928
                    // If we are in a session, the test is invisible
8929
                    // in the base course, it is included in a LP
8930
                    // *and* the setting to show it is *not*
8931
                    // specifically set to true, then hide it.
8932
                    if (false === $visibility) {
8933
                        if (!$visibilitySetting) {
8934
                            if ($exercise->exercise_was_added_in_lp) {
8935
                                continue;
8936
                            }
8937
                        }
8938
                    }
8939
8940
                    $visibility = $visibilityInSession = $exerciseEntity->isVisible($course, $session);
8941
                }
8942
8943
                // Validation when belongs to a session
8944
                $isBaseCourseExercise = true;
8945
                if (!($visibilityInCourse && $visibilityInSession)) {
8946
                    $isBaseCourseExercise = false;
8947
                }
8948
8949
                if (!empty($sessionId) && $isBaseCourseExercise) {
8950
                    $allowToEditBaseCourse = false;
8951
                }
8952
8953
                $resourceLink = $exerciseEntity->getFirstResourceLink();
8954
                if ($resourceLink && !$sessionId && $resourceLink->getSession() === null) {
8955
                    $allowToEditBaseCourse = true;
8956
                }
8957
8958
                $allowToEditSession = ($resourceLink && $resourceLink->getSession() && $resourceLink->getSession()->getId() === $sessionId);
8959
                $sessionStar = null;
8960
                if ($allowToEditSession) {
8961
                    $sessionStar = api_get_session_image($sessionId, $user);
8962
                }
8963
8964
                $locked = $exercise->is_gradebook_locked;
8965
8966
                $startTime = $exerciseEntity->getStartTime();
8967
                $endTime = $exerciseEntity->getEndTime();
8968
                $time_limits = false;
8969
                if (!empty($startTime) || !empty($endTime)) {
8970
                    $time_limits = true;
8971
                }
8972
8973
                $is_actived_time = false;
8974
                if ($time_limits) {
8975
                    // check if start time
8976
                    $start_time = false;
8977
                    if (!empty($startTime)) {
8978
                        $start_time = api_strtotime($startTime->format('Y-m-d H:i:s'), 'UTC');
8979
                    }
8980
                    $end_time = false;
8981
                    if (!empty($endTime)) {
8982
                        $end_time = api_strtotime($endTime->format('Y-m-d H:i:s'), 'UTC');
8983
                    }
8984
                    $now = time();
8985
                    //If both "clocks" are enable
8986
                    if ($start_time && $end_time) {
8987
                        if ($now > $start_time && $end_time > $now) {
8988
                            $is_actived_time = true;
8989
                        }
8990
                    } else {
8991
                        //we check the start and end
8992
                        if ($start_time) {
8993
                            if ($now > $start_time) {
8994
                                $is_actived_time = true;
8995
                            }
8996
                        }
8997
                        if ($end_time) {
8998
                            if ($end_time > $now) {
8999
                                $is_actived_time = true;
9000
                            }
9001
                        }
9002
                    }
9003
                }
9004
9005
                $cut_title = $exercise->getCutTitle();
9006
                $alt_title = '';
9007
                if ($cut_title != $exerciseEntity->getTitle()) {
9008
                    $alt_title = ' title = "'.$exercise->getUnformattedTitle().'" ';
9009
                }
9010
9011
                // Teacher only.
9012
                if ($is_allowedToEdit) {
9013
                    $lp_blocked = null;
9014
                    if (true == $exercise->exercise_was_added_in_lp) {
9015
                        $lp_blocked = Display::div(
9016
                            get_lang(
9017
                                '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.'
9018
                            ),
9019
                            ['class' => 'lp_content_type_label']
9020
                        );
9021
                    }
9022
9023
                    $style = '';
9024
                    if (!$visibility) {
9025
                        $style = 'color:grey';
9026
                    }
9027
9028
                    $title = $cut_title;
9029
9030
                    $url = '<a
9031
                        '.$alt_title.'
9032
                        id="tooltip_'.$exerciseId.'"
9033
                        href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$exerciseId.'"
9034
                        style = "'.$style.';float:left;"
9035
                        >
9036
                         '.Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, ICON_SIZE_SMALL, $title).$title.
9037
                        '</a>'.$sessionStar;
9038
9039
                    if (ExerciseLib::isQuizEmbeddable($exerciseEntity)) {
9040
                        $embeddableIcon = Display::getMdiIcon('book-music-outline', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('This quiz can be embeddable'));
9041
                        $url .= Display::div($embeddableIcon, ['class' => 'pull-right']);
9042
                    }
9043
9044
                    $pendingCount = $pendingAttempts[$exerciseId] ?? 0;
9045
                    if ($pendingCount > 0) {
9046
                        $pendingIcon = Display::getMdiIcon(
9047
                            ActionIcon::ALERT->value,
9048
                            'ch-tool-icon',
9049
                            null,
9050
                            ICON_SIZE_SMALL,
9051
                            get_lang('Pending attempts') . ": $pendingCount"
9052
                        );
9053
                        $url .= " $pendingIcon";
9054
                    }
9055
9056
                    $currentRow['title'] = $url.$lp_blocked;
9057
                    $rowi = $exerciseEntity->getQuestions()->count();
9058
                    if ($allowToEditBaseCourse || $allowToEditSession) {
9059
                        // Questions list
9060
                        $actions = Display::url(
9061
                            Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit')),
9062
                            'admin.php?'.api_get_cidreq().'&exerciseId='.$exerciseId
9063
                        );
9064
9065
                        // Test settings
9066
                        $settings = Display::url(
9067
                            Display::getMdiIcon(ToolIcon::SETTINGS, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Configure')),
9068
                            'exercise_admin.php?'.api_get_cidreq().'&exerciseId='.$exerciseId
9069
                        );
9070
9071
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9072
                            $settings = '';
9073
                        }
9074
                        $actions .= $settings;
9075
9076
                        // Exercise results
9077
                        $resultsLink = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9078
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).'</a>';
9079
9080
                        if ($limitTeacherAccess) {
9081
                            if (api_is_platform_admin()) {
9082
                                $actions .= $resultsLink;
9083
                            }
9084
                        } else {
9085
                            // Exercise results
9086
                            $actions .= $resultsLink;
9087
                        }
9088
9089
                        // Auto launch
9090
                        $autoLaunch = $exercise->getAutoLaunch();
9091
                        if (empty($autoLaunch)) {
9092
                            $actions .= Display::url(
9093
                                Display::getMdiIcon('rocket-launch', 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang('Enable')),
9094
                                'exercise.php?'.api_get_cidreq(
9095
                                ).'&action=enable_launch&sec_token='.$token.'&exerciseId='.$exerciseId
9096
                            );
9097
                        } else {
9098
                            $actions .= Display::url(
9099
                                Display::getMdiIcon('rocket-launch', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Disable')),
9100
                                'exercise.php?'.api_get_cidreq(
9101
                                ).'&action=disable_launch&sec_token='.$token.'&exerciseId='.$exerciseId
9102
                            );
9103
                        }
9104
9105
                        // Export
9106
                        $actions .= Display::url(
9107
                            Display::getMdiIcon('disc', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Copy this exercise as a new one')),
9108
                            '',
9109
                            [
9110
                                'onclick' => "javascript:if(!confirm('".addslashes(
9111
                                        api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES)
9112
                                    )." ".addslashes($title)."?"."')) return false;",
9113
                                'href' => 'exercise.php?'.api_get_cidreq(
9114
                                    ).'&action=copy_exercise&sec_token='.$token.'&exerciseId='.$exerciseId,
9115
                            ]
9116
                        );
9117
9118
                        // Clean exercise
9119
                        $clean = '';
9120
                        if (true === $allowClean) {
9121
                            if (!$locked) {
9122
                                $clean = Display::url(
9123
                                    Display::getMdiIcon(ActionIcon::RESET, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Clean student results')
9124
                                    ),
9125
                                    '',
9126
                                    [
9127
                                        'onclick' => "javascript:if(!confirm('".
9128
                                            addslashes(
9129
                                                api_htmlentities(
9130
                                                    get_lang('Are you sure to delete results'),
9131
                                                    ENT_QUOTES
9132
                                                )
9133
                                            )." ".addslashes($title)."?"."')) return false;",
9134
                                        'href' => 'exercise.php?'.api_get_cidreq(
9135
                                            ).'&action=clean_results&sec_token='.$token.'&exerciseId='.$exerciseId,
9136
                                    ]
9137
                                );
9138
                            } else {
9139
                                $clean = Display::getMdiIcon(ActionIcon::RESET, 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang('Resource locked by gradebook')
9140
                                );
9141
                            }
9142
                        }
9143
9144
                        $actions .= $clean;
9145
                        // Visible / invisible
9146
                        // Check if this exercise was added in a LP
9147
                        $visibility = '';
9148
                        if (api_is_platform_admin()) {
9149
                            if ($exercise->exercise_was_added_in_lp) {
9150
                                $visibility = Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('AddedToLPCannotBeAccessed')
9151
                                );
9152
                            } else {
9153
                                if (!$exerciseEntity->isVisible($course, $session)) {
9154
                                    $visibility = Display::url(
9155
                                        Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Activate')
9156
                                        ),
9157
                                        'exercise.php?' . api_get_cidreq() . '&action=enable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9158
                                    );
9159
                                } else {
9160
                                    // else if not active
9161
                                    $visibility = Display::url(
9162
                                        Display::getMdiIcon(StateIcon::PUBLIC_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Deactivate')
9163
                                        ),
9164
                                        'exercise.php?' . api_get_cidreq() . '&action=disable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9165
                                    );
9166
                                }
9167
                            }
9168
                        }
9169
9170
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9171
                            $visibility = '';
9172
                        }
9173
9174
                        $actions .= $visibility;
9175
9176
                        // Export qti ...
9177
                        $export = Display::url(
9178
                            Display::getMdiIcon(
9179
                                'database',
9180
                                'ch-tool-icon',
9181
                                null,
9182
                                ICON_SIZE_SMALL,
9183
                                'IMS/QTI'
9184
                            ),
9185
                            'exercise.php?action=exportqti2&exerciseId='.$exerciseId.'&'.api_get_cidreq()
9186
                        );
9187
9188
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9189
                            $export = '';
9190
                        }
9191
9192
                        $actions .= $export;
9193
                    } else {
9194
                        // not session
9195
                        $actions = Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang('Exercise edition not available in session')
9196
                        );
9197
9198
                        // Check if this exercise was added in a LP
9199
                        $visibility = '';
9200
                        if (api_is_platform_admin()) {
9201
                            if ($exercise->exercise_was_added_in_lp) {
9202
                                $visibility = Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Added to learnpath cannot be accessed')
9203
                                );
9204
                            } else {
9205
                                if (0 === $exerciseEntity->getActive() || 0 == $visibility) {
9206
                                    $visibility = Display::url(
9207
                                        Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Activate')
9208
                                        ),
9209
                                        'exercise.php?' . api_get_cidreq() . '&action=enable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9210
                                    );
9211
                                } else {
9212
                                    // else if not active
9213
                                    $visibility = Display::url(
9214
                                        Display::getMdiIcon(StateIcon::PUBLIC_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Deactivate')
9215
                                        ),
9216
                                        'exercise.php?' . api_get_cidreq() . '&action=disable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9217
                                    );
9218
                                }
9219
                            }
9220
                        }
9221
9222
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9223
                            $visibility = '';
9224
                        }
9225
9226
                        $actions .= $visibility;
9227
                        $actions .= '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9228
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).'</a>';
9229
                        $actions .= Display::url(
9230
                            Display::getMdiIcon('disc', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Copy this exercise as a new one')),
9231
                            '',
9232
                            [
9233
                                'onclick' => "javascript:if(!confirm('".addslashes(
9234
                                        api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES)
9235
                                    )." ".addslashes($title)."?"."')) return false;",
9236
                                'href' => 'exercise.php?'.api_get_cidreq(
9237
                                    ).'&action=copy_exercise&sec_token='.$token.'&exerciseId='.$exerciseId,
9238
                            ]
9239
                        );
9240
                    }
9241
9242
                    // Delete
9243
                    $delete = '';
9244
                    if ($repo->isGranted('DELETE', $exerciseEntity) && $allowToEditBaseCourse) {
9245
                        if (!$locked) {
9246
                            $deleteUrl = 'exercise.php?'.api_get_cidreq().'&action=delete&sec_token='.$token.'&exerciseId='.$exerciseId;
9247
                            $delete = Display::url(
9248
                                Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete')),
9249
                                '',
9250
                                [
9251
                                    'onclick' => "javascript:if(!confirm('".
9252
                                        addslashes(api_htmlentities(get_lang('Are you sure to delete?')))." ".
9253
                                        addslashes($exercise->getUnformattedTitle())."?"."')) return false;",
9254
                                    'href' => $deleteUrl,
9255
                                ]
9256
                            );
9257
                        } else {
9258
                            $delete = Display::getMdiIcon(
9259
                                ActionIcon::DELETE,
9260
                                'ch-tool-icon-disabled',
9261
                                null,
9262
                                ICON_SIZE_SMALL,
9263
                                get_lang(
9264
                                    '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.'
9265
                                )
9266
                            );
9267
                        }
9268
                    }
9269
9270
                    if ($limitTeacherAccess && !api_is_platform_admin()) {
9271
                        $delete = '';
9272
                    }
9273
9274
                    if (!empty($minCategoriesInExercise)) {
9275
                        $cats = TestCategory::getListOfCategoriesForTest($exercise);
9276
                        if (!(count($cats) >= $minCategoriesInExercise)) {
9277
                            continue;
9278
                        }
9279
                    }
9280
                    $actions .= $delete;
9281
9282
                    // Number of questions.
9283
                    $random = $exerciseEntity->getRandom();
9284
                    if ($random > 0 || -1 == $random) {
9285
                        // if random == -1 means use random questions with all questions
9286
                        $random_number_of_question = $random;
9287
                        if (-1 == $random_number_of_question) {
9288
                            $random_number_of_question = $rowi;
9289
                        }
9290
                        if ($exerciseEntity->getRandomByCategory() > 0) {
9291
                            $nbQuestionsTotal = TestCategory::getNumberOfQuestionRandomByCategory(
9292
                                $exerciseId,
9293
                                $random_number_of_question
9294
                            );
9295
                            $number_of_questions = $nbQuestionsTotal.' ';
9296
                            $number_of_questions .= ($nbQuestionsTotal > 1) ? get_lang('Questions lower case') : get_lang(
9297
                                'Question lower case'
9298
                            );
9299
                            $number_of_questions .= ' - ';
9300
                            $number_of_questions .= min(
9301
                                    TestCategory::getNumberMaxQuestionByCat($exerciseId),
9302
                                    $random_number_of_question
9303
                                ).' '.get_lang('Question by category');
9304
                        } else {
9305
                            $random_label = ' ('.get_lang('Random').') ';
9306
                            $number_of_questions = $random_number_of_question.' '.$random_label.' / '.$rowi;
9307
                            // Bug if we set a random value bigger than the real number of questions
9308
                            if ($random_number_of_question > $rowi) {
9309
                                $number_of_questions = $rowi.' '.$random_label;
9310
                            }
9311
                        }
9312
                    } else {
9313
                        $number_of_questions = $rowi;
9314
                    }
9315
9316
                    $currentRow['count_questions'] = $number_of_questions;
9317
                } else {
9318
                    // Student only.
9319
                    $visibility = $exerciseEntity->isVisible($course, null);
9320
                    if (false === $visibility && !empty($sessionId)) {
9321
                        $visibility = $exerciseEntity->isVisible($course, $session);
9322
                    }
9323
9324
                    if (false === $visibility) {
9325
                        continue;
9326
                    }
9327
9328
                    $url = '<a '.$alt_title.'
9329
                        href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$exerciseId.'">'.
9330
                        $cut_title.'</a>';
9331
9332
                    // Link of the exercise.
9333
                    $currentRow['title'] = $url.' '.$sessionStar;
9334
                    // This query might be improved later on by ordering by the new "tms" field rather than by exe_id
9335
                    if ($returnData) {
9336
                        $currentRow['title'] = $exercise->getUnformattedTitle();
9337
                    }
9338
9339
                    $sessionCondition = api_get_session_condition(api_get_session_id());
9340
                    // Don't remove this marker: note-query-exe-results
9341
                    $sql = "SELECT * FROM $TBL_TRACK_EXERCISES
9342
                            WHERE
9343
                                exe_exo_id = ".$exerciseId." AND
9344
                                exe_user_id = $userId AND
9345
                                c_id = ".api_get_course_int_id()." AND
9346
                                status <> 'incomplete' AND
9347
                                orig_lp_id = 0 AND
9348
                                orig_lp_item_id = 0
9349
                                $sessionCondition
9350
                            ORDER BY exe_id DESC";
9351
9352
                    $qryres = Database::query($sql);
9353
                    $num = Database:: num_rows($qryres);
9354
9355
                    // Hide the results.
9356
                    $my_result_disabled = $exerciseEntity->getResultsDisabled();
9357
                    $attempt_text = '-';
9358
                    // Time limits are on
9359
                    if ($time_limits) {
9360
                        // Exam is ready to be taken
9361
                        if ($is_actived_time) {
9362
                            // Show results
9363
                            if (
9364
                            in_array(
9365
                                $my_result_disabled,
9366
                                [
9367
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9368
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9369
                                    RESULT_DISABLE_SHOW_SCORE_ONLY,
9370
                                    RESULT_DISABLE_RANKING,
9371
                                ]
9372
                            )
9373
                            ) {
9374
                                // More than one attempt
9375
                                if ($num > 0) {
9376
                                    $row_track = Database:: fetch_array($qryres);
9377
                                    $attempt_text = get_lang('Latest attempt').' : ';
9378
                                    $attempt_text .= ExerciseLib::show_score(
9379
                                        $row_track['score'],
9380
                                        $row_track['max_score']
9381
                                    );
9382
                                } else {
9383
                                    //No attempts
9384
                                    $attempt_text = get_lang('Not attempted');
9385
                                }
9386
                            } else {
9387
                                $attempt_text = '-';
9388
                            }
9389
                        } else {
9390
                            // Quiz not ready due to time limits
9391
                            //@todo use the is_visible function
9392
                            if (!empty($startTime) && !empty($endTime)) {
9393
                                $today = time();
9394
                                if ($today < $start_time) {
9395
                                    $attempt_text = sprintf(
9396
                                        get_lang('Exercise will be activated from %s to %s'),
9397
                                        api_convert_and_format_date($start_time),
9398
                                        api_convert_and_format_date($end_time)
9399
                                    );
9400
                                } else {
9401
                                    if ($today > $end_time) {
9402
                                        $attempt_text = sprintf(
9403
                                            get_lang('Exercise was activated from %s to %s'),
9404
                                            api_convert_and_format_date($start_time),
9405
                                            api_convert_and_format_date($end_time)
9406
                                        );
9407
                                    }
9408
                                }
9409
                            } else {
9410
                                if (!empty($startTime)) {
9411
                                    $attempt_text = sprintf(
9412
                                        get_lang('Exercise available from %s'),
9413
                                        api_convert_and_format_date($start_time)
9414
                                    );
9415
                                }
9416
                                if (!empty($endTime)) {
9417
                                    $attempt_text = sprintf(
9418
                                        get_lang('Exercise available until %s'),
9419
                                        api_convert_and_format_date($end_time)
9420
                                    );
9421
                                }
9422
                            }
9423
                        }
9424
                    } else {
9425
                        // Normal behaviour.
9426
                        // Show results.
9427
                        if (
9428
                        in_array(
9429
                            $my_result_disabled,
9430
                            [
9431
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9432
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9433
                                RESULT_DISABLE_SHOW_SCORE_ONLY,
9434
                                RESULT_DISABLE_RANKING,
9435
                                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
9436
                            ]
9437
                        )
9438
                        ) {
9439
                            if ($num > 0) {
9440
                                $row_track = Database::fetch_array($qryres);
9441
                                $attempt_text = get_lang('Latest attempt').' : ';
9442
                                $attempt_text .= ExerciseLib::show_score(
9443
                                    $row_track['score'],
9444
                                    $row_track['max_score']
9445
                                );
9446
                            } else {
9447
                                $attempt_text = get_lang('Not attempted');
9448
                            }
9449
                        }
9450
                    }
9451
                    if ($returnData) {
9452
                        $attempt_text = $num;
9453
                    }
9454
                }
9455
9456
                $currentRow['attempt'] = $attempt_text;
9457
                $currentRow['iid'] = $exerciseId;
9458
9459
                if ($is_allowedToEdit) {
9460
                    $additionalActions = ExerciseLib::getAdditionalTeacherActions($exerciseId);
9461
9462
                    if (!empty($additionalActions)) {
9463
                        $actions .= $additionalActions.PHP_EOL;
9464
                    }
9465
9466
                    if (!empty($myActions) && is_callable($myActions)) {
9467
                        $actions = $myActions($currentRow);
9468
                    }
9469
                    $currentRow = [
9470
                        $exerciseId,
9471
                        $currentRow['title'],
9472
                        $currentRow['count_questions'],
9473
                        $actions,
9474
                    ];
9475
                } else {
9476
                    $currentRow = [
9477
                        $currentRow['title'],
9478
                        $currentRow['attempt'],
9479
                    ];
9480
9481
                    if ($isDrhOfCourse) {
9482
                        $currentRow[] = '<a
9483
                            href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9484
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).
9485
                            '</a>';
9486
                    }
9487
                    if ($returnData) {
9488
                        $currentRow['id'] = $exercise->id;
9489
                        $currentRow['url'] = $webPath.'exercise/overview.php?'
9490
                            .api_get_cidreq().'&'
9491
                            ."$mylpid$mylpitemid&exerciseId={$exercise->id}";
9492
                        $currentRow['name'] = $currentRow[0];
9493
                    }
9494
                }
9495
                $tableRows[] = $currentRow;
9496
            }
9497
        }
9498
9499
        if (empty($tableRows) && empty($categoryId)) {
9500
            if ($is_allowedToEdit && 'learnpath' !== $origin) {
9501
                if (!empty($_GET['keyword'])) {
9502
                    $content .= Display::return_message(
9503
                        get_lang('No Results for keyword: ').Security::remove_XSS($_GET['keyword']),
9504
                        'warning'
9505
                    );
9506
                } else {
9507
                    $content .= Display::noDataView(
9508
                        get_lang('Quiz'),
9509
                        Display::getMdiIcon(ToolIcon::QUIZ, 'ch-tool-icon', null, ICON_SIZE_BIG),
9510
                        get_lang('Create a new test'),
9511
                        'exercise_admin.php?'.api_get_cidreq()
9512
                    );
9513
                }
9514
            }
9515
        } else {
9516
            if (empty($tableRows)) {
9517
                return '';
9518
            }
9519
            $table->setTableData($tableRows);
9520
            $table->setTotalNumberOfItems($total);
9521
            $table->set_additional_parameters(
9522
                [
9523
                    'cid' => api_get_course_int_id(),
9524
                    'sid' => api_get_session_id(),
9525
                    'category_id' => $categoryId,
9526
                ]
9527
            );
9528
9529
            if ($is_allowedToEdit) {
9530
                $formActions = [];
9531
                $formActions['visible'] = get_lang('Activate');
9532
                $formActions['invisible'] = get_lang('Deactivate');
9533
                $formActions['delete'] = get_lang('Delete');
9534
                $table->set_form_actions($formActions);
9535
            }
9536
9537
            $i = 0;
9538
            if ($is_allowedToEdit) {
9539
                $table->set_header($i++, '', false, 'width="18px"');
9540
            }
9541
            $table->set_header($i++, get_lang('Test name'), false);
9542
9543
            if ($is_allowedToEdit) {
9544
                $table->set_header($i++, get_lang('Questions'), false, [], ['class' => 'text-center']);
9545
                $table->set_header($i++, get_lang('Actions'), false, [], ['class' => 'text-center']);
9546
            } else {
9547
                $table->set_header($i++, get_lang('Status'), false);
9548
                if ($isDrhOfCourse) {
9549
                    $table->set_header($i++, get_lang('Actions'), false, [], ['class' => 'text-center']);
9550
                }
9551
            }
9552
9553
            if ($returnTable) {
9554
                return $table;
9555
            }
9556
            $content .= $table->return_table();
9557
        }
9558
9559
        return $content;
9560
    }
9561
9562
    /**
9563
     * @return int value in minutes
9564
     */
9565
    public function getResultAccess()
9566
    {
9567
        $extraFieldValue = new ExtraFieldValue('exercise');
9568
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9569
            $this->iId,
9570
            'results_available_for_x_minutes'
9571
        );
9572
9573
        if (!empty($value) && isset($value['value'])) {
9574
            return (int) $value['value'];
9575
        }
9576
9577
        return 0;
9578
    }
9579
9580
    /**
9581
     * @param array $exerciseResultInfo
9582
     *
9583
     * @return bool
9584
     */
9585
    public function getResultAccessTimeDiff($exerciseResultInfo)
9586
    {
9587
        $value = $this->getResultAccess();
9588
        if (!empty($value)) {
9589
            $endDate = new DateTime($exerciseResultInfo['exe_date'], new DateTimeZone('UTC'));
9590
            $endDate->add(new DateInterval('PT'.$value.'M'));
9591
            $now = time();
9592
            if ($endDate->getTimestamp() > $now) {
9593
                return (int) $endDate->getTimestamp() - $now;
9594
            }
9595
        }
9596
9597
        return 0;
9598
    }
9599
9600
    /**
9601
     * @param array $exerciseResultInfo
9602
     *
9603
     * @return bool
9604
     */
9605
    public function hasResultsAccess($exerciseResultInfo)
9606
    {
9607
        $diff = $this->getResultAccessTimeDiff($exerciseResultInfo);
9608
        if (0 === $diff) {
9609
            return false;
9610
        }
9611
9612
        return true;
9613
    }
9614
9615
    /**
9616
     * @return int
9617
     */
9618
    public function getResultsAccess()
9619
    {
9620
        $extraFieldValue = new ExtraFieldValue('exercise');
9621
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9622
            $this->iId,
9623
            'results_available_for_x_minutes'
9624
        );
9625
        if (!empty($value)) {
9626
            return (int) $value;
9627
        }
9628
9629
        return 0;
9630
    }
9631
9632
    /**
9633
     * @param int   $questionId
9634
     * @param bool  $show_results
9635
     * @param array $question_result
9636
     */
9637
    public function getDelineationResult(Question $objQuestionTmp, $questionId, $show_results, $question_result)
9638
    {
9639
        $id = (int) $objQuestionTmp->id;
9640
        $questionId = (int) $questionId;
9641
9642
        $final_overlap = $question_result['extra']['final_overlap'];
9643
        $final_missing = $question_result['extra']['final_missing'];
9644
        $final_excess = $question_result['extra']['final_excess'];
9645
9646
        $overlap_color = $question_result['extra']['overlap_color'];
9647
        $missing_color = $question_result['extra']['missing_color'];
9648
        $excess_color = $question_result['extra']['excess_color'];
9649
9650
        $threadhold1 = $question_result['extra']['threadhold1'];
9651
        $threadhold2 = $question_result['extra']['threadhold2'];
9652
        $threadhold3 = $question_result['extra']['threadhold3'];
9653
9654
        if ($show_results) {
9655
            if ($overlap_color) {
9656
                $overlap_color = 'green';
9657
            } else {
9658
                $overlap_color = 'red';
9659
            }
9660
9661
            if ($missing_color) {
9662
                $missing_color = 'green';
9663
            } else {
9664
                $missing_color = 'red';
9665
            }
9666
            if ($excess_color) {
9667
                $excess_color = 'green';
9668
            } else {
9669
                $excess_color = 'red';
9670
            }
9671
9672
            if (!is_numeric($final_overlap)) {
9673
                $final_overlap = 0;
9674
            }
9675
9676
            if (!is_numeric($final_missing)) {
9677
                $final_missing = 0;
9678
            }
9679
            if (!is_numeric($final_excess)) {
9680
                $final_excess = 0;
9681
            }
9682
9683
            if ($final_excess > 100) {
9684
                $final_excess = 100;
9685
            }
9686
9687
            $table_resume = '
9688
                    <table class="table table-hover table-striped data_table">
9689
                        <tr class="row_odd" >
9690
                            <td>&nbsp;</td>
9691
                            <td><b>'.get_lang('Requirements').'</b></td>
9692
                            <td><b>'.get_lang('YourAnswer').'</b></td>
9693
                        </tr>
9694
                        <tr class="row_even">
9695
                            <td><b>'.get_lang('Overlap').'</b></td>
9696
                            <td>'.get_lang('Min').' '.$threadhold1.'</td>
9697
                            <td>
9698
                                <div style="color:'.$overlap_color.'">
9699
                                    '.(($final_overlap < 0) ? 0 : intval($final_overlap)).'
9700
                                </div>
9701
                            </td>
9702
                        </tr>
9703
                        <tr>
9704
                            <td><b>'.get_lang('Excess').'</b></td>
9705
                            <td>'.get_lang('Max').' '.$threadhold2.'</td>
9706
                            <td>
9707
                                <div style="color:'.$excess_color.'">
9708
                                    '.(($final_excess < 0) ? 0 : intval($final_excess)).'
9709
                                </div>
9710
                            </td>
9711
                        </tr>
9712
                        <tr class="row_even">
9713
                            <td><b>'.get_lang('Missing').'</b></td>
9714
                            <td>'.get_lang('Max').' '.$threadhold3.'</td>
9715
                            <td>
9716
                                <div style="color:'.$missing_color.'">
9717
                                    '.(($final_missing < 0) ? 0 : intval($final_missing)).'
9718
                                </div>
9719
                            </td>
9720
                        </tr>
9721
                    </table>
9722
                ';
9723
9724
            $answerType = $objQuestionTmp->selectType();
9725
            /*if ($next == 0) {
9726
                $try = $try_hotspot;
9727
                $lp = $lp_hotspot;
9728
                $destinationid = $select_question_hotspot;
9729
                $url = $url_hotspot;
9730
            } else {
9731
                //show if no error
9732
                $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
9733
                $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
9734
            }
9735
            echo '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>';
9736
            if ($organs_at_risk_hit > 0) {
9737
                $message = '<br />'.get_lang('ResultIs').' <b>'.$result_comment.'</b><br />';
9738
                $message .= '<p style="color:#DC0A0A;"><b>'.get_lang('OARHit').'</b></p>';
9739
            } else {
9740
                $message = '<p>'.get_lang('YourDelineation').'</p>';
9741
                $message .= $table_resume;
9742
                $message .= '<br />'.get_lang('ResultIs').' <b>'.$result_comment.'</b><br />';
9743
            }
9744
            $message .= '<p>'.$comment.'</p>';
9745
            echo $message;*/
9746
9747
            // Showing the score
9748
            /*$queryfree = "SELECT marks FROM $TBL_TRACK_ATTEMPT
9749
                          WHERE exe_id = $id AND question_id =  $questionId";
9750
            $resfree = Database::query($queryfree);
9751
            $questionScore = Database::result($resfree, 0, 'marks');
9752
            $totalScore += $questionScore;*/
9753
            $relPath = api_get_path(REL_CODE_PATH);
9754
            echo '</table></td></tr>';
9755
            echo "
9756
                        <tr>
9757
                            <td colspan=\"2\">
9758
                                <div id=\"hotspot-solution\"></div>
9759
                                <script>
9760
                                    $(function() {
9761
                                        new HotspotQuestion({
9762
                                            questionId: $questionId,
9763
                                            exerciseId: {$this->id},
9764
                                            exeId: $id,
9765
                                            selector: '#hotspot-solution',
9766
                                            for: 'solution',
9767
                                            relPath: '$relPath'
9768
                                        });
9769
                                    });
9770
                                </script>
9771
                            </td>
9772
                        </tr>
9773
                    </table>
9774
                ";
9775
        }
9776
    }
9777
9778
    /**
9779
     * Clean exercise session variables.
9780
     */
9781
    public static function cleanSessionVariables()
9782
    {
9783
        Session::erase('objExercise');
9784
        Session::erase('exe_id');
9785
        Session::erase('calculatedAnswerId');
9786
        Session::erase('duration_time_previous');
9787
        Session::erase('duration_time');
9788
        Session::erase('objQuestion');
9789
        Session::erase('objAnswer');
9790
        Session::erase('questionList');
9791
        Session::erase('categoryList');
9792
        Session::erase('exerciseResult');
9793
        Session::erase('firstTime');
9794
9795
        Session::erase('time_per_question');
9796
        Session::erase('question_start');
9797
        Session::erase('exerciseResultCoordinates');
9798
        Session::erase('hotspot_coord');
9799
        Session::erase('hotspot_dest');
9800
        Session::erase('hotspot_delineation_result');
9801
    }
9802
9803
    /**
9804
     * Get the first LP found matching the session ID.
9805
     *
9806
     * @param int $sessionId
9807
     *
9808
     * @return array
9809
     */
9810
    public function getLpBySession($sessionId)
9811
    {
9812
        if (!empty($this->lpList)) {
9813
            $sessionId = (int) $sessionId;
9814
9815
            foreach ($this->lpList as $lp) {
9816
                if (isset($lp['session_id']) && (int) $lp['session_id'] == $sessionId) {
9817
                    return $lp;
9818
                }
9819
            }
9820
9821
            return current($this->lpList);
9822
        }
9823
9824
        return [
9825
            'lp_id' => 0,
9826
            'max_score' => 0,
9827
            'session_id' => 0,
9828
        ];
9829
    }
9830
9831
    public static function saveExerciseInLp($safe_item_id, $safe_exe_id, $course_id = null)
9832
    {
9833
        $lp = Session::read('oLP');
9834
9835
        $safe_exe_id = (int) $safe_exe_id;
9836
        $safe_item_id = (int) $safe_item_id;
9837
9838
        if (empty($lp) || empty($safe_exe_id) || empty($safe_item_id)) {
9839
            return false;
9840
        }
9841
9842
        $viewId = $lp->get_view_id();
9843
        if (!isset($course_id)) {
9844
            $course_id = api_get_course_int_id();
9845
        }
9846
        $userId = (int) api_get_user_id();
9847
        $viewId = (int) $viewId;
9848
9849
        $TBL_TRACK_EXERCICES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
9850
        $TBL_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
9851
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
9852
9853
        $sql = "SELECT start_date, exe_date, score, max_score, exe_exo_id, exe_duration
9854
                FROM $TBL_TRACK_EXERCICES
9855
                WHERE exe_id = $safe_exe_id AND exe_user_id = $userId";
9856
        $res = Database::query($sql);
9857
        $row_dates = Database::fetch_array($res);
9858
9859
        if (empty($row_dates)) {
9860
            return false;
9861
        }
9862
9863
        $duration = (int) $row_dates['exe_duration'];
9864
        $score = (float) $row_dates['score'];
9865
        $max_score = (float) $row_dates['max_score'];
9866
9867
        $sql = "UPDATE $TBL_LP_ITEM SET
9868
                    max_score = '$max_score'
9869
                WHERE iid = $safe_item_id";
9870
        Database::query($sql);
9871
9872
        $sql = "SELECT iid FROM $TBL_LP_ITEM_VIEW
9873
                WHERE
9874
                    lp_item_id = $safe_item_id AND
9875
                    lp_view_id = $viewId
9876
                ORDER BY iid DESC
9877
                LIMIT 1";
9878
        $res_last_attempt = Database::query($sql);
9879
9880
        if (Database::num_rows($res_last_attempt) && !api_is_invitee()) {
9881
            $row_last_attempt = Database::fetch_row($res_last_attempt);
9882
            $lp_item_view_id = $row_last_attempt[0];
9883
9884
            $exercise = new Exercise($course_id);
9885
            $exercise->read($row_dates['exe_exo_id']);
9886
            $status = 'completed';
9887
9888
            if (!empty($exercise->pass_percentage)) {
9889
                $status = 'failed';
9890
                $success = ExerciseLib::isSuccessExerciseResult(
9891
                    $score,
9892
                    $max_score,
9893
                    $exercise->pass_percentage
9894
                );
9895
                if ($success) {
9896
                    $status = 'passed';
9897
                }
9898
            }
9899
9900
            $sql = "UPDATE $TBL_LP_ITEM_VIEW SET
9901
                        status = '$status',
9902
                        score = '$score',
9903
                        total_time = '$duration'
9904
                    WHERE iid = $lp_item_view_id";
9905
            Database::query($sql);
9906
9907
            $sql = "UPDATE $TBL_TRACK_EXERCICES SET
9908
                        orig_lp_item_view_id = '$lp_item_view_id'
9909
                    WHERE exe_id = ".$safe_exe_id;
9910
            Database::query($sql);
9911
        }
9912
    }
9913
9914
    /**
9915
     * Get the user answers saved in exercise.
9916
     *
9917
     * @param int $attemptId
9918
     *
9919
     * @return array
9920
     */
9921
    public function getUserAnswersSavedInExercise($attemptId)
9922
    {
9923
        $exerciseResult = [];
9924
9925
        $attemptList = Event::getAllExerciseEventByExeId($attemptId);
9926
9927
        foreach ($attemptList as $questionId => $options) {
9928
            foreach ($options as $option) {
9929
                $question = Question::read($option['question_id']);
9930
9931
                if ($question) {
9932
                    switch ($question->type) {
9933
                        case FILL_IN_BLANKS:
9934
                            $option['answer'] = $this->fill_in_blank_answer_to_string($option['answer']);
9935
                            break;
9936
                    }
9937
                }
9938
9939
                if (!empty($option['answer'])) {
9940
                    $exerciseResult[] = $questionId;
9941
9942
                    break;
9943
                }
9944
            }
9945
        }
9946
9947
        return $exerciseResult;
9948
    }
9949
9950
    /**
9951
     * Get the number of user answers saved in exercise.
9952
     *
9953
     * @param int $attemptId
9954
     *
9955
     * @return int
9956
     */
9957
    public function countUserAnswersSavedInExercise($attemptId)
9958
    {
9959
        $answers = $this->getUserAnswersSavedInExercise($attemptId);
9960
9961
        return count($answers);
9962
    }
9963
9964
    public static function allowAction($action)
9965
    {
9966
        if (api_is_platform_admin()) {
9967
            return true;
9968
        }
9969
9970
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
9971
        $disableClean = ('true' === api_get_setting('exercise.disable_clean_exercise_results_for_teachers'));
9972
9973
        switch ($action) {
9974
            case 'delete':
9975
                if (api_is_allowed_to_edit(null, true)) {
9976
                    if ($limitTeacherAccess) {
9977
                        return false;
9978
                    }
9979
9980
                    return true;
9981
                }
9982
                break;
9983
            case 'clean_results':
9984
                if (api_is_allowed_to_edit(null, true)) {
9985
                    if ($limitTeacherAccess) {
9986
                        return false;
9987
                    }
9988
9989
                    if ($disableClean) {
9990
                        return false;
9991
                    }
9992
9993
                    return true;
9994
                }
9995
9996
                break;
9997
        }
9998
9999
        return false;
10000
    }
10001
10002
    public static function getLpListFromExercise($exerciseId, $courseId)
10003
    {
10004
        $tableLpItem = Database::get_course_table(TABLE_LP_ITEM);
10005
        $tblLp = Database::get_course_table(TABLE_LP_MAIN);
10006
10007
        $exerciseId = (int) $exerciseId;
10008
        $courseId = (int) $courseId;
10009
10010
        $sql = "SELECT
10011
                    lp.title,
10012
                    lpi.lp_id,
10013
                    lpi.max_score
10014
                FROM $tableLpItem lpi
10015
                INNER JOIN $tblLp lp
10016
                ON (lpi.lp_id = lp.iid)
10017
                WHERE
10018
                    lpi.item_type = '".TOOL_QUIZ."' AND
10019
                    lpi.path = '$exerciseId'";
10020
        $result = Database::query($sql);
10021
        $lpList = [];
10022
        if (Database::num_rows($result) > 0) {
10023
            $lpList = Database::store_result($result, 'ASSOC');
10024
        }
10025
10026
        return $lpList;
10027
    }
10028
10029
    public function getReminderTable($questionList, $exercise_stat_info, $disableCheckBoxes = false)
10030
    {
10031
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10032
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10033
        $learnpath_item_view_id = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10034
        $categoryId = isset($_REQUEST['category_id']) ? (int) $_REQUEST['category_id'] : 0;
10035
10036
        if (empty($exercise_stat_info)) {
10037
            return '';
10038
        }
10039
10040
        $remindList = $exercise_stat_info['questions_to_check'];
10041
        $remindList = explode(',', $remindList);
10042
10043
        $exeId = $exercise_stat_info['exe_id'];
10044
        $exerciseId = $exercise_stat_info['exe_exo_id'];
10045
        $exercise_result = $this->getUserAnswersSavedInExercise($exeId);
10046
10047
        $content = Display::label(get_lang('QuestionWithNoAnswer'), 'danger');
10048
        $content .= '<div class="clear"></div><br />';
10049
        $table = '';
10050
        $counter = 0;
10051
        // Loop over all question to show results for each of them, one by one
10052
        foreach ($questionList as $questionId) {
10053
            $objQuestionTmp = Question::read($questionId);
10054
            $check_id = 'remind_list['.$questionId.']';
10055
            $attributes = [
10056
                'id' => $check_id,
10057
                'onclick' => "save_remind_item(this, '$questionId');",
10058
                'data-question-id' => $questionId,
10059
            ];
10060
            if (in_array($questionId, $remindList)) {
10061
                $attributes['checked'] = 1;
10062
            }
10063
10064
            $checkbox = Display::input('checkbox', 'remind_list['.$questionId.']', '', $attributes);
10065
            $checkbox = '<div class="pretty p-svg p-curve">
10066
                        '.$checkbox.'
10067
                        <div class="state p-primary ">
10068
                         <svg class="svg svg-icon" viewBox="0 0 20 20">
10069
                            <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>
10070
                         </svg>
10071
                         <label>&nbsp;</label>
10072
                        </div>
10073
                    </div>';
10074
            $counter++;
10075
            $questionTitle = $counter.'. '.strip_tags($objQuestionTmp->selectTitle());
10076
            // Check if the question doesn't have an answer.
10077
            if (!in_array($questionId, $exercise_result)) {
10078
                $questionTitle = Display::label($questionTitle, 'danger');
10079
            }
10080
10081
            $label_attributes = [];
10082
            $label_attributes['for'] = $check_id;
10083
            if (false === $disableCheckBoxes) {
10084
                $questionTitle = Display::tag('label', $checkbox.$questionTitle, $label_attributes);
10085
            }
10086
            $table .= Display::div($questionTitle, ['class' => 'exercise_reminder_item ']);
10087
        }
10088
10089
        $content .= Display::div('', ['id' => 'message']).
10090
            Display::div($table, ['class' => 'question-check-test']);
10091
10092
        $content .= '<script>
10093
        var lp_data = $.param({
10094
            "learnpath_id": '.$learnpath_id.',
10095
            "learnpath_item_id" : '.$learnpath_item_id.',
10096
            "learnpath_item_view_id": '.$learnpath_item_view_id.'
10097
        });
10098
10099
        function final_submit() {
10100
            // Normal inputs.
10101
            window.location = "'.api_get_path(WEB_CODE_PATH).'exercise/exercise_result.php?'.api_get_cidreq().'&exe_id='.$exeId.'&" + lp_data;
10102
        }
10103
10104
        function selectAll() {
10105
            $("input[type=checkbox]").each(function () {
10106
                $(this).prop("checked", 1);
10107
                var question_id = $(this).data("question-id");
10108
                var action = "add";
10109
                $.ajax({
10110
                    url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10111
                    data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
10112
                    success: function(returnValue) {
10113
                    }
10114
                });
10115
            });
10116
        }
10117
10118
        function changeOptionStatus(status)
10119
        {
10120
            $("input[type=checkbox]").each(function () {
10121
                $(this).prop("checked", status);
10122
            });
10123
10124
            var action = "";
10125
            var option = "remove_all";
10126
            if (status == 1) {
10127
                option = "add_all";
10128
            }
10129
            $.ajax({
10130
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10131
                data: "option="+option+"&exe_id='.$exeId.'&action="+action,
10132
                success: function(returnValue) {
10133
                }
10134
            });
10135
        }
10136
10137
        function reviewQuestions() {
10138
            var isChecked = 1;
10139
            $("input[type=checkbox]").each(function () {
10140
                if ($(this).prop("checked")) {
10141
                    isChecked = 2;
10142
                    return false;
10143
                }
10144
            });
10145
10146
            if (isChecked == 1) {
10147
                $("#message").addClass("warning-message");
10148
                $("#message").html("'.addslashes(get_lang('SelectAQuestionToReview')).'");
10149
            } else {
10150
                window.location = "exercise_submit.php?'.api_get_cidreq().'&category_id='.$categoryId.'&exerciseId='.$exerciseId.'&reminder=2&" + lp_data;
10151
            }
10152
        }
10153
10154
        function save_remind_item(obj, question_id) {
10155
            var action = "";
10156
            if ($(obj).prop("checked")) {
10157
                action = "add";
10158
            } else {
10159
                action = "delete";
10160
            }
10161
            $.ajax({
10162
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10163
                data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
10164
                success: function(returnValue) {
10165
                }
10166
            });
10167
        }
10168
        </script>';
10169
10170
        return $content;
10171
    }
10172
10173
    public function getRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10174
    {
10175
        $dataSet = [];
10176
        $labels = [];
10177
        $labelsWithId = [];
10178
        /** @var Exercise $exercise */
10179
        foreach ($exercises as $exercise) {
10180
            if (empty($labels)) {
10181
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10182
                if (!empty($categoryNameList)) {
10183
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10184
                    asort($labelsWithId);
10185
                    $labels = array_values($labelsWithId);
10186
                }
10187
            }
10188
10189
            foreach ($userList as $userId) {
10190
                $results = Event::getExerciseResultsByUser(
10191
                    $userId,
10192
                    $exercise->iId,
10193
                    $courseId,
10194
                    $sessionId
10195
                );
10196
10197
                if ($results) {
10198
                    $firstAttempt = end($results);
10199
                    $exeId = $firstAttempt['exe_id'];
10200
10201
                    ob_start();
10202
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10203
                        $exercise,
10204
                        $exeId,
10205
                        false
10206
                    );
10207
                    ob_end_clean();
10208
10209
                    $categoryList = $stats['category_list'];
10210
                    $tempResult = [];
10211
                    foreach ($labelsWithId as $category_id => $title) {
10212
                        if (isset($categoryList[$category_id])) {
10213
                            $category_item = $categoryList[$category_id];
10214
                            $tempResult[] = round($category_item['score'] / $category_item['total'] * 10);
10215
                        } else {
10216
                            $tempResult[] = 0;
10217
                        }
10218
                    }
10219
                    $dataSet[] = $tempResult;
10220
                }
10221
            }
10222
        }
10223
10224
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10225
    }
10226
10227
    public function getAverageRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10228
    {
10229
        $dataSet = [];
10230
        $labels = [];
10231
        $labelsWithId = [];
10232
10233
        $tempResult = [];
10234
        /** @var Exercise $exercise */
10235
        foreach ($exercises as $exercise) {
10236
            $exerciseId = $exercise->iId;
10237
            if (empty($labels)) {
10238
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10239
                if (!empty($categoryNameList)) {
10240
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10241
                    asort($labelsWithId);
10242
                    $labels = array_values($labelsWithId);
10243
                }
10244
            }
10245
10246
            foreach ($userList as $userId) {
10247
                $results = Event::getExerciseResultsByUser(
10248
                    $userId,
10249
                    $exerciseId,
10250
                    $courseId,
10251
                    $sessionId
10252
                );
10253
10254
                if ($results) {
10255
                    $firstAttempt = end($results);
10256
                    $exeId = $firstAttempt['exe_id'];
10257
10258
                    ob_start();
10259
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10260
                        $exercise,
10261
                        $exeId,
10262
                        false
10263
                    );
10264
                    ob_end_clean();
10265
10266
                    $categoryList = $stats['category_list'];
10267
                    foreach ($labelsWithId as $category_id => $title) {
10268
                        if (isset($categoryList[$category_id])) {
10269
                            $category_item = $categoryList[$category_id];
10270
                            if (!isset($tempResult[$exerciseId][$category_id])) {
10271
                                $tempResult[$exerciseId][$category_id] = 0;
10272
                            }
10273
                            $tempResult[$exerciseId][$category_id] += $category_item['score'] / $category_item['total'] * 10;
10274
                        }
10275
                    }
10276
                }
10277
            }
10278
        }
10279
10280
        $totalUsers = count($userList);
10281
10282
        foreach ($exercises as $exercise) {
10283
            $exerciseId = $exercise->iId;
10284
            $data = [];
10285
            foreach ($labelsWithId as $category_id => $title) {
10286
                if (isset($tempResult[$exerciseId]) && isset($tempResult[$exerciseId][$category_id])) {
10287
                    $data[] = round($tempResult[$exerciseId][$category_id] / $totalUsers);
10288
                } else {
10289
                    $data[] = 0;
10290
                }
10291
            }
10292
            $dataSet[] = $data;
10293
        }
10294
10295
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10296
    }
10297
10298
    public function getRadar($labels, $dataSet, $dataSetLabels = [])
10299
    {
10300
        if (empty($labels) || empty($dataSet)) {
10301
            return '';
10302
        }
10303
10304
        $displayLegend = 0;
10305
        if (!empty($dataSetLabels)) {
10306
            $displayLegend = 1;
10307
        }
10308
10309
        $labels = json_encode($labels);
10310
10311
        $colorList = ChamiloHelper::getColorPalette(true, true);
10312
10313
        $dataSetToJson = [];
10314
        $counter = 0;
10315
        foreach ($dataSet as $index => $resultsArray) {
10316
            $color = isset($colorList[$counter]) ? $colorList[$counter] : 'rgb('.rand(0, 255).', '.rand(0, 255).', '.rand(0, 255).', 1.0)';
10317
10318
            $label = isset($dataSetLabels[$index]) ? $dataSetLabels[$index] : '';
10319
            $background = str_replace('1.0', '0.2', $color);
10320
            $dataSetToJson[] = [
10321
                'fill' => false,
10322
                'label' => $label,
10323
                'backgroundColor' => $background,
10324
                'borderColor' => $color,
10325
                'pointBackgroundColor' => $color,
10326
                'pointBorderColor' => '#fff',
10327
                'pointHoverBackgroundColor' => '#fff',
10328
                'pointHoverBorderColor' => $color,
10329
                'pointRadius' => 6,
10330
                'pointBorderWidth' => 3,
10331
                'pointHoverRadius' => 10,
10332
                'data' => $resultsArray,
10333
            ];
10334
            $counter++;
10335
        }
10336
        $resultsToJson = json_encode($dataSetToJson);
10337
10338
        return "
10339
                <canvas id='categoryRadar' height='200'></canvas>
10340
                <script>
10341
                    var data = {
10342
                        labels: $labels,
10343
                        datasets: $resultsToJson
10344
                    }
10345
                    var options = {
10346
                        responsive: true,
10347
                        scale: {
10348
                            angleLines: {
10349
                                display: false
10350
                            },
10351
                            ticks: {
10352
                                beginAtZero: true,
10353
                                  min: 0,
10354
                                  max: 10,
10355
                                stepSize: 1,
10356
                            },
10357
                            pointLabels: {
10358
                              fontSize: 14,
10359
                              //fontStyle: 'bold'
10360
                            },
10361
                        },
10362
                        elements: {
10363
                            line: {
10364
                                tension: 0,
10365
                                borderWidth: 3
10366
                            }
10367
                        },
10368
                        legend: {
10369
                            //position: 'bottom'
10370
                            display: $displayLegend
10371
                        },
10372
                        animation: {
10373
                            animateScale: true,
10374
                            animateRotate: true
10375
                        },
10376
                    };
10377
                    var ctx = document.getElementById('categoryRadar').getContext('2d');
10378
                    var myRadarChart = new Chart(ctx, {
10379
                        type: 'radar',
10380
                        data: data,
10381
                        options: options
10382
                    });
10383
                </script>
10384
                ";
10385
    }
10386
10387
    /**
10388
     * Returns true if the exercise is locked by percentage. an exercise attempt must be passed.
10389
     */
10390
    public function isBlockedByPercentage(array $attempt = []): bool
10391
    {
10392
        if (empty($attempt)) {
10393
            return false;
10394
        }
10395
        $extraFieldValue = new ExtraFieldValue('exercise');
10396
        $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
10397
            $this->iId,
10398
            'blocking_percentage'
10399
        );
10400
10401
        if (empty($blockExercise['value'])) {
10402
            return false;
10403
        }
10404
10405
        $blockPercentage = (int) $blockExercise['value'];
10406
10407
        if (0 === $blockPercentage) {
10408
            return false;
10409
        }
10410
10411
        $resultPercentage = 0;
10412
10413
        if (isset($attempt['score']) && isset($attempt['max_score'])) {
10414
            $weight = (int) $attempt['max_score'];
10415
            $weight = (0 == $weight) ? 1 : $weight;
10416
            $resultPercentage = float_format(
10417
                ($attempt['score'] / $weight) * 100,
10418
                1
10419
            );
10420
        }
10421
        if ($resultPercentage <= $blockPercentage) {
10422
            return true;
10423
        }
10424
10425
        return false;
10426
    }
10427
10428
    /**
10429
     * Gets the question list ordered by the question_order setting (drag and drop).
10430
     *
10431
     * @param bool $adminView Optional.
10432
     *
10433
     * @return array
10434
     */
10435
    public function getQuestionOrderedList($adminView = false)
10436
    {
10437
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
10438
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
10439
10440
        // Getting question_order to verify that the question
10441
        // list is correct and all question_order's were set
10442
        $sql = "SELECT DISTINCT count(e.question_order) as count
10443
                FROM $TBL_EXERCICE_QUESTION e
10444
                INNER JOIN $TBL_QUESTIONS q
10445
                ON (e.question_id = q.iid)
10446
                WHERE
10447
                  e.quiz_id	= ".$this->getId();
10448
10449
        $result = Database::query($sql);
10450
        $row = Database::fetch_array($result);
10451
        $count_question_orders = $row['count'];
10452
10453
        // Getting question list from the order (question list drag n drop interface).
10454
        $sql = "SELECT DISTINCT e.question_id, e.question_order
10455
                FROM $TBL_EXERCICE_QUESTION e
10456
                INNER JOIN $TBL_QUESTIONS q
10457
                ON (e.question_id = q.iid)
10458
                WHERE
10459
10460
                    e.quiz_id = '".$this->getId()."'
10461
                ORDER BY question_order";
10462
        $result = Database::query($sql);
10463
10464
        // Fills the array with the question ID for this exercise
10465
        // the key of the array is the question position
10466
        $temp_question_list = [];
10467
        $counter = 1;
10468
        $questionList = [];
10469
        while ($new_object = Database::fetch_object($result)) {
10470
            if (!$adminView) {
10471
                // Correct order.
10472
                $questionList[$new_object->question_order] = $new_object->question_id;
10473
            } else {
10474
                $questionList[$counter] = $new_object->question_id;
10475
            }
10476
10477
            // Just in case we save the order in other array
10478
            $temp_question_list[$counter] = $new_object->question_id;
10479
            $counter++;
10480
        }
10481
10482
        if (!empty($temp_question_list)) {
10483
            /* If both array don't match it means that question_order was not correctly set
10484
               for all questions using the default mysql order */
10485
            if (count($temp_question_list) != $count_question_orders) {
10486
                $questionList = $temp_question_list;
10487
            }
10488
        }
10489
10490
        return $questionList;
10491
    }
10492
10493
    /**
10494
     * Get number of questions in exercise by user attempt.
10495
     *
10496
     * @return int
10497
     */
10498
    private function countQuestionsInExercise()
10499
    {
10500
        $lpId = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10501
        $lpItemId = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10502
        $lpItemViewId = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10503
10504
        $trackInfo = $this->get_stat_track_exercise_info($lpId, $lpItemId, $lpItemViewId);
10505
10506
        if (!empty($trackInfo)) {
10507
            $questionIds = explode(',', $trackInfo['data_tracking']);
10508
10509
            return count($questionIds);
10510
        }
10511
10512
        return $this->getQuestionCount();
10513
    }
10514
10515
    /**
10516
     * Select N values from the questions per category array.
10517
     *
10518
     * @param array $categoriesAddedInExercise
10519
     * @param array $question_list
10520
     * @param array $questions_by_category
10521
     * @param bool  $flatResult
10522
     * @param bool  $randomizeQuestions
10523
     * @param array $questionsByCategoryMandatory
10524
     *
10525
     * @return array
10526
     */
10527
    private function pickQuestionsPerCategory(
10528
        $categoriesAddedInExercise,
10529
        $question_list,
10530
        &$questions_by_category,
10531
        $flatResult = true,
10532
        $randomizeQuestions = false,
10533
        $questionsByCategoryMandatory = []
10534
    ) {
10535
        $addAll = true;
10536
        $categoryCountArray = [];
10537
10538
        // Getting how many questions will be selected per category.
10539
        if (!empty($categoriesAddedInExercise)) {
10540
            $addAll = false;
10541
            // Parsing question according the category rel exercise settings
10542
            foreach ($categoriesAddedInExercise as $category_info) {
10543
                $category_id = $category_info['category_id'];
10544
                if (isset($questions_by_category[$category_id])) {
10545
                    // How many question will be picked from this category.
10546
                    $count = $category_info['count_questions'];
10547
                    // -1 means all questions
10548
                    $categoryCountArray[$category_id] = $count;
10549
                    if (-1 == $count) {
10550
                        $categoryCountArray[$category_id] = 999;
10551
                    }
10552
                }
10553
            }
10554
        }
10555
10556
        if (!empty($questions_by_category)) {
10557
            $temp_question_list = [];
10558
            foreach ($questions_by_category as $category_id => &$categoryQuestionList) {
10559
                if (isset($categoryCountArray) && !empty($categoryCountArray)) {
10560
                    $numberOfQuestions = 0;
10561
                    if (isset($categoryCountArray[$category_id])) {
10562
                        $numberOfQuestions = $categoryCountArray[$category_id];
10563
                    }
10564
                }
10565
10566
                if ($addAll) {
10567
                    $numberOfQuestions = 999;
10568
                }
10569
                if (!empty($numberOfQuestions)) {
10570
                    $mandatoryQuestions = [];
10571
                    if (isset($questionsByCategoryMandatory[$category_id])) {
10572
                        $mandatoryQuestions = $questionsByCategoryMandatory[$category_id];
10573
                    }
10574
10575
                    $elements = TestCategory::getNElementsFromArray(
10576
                        $categoryQuestionList,
10577
                        $numberOfQuestions,
10578
                        $randomizeQuestions,
10579
                        $mandatoryQuestions
10580
                    );
10581
10582
                    if (!empty($elements)) {
10583
                        $temp_question_list[$category_id] = $elements;
10584
                        $categoryQuestionList = $elements;
10585
                    }
10586
                }
10587
            }
10588
10589
            if (!empty($temp_question_list)) {
10590
                if ($flatResult) {
10591
                    $temp_question_list = array_flatten($temp_question_list);
10592
                }
10593
                $question_list = $temp_question_list;
10594
            }
10595
        }
10596
10597
        return $question_list;
10598
    }
10599
10600
    /**
10601
     * Sends a notification when a user ends an examn.
10602
     *
10603
     * @param array  $question_list_answers
10604
     * @param string $origin
10605
     * @param array  $user_info
10606
     * @param string $url_email
10607
     * @param array  $teachers
10608
     */
10609
    private function sendNotificationForOpenQuestions(
10610
        $question_list_answers,
10611
        $origin,
10612
        $user_info,
10613
        $url_email,
10614
        $teachers
10615
    ) {
10616
        // Email configuration settings
10617
        $courseCode = api_get_course_id();
10618
        $courseInfo = api_get_course_info($courseCode);
10619
        $sessionId = api_get_session_id();
10620
        $sessionData = '';
10621
        if (!empty($sessionId)) {
10622
            $sessionInfo = api_get_session_info($sessionId);
10623
            if (!empty($sessionInfo)) {
10624
                $sessionData = '<tr>'
10625
                    .'<td><em>'.get_lang('Session name').'</em></td>'
10626
                    .'<td>&nbsp;<b>'.$sessionInfo['name'].'</b></td>'
10627
                    .'</tr>';
10628
            }
10629
        }
10630
10631
        $msg = get_lang('A learner has answered an open question').'<br /><br />'
10632
            .get_lang('Attempt details').' : <br /><br />'
10633
            .'<table>'
10634
            .'<tr>'
10635
            .'<td><em>'.get_lang('Course name').'</em></td>'
10636
            .'<td>&nbsp;<b>#course#</b></td>'
10637
            .'</tr>'
10638
            .$sessionData
10639
            .'<tr>'
10640
            .'<td>'.get_lang('Test attempted').'</td>'
10641
            .'<td>&nbsp;#exercise#</td>'
10642
            .'</tr>'
10643
            .'<tr>'
10644
            .'<td>'.get_lang('Learner name').'</td>'
10645
            .'<td>&nbsp;#firstName# #lastName#</td>'
10646
            .'</tr>'
10647
            .'<tr>'
10648
            .'<td>'.get_lang('Learner e-mail').'</td>'
10649
            .'<td>&nbsp;#mail#</td>'
10650
            .'</tr>'
10651
            .'</table>';
10652
10653
        $open_question_list = null;
10654
        foreach ($question_list_answers as $item) {
10655
            $question = $item['question'];
10656
            $answer = $item['answer'];
10657
            $answer_type = $item['answer_type'];
10658
10659
            if (!empty($question) && !empty($answer) && FREE_ANSWER == $answer_type) {
10660
                $open_question_list .=
10661
                    '<tr>
10662
                    <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
10663
                    <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
10664
                    </tr>
10665
                    <tr>
10666
                    <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
10667
                    <td valign="top" bgcolor="#F3F3F3">'.$answer.'</td>
10668
                    </tr>';
10669
            }
10670
        }
10671
10672
        if (!empty($open_question_list)) {
10673
            $msg .= '<p><br />'.get_lang('A learner has answered an open questionAre').' :</p>'.
10674
                '<table width="730" height="136" border="0" cellpadding="3" cellspacing="3">';
10675
            $msg .= $open_question_list;
10676
            $msg .= '</table><br />';
10677
10678
            $msg = str_replace('#exercise#', $this->exercise, $msg);
10679
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg);
10680
            $msg = str_replace('#lastName#', $user_info['lastname'], $msg);
10681
            $msg = str_replace('#mail#', $user_info['email'], $msg);
10682
            $msg = str_replace(
10683
                '#course#',
10684
                Display::url($courseInfo['title'], $courseInfo['course_public_url'].'?sid='.$sessionId),
10685
                $msg
10686
            );
10687
10688
            if ('learnpath' !== $origin) {
10689
                $msg .= '<br /><a href="#url#">'.get_lang(
10690
                        'Click this link to check the answer and/or give feedback'
10691
                    ).'</a>';
10692
            }
10693
            $msg = str_replace('#url#', $url_email, $msg);
10694
            $subject = get_lang('A learner has answered an open question');
10695
10696
            if (!empty($teachers)) {
10697
                foreach ($teachers as $user_id => $teacher_data) {
10698
                    MessageManager::send_message_simple(
10699
                        $user_id,
10700
                        $subject,
10701
                        $msg
10702
                    );
10703
                }
10704
            }
10705
        }
10706
    }
10707
10708
    /**
10709
     * Send notification for oral questions.
10710
     *
10711
     * @param array  $question_list_answers
10712
     * @param string $origin
10713
     * @param int    $exe_id
10714
     * @param array  $user_info
10715
     * @param string $url_email
10716
     * @param array  $teachers
10717
     */
10718
    private function sendNotificationForOralQuestions(
10719
        $question_list_answers,
10720
        $origin,
10721
        $exe_id,
10722
        $user_info,
10723
        $url_email,
10724
        $teachers
10725
    ): void {
10726
10727
        // Email configuration settings
10728
        $courseCode = api_get_course_id();
10729
        $courseInfo = api_get_course_info($courseCode);
10730
        $oral_question_list = null;
10731
        foreach ($question_list_answers as $item) {
10732
            $question = $item['question'];
10733
            $file = $item['generated_oral_file'];
10734
            $answer = $item['answer'];
10735
            if (0 == $answer) {
10736
                $answer = '';
10737
            }
10738
            $answer_type = $item['answer_type'];
10739
            if (!empty($question) && (!empty($answer) || !empty($file)) && ORAL_EXPRESSION == $answer_type) {
10740
                $oral_question_list .= '<br />
10741
                    <table width="730" height="136" border="0" cellpadding="3" cellspacing="3">
10742
                    <tr>
10743
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
10744
                        <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
10745
                    </tr>
10746
                    <tr>
10747
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
10748
                        <td valign="top" bgcolor="#F3F3F3"><p>'.$answer.'</p><p>'.$file.'</p></td>
10749
                    </tr></table>';
10750
            }
10751
        }
10752
10753
        if (!empty($oral_question_list)) {
10754
            $msg = get_lang('A learner has attempted one or more oral question').'<br /><br />
10755
                    '.get_lang('Attempt details').' : <br /><br />
10756
                    <table>
10757
                        <tr>
10758
                            <td><em>'.get_lang('Course name').'</em></td>
10759
                            <td>&nbsp;<b>#course#</b></td>
10760
                        </tr>
10761
                        <tr>
10762
                            <td>'.get_lang('Test attempted').'</td>
10763
                            <td>&nbsp;#exercise#</td>
10764
                        </tr>
10765
                        <tr>
10766
                            <td>'.get_lang('Learner name').'</td>
10767
                            <td>&nbsp;#firstName# #lastName#</td>
10768
                        </tr>
10769
                        <tr>
10770
                            <td>'.get_lang('Learner e-mail').'</td>
10771
                            <td>&nbsp;#mail#</td>
10772
                        </tr>
10773
                    </table>';
10774
            $msg .= '<br />'.sprintf(
10775
                    get_lang('The attempted oral questions are %s'),
10776
                    $oral_question_list
10777
                ).'<br />';
10778
            $msg1 = str_replace('#exercise#', $this->exercise, $msg);
10779
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg1);
10780
            $msg1 = str_replace('#lastName#', $user_info['lastname'], $msg);
10781
            $msg = str_replace('#mail#', $user_info['email'], $msg1);
10782
            $msg1 = str_replace('#course#', $courseInfo['name'], $msg);
10783
10784
            if (!in_array($origin, ['learnpath', 'embeddable'])) {
10785
                $msg1 .= '<br /><a href="#url#">'.get_lang(
10786
                        'Click this link to check the answer and/or give feedback'
10787
                    ).'</a>';
10788
            }
10789
            $msg = str_replace('#url#', $url_email, $msg1);
10790
            $mail_content = $msg;
10791
            $subject = get_lang('A learner has attempted one or more oral question');
10792
10793
            if (!empty($teachers)) {
10794
                foreach ($teachers as $user_id => $teacher_data) {
10795
                    MessageManager::send_message_simple(
10796
                        $user_id,
10797
                        $subject,
10798
                        $mail_content
10799
                    );
10800
                }
10801
            }
10802
        }
10803
    }
10804
10805
    /**
10806
     * Returns an array with the media list.
10807
     *
10808
     * @param array $questionList question list
10809
     *
10810
     * @example there's 1 question with iid 5 that belongs to the media question with iid = 100
10811
     * <code>
10812
     * array (size=2)
10813
     *  999 =>
10814
     *    array (size=3)
10815
     *      0 => int 7
10816
     *      1 => int 6
10817
     *      2 => int 3254
10818
     *  100 =>
10819
     *   array (size=1)
10820
     *      0 => int 5
10821
     *  </code>
10822
     */
10823
    private function setMediaList($questionList)
10824
    {
10825
        $mediaList = [];
10826
        /*
10827
         * Media feature is not activated in 1.11.x
10828
        if (!empty($questionList)) {
10829
            foreach ($questionList as $questionId) {
10830
                $objQuestionTmp = Question::read($questionId, $this->course_id);
10831
                // If a media question exists
10832
                if (isset($objQuestionTmp->parent_id) && $objQuestionTmp->parent_id != 0) {
10833
                    $mediaList[$objQuestionTmp->parent_id][] = $objQuestionTmp->id;
10834
                } else {
10835
                    // Always the last item
10836
                    $mediaList[999][] = $objQuestionTmp->id;
10837
                }
10838
            }
10839
        }*/
10840
10841
        $this->mediaList = $mediaList;
10842
    }
10843
10844
    /**
10845
     * @return HTML_QuickForm_group
10846
     */
10847
    private function setResultDisabledGroup(FormValidator $form)
10848
    {
10849
        $resultDisabledGroup = [];
10850
10851
        $resultDisabledGroup[] = $form->createElement(
10852
            'radio',
10853
            'results_disabled',
10854
            null,
10855
            get_lang('Auto-evaluation mode: show score and expected answers'),
10856
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
10857
            ['id' => 'result_disabled_0']
10858
        );
10859
10860
        $warning = sprintf(
10861
            get_lang('TheSettingXWillChangeToX'),
10862
            get_lang('FeedbackType'),
10863
            get_lang('NoFeedback')
10864
        );
10865
        $resultDisabledGroup[] = $form->createElement(
10866
            'radio',
10867
            'results_disabled',
10868
            null,
10869
            get_lang('Exam mode: Do not show score nor answers'),
10870
            RESULT_DISABLE_NO_SCORE_AND_EXPECTED_ANSWERS,
10871
            [
10872
                'id' => 'result_disabled_1',
10873
                //'onclick' => 'check_results_disabled()'
10874
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
10875
            ]
10876
        );
10877
10878
        $resultDisabledGroup[] = $form->createElement(
10879
            'radio',
10880
            'results_disabled',
10881
            null,
10882
            get_lang('Practice mode: Show score only, by category if at least one is used'),
10883
            RESULT_DISABLE_SHOW_SCORE_ONLY,
10884
            [
10885
                'id' => 'result_disabled_2',
10886
                //'onclick' => 'check_results_disabled()'
10887
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
10888
            ]
10889
        );
10890
10891
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
10892
            return $form->addGroup(
10893
                $resultDisabledGroup,
10894
                null,
10895
                get_lang(
10896
                    'Show score to learner'
10897
                )
10898
            );
10899
        }
10900
10901
        $resultDisabledGroup[] = $form->createElement(
10902
            'radio',
10903
            'results_disabled',
10904
            null,
10905
            get_lang(
10906
                'Show score on every attempt, show correct answers only on last attempt (only works with an attempts limit)'
10907
            ),
10908
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
10909
            ['id' => 'result_disabled_4']
10910
        );
10911
10912
        $resultDisabledGroup[] = $form->createElement(
10913
            'radio',
10914
            'results_disabled',
10915
            null,
10916
            get_lang(
10917
                'Do not show the score (only when user finishes all attempts) but show feedback for each attempt.'
10918
            ),
10919
            RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
10920
            [
10921
                'id' => 'result_disabled_5',
10922
                //'onclick' => 'check_results_disabled()'
10923
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
10924
            ]
10925
        );
10926
10927
        $resultDisabledGroup[] = $form->createElement(
10928
            'radio',
10929
            'results_disabled',
10930
            null,
10931
            get_lang(
10932
                'Ranking mode: Do not show results details question by question and show a table with the ranking of all other users.'
10933
            ),
10934
            RESULT_DISABLE_RANKING,
10935
            ['id' => 'result_disabled_6']
10936
        );
10937
10938
        $resultDisabledGroup[] = $form->createElement(
10939
            'radio',
10940
            'results_disabled',
10941
            null,
10942
            get_lang(
10943
                'Show only global score (not question score) and show only the correct answers, do not show incorrect answers at all'
10944
            ),
10945
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
10946
            ['id' => 'result_disabled_7']
10947
        );
10948
10949
        $resultDisabledGroup[] = $form->createElement(
10950
            'radio',
10951
            'results_disabled',
10952
            null,
10953
            get_lang('Auto-evaluation mode and ranking'),
10954
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
10955
            ['id' => 'result_disabled_8']
10956
        );
10957
10958
        $resultDisabledGroup[] = $form->createElement(
10959
            'radio',
10960
            'results_disabled',
10961
            null,
10962
            get_lang('Show score by category on a radar/spiderweb chart'),
10963
            RESULT_DISABLE_RADAR,
10964
            ['id' => 'result_disabled_9']
10965
        );
10966
10967
        $resultDisabledGroup[] = $form->createElement(
10968
            'radio',
10969
            'results_disabled',
10970
            null,
10971
            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.'),
10972
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
10973
            ['id' => 'result_disabled_10']
10974
        );
10975
10976
        return $form->addGroup(
10977
            $resultDisabledGroup,
10978
            null,
10979
            get_lang('Show score to learner')
10980
        );
10981
    }
10982
10983
    /**
10984
     * Return the text to display, based on the score and the max score.
10985
     * @param int|float $score
10986
     * @param int|float $maxScore
10987
     * @return string
10988
     */
10989
    public function getFinishText(int|float $score, int|float $maxScore): string
10990
    {
10991
        $passPercentage = $this->selectPassPercentage();
10992
        if (!empty($passPercentage)) {
10993
            $percentage = float_format(
10994
                ($score / (0 != $maxScore ? $maxScore : 1)) * 100,
10995
                1
10996
            );
10997
            if ($percentage >= $passPercentage) {
10998
                return $this->getTextWhenFinished();
10999
            } else {
11000
                return $this->getTextWhenFinishedFailure();
11001
            }
11002
        } else {
11003
            return $this->getTextWhenFinished();
11004
        }
11005
11006
        return '';
0 ignored issues
show
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...
11007
    }
11008
}
11009