Passed
Push — master ( 4b8829...cf401b )
by Yannick
07:57
created

Exercise::getReminderTable()   C

Complexity

Conditions 10
Paths 160

Size

Total Lines 142
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 52
nc 160
nop 3
dl 0
loc 142
rs 6.7806
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
6
use Chamilo\CoreBundle\Component\Utils\ToolIcon;
7
use Chamilo\CoreBundle\Entity\GradebookLink;
8
use Chamilo\CoreBundle\Entity\TrackEExercise;
9
use Chamilo\CoreBundle\Entity\TrackEExerciseConfirmation;
10
use Chamilo\CoreBundle\Entity\TrackEHotspot;
11
use Chamilo\CoreBundle\Framework\Container;
12
use Chamilo\CourseBundle\Entity\CExerciseCategory;
13
use Chamilo\CourseBundle\Entity\CQuiz;
14
use Chamilo\CourseBundle\Entity\CQuizRelQuestionCategory;
15
use ChamiloSession as Session;
16
use Chamilo\CoreBundle\Component\Utils\ActionIcon;
17
use Chamilo\CoreBundle\Component\Utils\StateIcon;
18
19
20
/**
21
 * @author Olivier Brouckaert
22
 * @author Julio Montoya Cleaning exercises
23
 * @author Hubert Borderiou #294
24
 */
25
class Exercise
26
{
27
    public const PAGINATION_ITEMS_PER_PAGE = 20;
28
    public $iId;
29
    public $id;
30
    public $name;
31
    public $title;
32
    public $exercise;
33
    public $description;
34
    public $sound;
35
    public $type; //ALL_ON_ONE_PAGE or ONE_PER_PAGE
36
    public $random;
37
    public $random_answers;
38
    public $active;
39
    public $timeLimit;
40
    public $attempts;
41
    public $feedback_type;
42
    public $end_time;
43
    public $start_time;
44
    public $questionList; // array with the list of this exercise's questions
45
    /* including question list of the media */
46
    public $questionListUncompressed;
47
    public $results_disabled;
48
    public $expired_time;
49
    public $course;
50
    public $course_id;
51
    public $propagate_neg;
52
    public $saveCorrectAnswers;
53
    public $review_answers;
54
    public $randomByCat;
55
    public $text_when_finished;
56
    public $text_when_finished_failure;
57
    public $display_category_name;
58
    public $pass_percentage;
59
    public $edit_exercise_in_lp = false;
60
    public $is_gradebook_locked = false;
61
    public $exercise_was_added_in_lp = false;
62
    public $lpList = [];
63
    public $force_edit_exercise_in_lp = false;
64
    public $categories;
65
    public $categories_grouping = true;
66
    public $endButton = 0;
67
    public $categoryWithQuestionList;
68
    public $mediaList;
69
    public $loadQuestionAJAX = false;
70
    // Notification send to the teacher.
71
    public $emailNotificationTemplate = null;
72
    // Notification send to the student.
73
    public $emailNotificationTemplateToUser = null;
74
    public $countQuestions = 0;
75
    public $fastEdition = false;
76
    public $modelType = 1;
77
    public $questionSelectionType = EX_Q_SELECTION_ORDERED;
78
    public $hideQuestionTitle = 0;
79
    public $scoreTypeModel = 0;
80
    public $categoryMinusOne = true; // Shows the category -1: See BT#6540
81
    public $globalCategoryId = null;
82
    public $onSuccessMessage = null;
83
    public $onFailedMessage = null;
84
    public $emailAlert;
85
    public $notifyUserByEmail = '';
86
    public $sessionId = 0;
87
    public $questionFeedbackEnabled = false;
88
    public $questionTypeWithFeedback;
89
    public $showPreviousButton;
90
    public $notifications;
91
    public $export = false;
92
    public $autolaunch;
93
    public $exerciseCategoryId;
94
    public $pageResultConfiguration;
95
    public $hideQuestionNumber;
96
    public $preventBackwards;
97
    public $currentQuestion;
98
    public $hideComment;
99
    public $hideNoAnswer;
100
    public $hideExpectedAnswer;
101
    public $forceShowExpectedChoiceColumn;
102
    public $disableHideCorrectAnsweredQuestions;
103
104
    /**
105
     * @param int $courseId
106
     */
107
    public function __construct($courseId = 0)
108
    {
109
        $this->iId = 0;
110
        $this->id = 0;
111
        $this->exercise = '';
112
        $this->description = '';
113
        $this->sound = '';
114
        $this->type = ALL_ON_ONE_PAGE;
115
        $this->random = 0;
116
        $this->random_answers = 0;
117
        $this->active = 1;
118
        $this->questionList = [];
119
        $this->timeLimit = 0;
120
        $this->end_time = '';
121
        $this->start_time = '';
122
        $this->results_disabled = 1;
123
        $this->expired_time = 0;
124
        $this->propagate_neg = 0;
125
        $this->saveCorrectAnswers = 0;
126
        $this->review_answers = false;
127
        $this->randomByCat = 0;
128
        $this->text_when_finished = '';
129
        $this->text_when_finished_failure = '';
130
        $this->display_category_name = 0;
131
        $this->pass_percentage = 0;
132
        $this->modelType = 1;
133
        $this->questionSelectionType = EX_Q_SELECTION_ORDERED;
134
        $this->endButton = 0;
135
        $this->scoreTypeModel = 0;
136
        $this->globalCategoryId = null;
137
        $this->notifications = [];
138
        $this->exerciseCategoryId = 0;
139
        $this->pageResultConfiguration = null;
140
        $this->hideQuestionNumber = 0;
141
        $this->preventBackwards = 0;
142
        $this->hideComment = false;
143
        $this->hideNoAnswer = false;
144
        $this->hideExpectedAnswer = false;
145
        $this->disableHideCorrectAnsweredQuestions = false;
146
147
        if (!empty($courseId)) {
148
            $courseInfo = api_get_course_info_by_id($courseId);
149
        } else {
150
            $courseInfo = api_get_course_info();
151
        }
152
        $this->course_id = $courseInfo['real_id'];
153
        $this->course = $courseInfo;
154
        $this->sessionId = api_get_session_id();
155
156
        // ALTER TABLE c_quiz_question ADD COLUMN feedback text;
157
        $this->questionFeedbackEnabled = ('true' === api_get_setting('exercise.allow_quiz_question_feedback'));
158
        $this->showPreviousButton = true;
159
    }
160
161
    /**
162
     * Reads exercise information from the data base.
163
     *
164
     * @param int  $id                - exercise Id
165
     * @param bool $parseQuestionList
166
     *
167
     * @return bool - true if exercise exists, otherwise false
168
     */
169
    public function read($id, $parseQuestionList = true)
170
    {
171
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
172
173
        $id = (int) $id;
174
        if (empty($this->course_id) || empty($id)) {
175
            return false;
176
        }
177
178
        $sql = "SELECT * FROM $table
179
                WHERE iid = $id";
180
        $result = Database::query($sql);
181
182
        // if the exercise has been found
183
        if ($object = Database::fetch_object($result)) {
184
            $this->id = $this->iId = (int) $object->iid;
185
            $this->exercise = $object->title;
186
            $this->name = $object->title;
187
            $this->title = $object->title;
188
            $this->description = $object->description;
189
            $this->sound = $object->sound;
190
            $this->type = $object->type;
191
            if (empty($this->type)) {
192
                $this->type = ONE_PER_PAGE;
193
            }
194
            $this->random = $object->random;
195
            $this->random_answers = $object->random_answers;
196
            $this->active = $object->active;
197
            $this->results_disabled = $object->results_disabled;
198
            $this->attempts = $object->max_attempt;
199
            $this->feedback_type = $object->feedback_type;
200
            //$this->sessionId = $object->session_id;
201
            $this->propagate_neg = $object->propagate_neg;
202
            $this->saveCorrectAnswers = $object->save_correct_answers;
203
            $this->randomByCat = $object->random_by_category;
204
            $this->text_when_finished = $object->text_when_finished;
205
            $this->text_when_finished_failure = $object->text_when_finished_failure;
206
            $this->display_category_name = $object->display_category_name;
207
            $this->pass_percentage = $object->pass_percentage;
208
            $this->is_gradebook_locked = api_resource_is_locked_by_gradebook($id, LINK_EXERCISE);
209
            $this->review_answers = isset($object->review_answers) && 1 == $object->review_answers ? true : false;
210
            $this->globalCategoryId = isset($object->global_category_id) ? $object->global_category_id : null;
211
            $this->questionSelectionType = isset($object->question_selection_type) ? (int) $object->question_selection_type : null;
212
            $this->hideQuestionTitle = isset($object->hide_question_title) ? (int) $object->hide_question_title : 0;
213
            $this->autolaunch = isset($object->autolaunch) ? (int) $object->autolaunch : 0;
214
            $this->exerciseCategoryId = isset($object->exercise_category_id) ? (int) $object->exercise_category_id : null;
215
            $this->preventBackwards = isset($object->prevent_backwards) ? (int) $object->prevent_backwards : 0;
216
            $this->exercise_was_added_in_lp = false;
217
            $this->lpList = [];
218
            $this->notifications = [];
219
            if (!empty($object->notifications)) {
220
                $this->notifications = explode(',', $object->notifications);
221
            }
222
223
            if (!empty($object->page_result_configuration)) {
224
                //$this->pageResultConfiguration = $object->page_result_configuration;
225
            }
226
227
            $this->hideQuestionNumber = 1 == $object->hide_question_number;
228
229
            if (isset($object->show_previous_button)) {
230
                $this->showPreviousButton = 1 == $object->show_previous_button ? true : false;
231
            }
232
233
            $list = self::getLpListFromExercise($id, $this->course_id);
234
            if (!empty($list)) {
235
                $this->exercise_was_added_in_lp = true;
236
                $this->lpList = $list;
237
            }
238
239
            $this->force_edit_exercise_in_lp = api_get_configuration_value('force_edit_exercise_in_lp');
240
            $this->edit_exercise_in_lp = true;
241
            if ($this->exercise_was_added_in_lp) {
242
                $this->edit_exercise_in_lp = true == $this->force_edit_exercise_in_lp;
243
            }
244
245
            if (!empty($object->end_time)) {
246
                $this->end_time = $object->end_time;
247
            }
248
            if (!empty($object->start_time)) {
249
                $this->start_time = $object->start_time;
250
            }
251
252
            // Control time
253
            $this->expired_time = $object->expired_time;
254
255
            // Checking if question_order is correctly set
256
            if ($parseQuestionList) {
257
                $this->setQuestionList(true);
258
            }
259
260
            //overload questions list with recorded questions list
261
            //load questions only for exercises of type 'one question per page'
262
            //this is needed only is there is no questions
263
264
            // @todo not sure were in the code this is used somebody mess with the exercise tool
265
            // @todo don't know who add that config and why $_configuration['live_exercise_tracking']
266
            /*global $_configuration, $questionList;
267
            if ($this->type == ONE_PER_PAGE && $_SERVER['REQUEST_METHOD'] != 'POST'
268
                && defined('QUESTION_LIST_ALREADY_LOGGED') &&
269
                isset($_configuration['live_exercise_tracking']) && $_configuration['live_exercise_tracking']
270
            ) {
271
                $this->questionList = $questionList;
272
            }*/
273
274
            return true;
275
        }
276
277
        return false;
278
    }
279
280
    public function getCutTitle(): string
281
    {
282
        $title = $this->getUnformattedTitle();
283
284
        return cut($title, EXERCISE_MAX_NAME_SIZE);
285
    }
286
287
    public function getId()
288
    {
289
        return (int) $this->iId;
290
    }
291
292
    /**
293
     * returns the exercise title.
294
     *
295
     * @param bool $unformattedText Optional. Get the title without HTML tags
296
     *
297
     * @return string - exercise title
298
     */
299
    public function selectTitle($unformattedText = false)
300
    {
301
        if ($unformattedText) {
302
            return $this->getUnformattedTitle();
303
        }
304
305
        return $this->exercise;
306
    }
307
308
    /**
309
     * returns the number of attempts setted.
310
     *
311
     * @return int - exercise attempts
312
     */
313
    public function selectAttempts()
314
    {
315
        return $this->attempts;
316
    }
317
318
    /**
319
     * Returns the number of FeedbackType
320
     *  0: Feedback , 1: DirectFeedback, 2: NoFeedback.
321
     *
322
     * @return int - exercise attempts
323
     */
324
    public function getFeedbackType()
325
    {
326
        return (int) $this->feedback_type;
327
    }
328
329
    /**
330
     * returns the time limit.
331
     *
332
     * @return int
333
     */
334
    public function selectTimeLimit()
335
    {
336
        return $this->timeLimit;
337
    }
338
339
    /**
340
     * returns the exercise description.
341
     *
342
     * @return string - exercise description
343
     */
344
    public function selectDescription()
345
    {
346
        return $this->description;
347
    }
348
349
    /**
350
     * returns the exercise sound file.
351
     */
352
    public function getSound()
353
    {
354
        return $this->sound;
355
    }
356
357
    /**
358
     * returns the exercise type.
359
     *
360
     * @return int - exercise type
361
     *
362
     * @author Olivier Brouckaert
363
     */
364
    public function selectType()
365
    {
366
        return $this->type;
367
    }
368
369
    /**
370
     * @return int
371
     */
372
    public function getModelType()
373
    {
374
        return $this->modelType;
375
    }
376
377
    /**
378
     * @return int
379
     */
380
    public function selectEndButton()
381
    {
382
        return $this->endButton;
383
    }
384
385
    /**
386
     * @return int : do we display the question category name for students
387
     *
388
     * @author hubert borderiou 30-11-11
389
     */
390
    public function selectDisplayCategoryName()
391
    {
392
        return $this->display_category_name;
393
    }
394
395
    /**
396
     * @return int
397
     */
398
    public function selectPassPercentage()
399
    {
400
        return $this->pass_percentage;
401
    }
402
403
    /**
404
     * Modify object to update the switch display_category_name.
405
     *
406
     * @param int $value is an integer 0 or 1
407
     *
408
     * @author hubert borderiou 30-11-11
409
     */
410
    public function updateDisplayCategoryName($value)
411
    {
412
        $this->display_category_name = $value;
413
    }
414
415
    /**
416
     * @return string html text : the text to display ay the end of the test
417
     *
418
     * @author hubert borderiou 28-11-11
419
     */
420
    public function getTextWhenFinished(): string
421
    {
422
        return $this->text_when_finished;
423
    }
424
425
    /**
426
     * @param string $text
427
     *
428
     * @author hubert borderiou 28-11-11
429
     */
430
    public function setTextWhenFinished(string $text): void
431
    {
432
        $this->text_when_finished = $text;
433
    }
434
435
    /**
436
     * Get the text to display when the user has failed the test
437
     * @return string html text : the text to display ay the end of the test
438
     */
439
    public function getTextWhenFinishedFailure(): string
440
    {
441
        if (empty($this->text_when_finished_failure)) {
442
            return '';
443
        }
444
445
        return $this->text_when_finished_failure;
446
    }
447
448
    /**
449
     * Set the text to display when the user has succeeded in the test
450
     * @param string $text
451
     */
452
    public function setTextWhenFinishedFailure(string $text): void
453
    {
454
        $this->text_when_finished_failure = $text;
455
    }
456
457
    /**
458
     * return 1 or 2 if randomByCat.
459
     *
460
     * @return int - quiz random by category
461
     *
462
     * @author hubert borderiou
463
     */
464
    public function getRandomByCategory()
465
    {
466
        return $this->randomByCat;
467
    }
468
469
    /**
470
     * return 0 if no random by cat
471
     * return 1 if random by cat, categories shuffled
472
     * return 2 if random by cat, categories sorted by alphabetic order.
473
     *
474
     * @return int - quiz random by category
475
     *
476
     * @author hubert borderiou
477
     */
478
    public function isRandomByCat()
479
    {
480
        $res = EXERCISE_CATEGORY_RANDOM_DISABLED;
481
        if (EXERCISE_CATEGORY_RANDOM_SHUFFLED == $this->randomByCat) {
482
            $res = EXERCISE_CATEGORY_RANDOM_SHUFFLED;
483
        } elseif (EXERCISE_CATEGORY_RANDOM_ORDERED == $this->randomByCat) {
484
            $res = EXERCISE_CATEGORY_RANDOM_ORDERED;
485
        }
486
487
        return $res;
488
    }
489
490
    /**
491
     * return nothing
492
     * update randomByCat value for object.
493
     *
494
     * @param int $random
495
     *
496
     * @author hubert borderiou
497
     */
498
    public function updateRandomByCat($random)
499
    {
500
        $this->randomByCat = EXERCISE_CATEGORY_RANDOM_DISABLED;
501
        if (in_array(
502
            $random,
503
            [
504
                EXERCISE_CATEGORY_RANDOM_SHUFFLED,
505
                EXERCISE_CATEGORY_RANDOM_ORDERED,
506
                EXERCISE_CATEGORY_RANDOM_DISABLED,
507
            ]
508
        )) {
509
            $this->randomByCat = $random;
510
        }
511
    }
512
513
    /**
514
     * Tells if questions are selected randomly, and if so returns the draws.
515
     *
516
     * @return int - results disabled exercise
517
     *
518
     * @author Carlos Vargas
519
     */
520
    public function selectResultsDisabled()
521
    {
522
        return $this->results_disabled;
523
    }
524
525
    /**
526
     * tells if questions are selected randomly, and if so returns the draws.
527
     *
528
     * @return bool
529
     *
530
     * @author Olivier Brouckaert
531
     */
532
    public function isRandom()
533
    {
534
        $isRandom = false;
535
        // "-1" means all questions will be random
536
        if ($this->random > 0 || -1 == $this->random) {
537
            $isRandom = true;
538
        }
539
540
        return $isRandom;
541
    }
542
543
    /**
544
     * returns random answers status.
545
     *
546
     * @author Juan Carlos Rana
547
     */
548
    public function getRandomAnswers()
549
    {
550
        return $this->random_answers;
551
    }
552
553
    /**
554
     * Same as isRandom() but has a name applied to values different than 0 or 1.
555
     *
556
     * @return int
557
     */
558
    public function getShuffle()
559
    {
560
        return $this->random;
561
    }
562
563
    /**
564
     * returns the exercise status (1 = enabled ; 0 = disabled).
565
     *
566
     * @return int - 1 if enabled, otherwise 0
567
     *
568
     * @author Olivier Brouckaert
569
     */
570
    public function selectStatus()
571
    {
572
        return $this->active;
573
    }
574
575
    /**
576
     * If false the question list will be managed as always if true
577
     * the question will be filtered
578
     * depending of the exercise settings (table c_quiz_rel_category).
579
     *
580
     * @param bool $status active or inactive grouping
581
     */
582
    public function setCategoriesGrouping($status)
583
    {
584
        $this->categories_grouping = (bool) $status;
585
    }
586
587
    /**
588
     * @return int
589
     */
590
    public function getHideQuestionTitle()
591
    {
592
        return $this->hideQuestionTitle;
593
    }
594
595
    /**
596
     * @param $value
597
     */
598
    public function setHideQuestionTitle($value)
599
    {
600
        $this->hideQuestionTitle = (int) $value;
601
    }
602
603
    /**
604
     * @return int
605
     */
606
    public function getScoreTypeModel()
607
    {
608
        return $this->scoreTypeModel;
609
    }
610
611
    /**
612
     * @param int $value
613
     */
614
    public function setScoreTypeModel($value)
615
    {
616
        $this->scoreTypeModel = (int) $value;
617
    }
618
619
    /**
620
     * @return int
621
     */
622
    public function getGlobalCategoryId()
623
    {
624
        return $this->globalCategoryId;
625
    }
626
627
    /**
628
     * @param int $value
629
     */
630
    public function setGlobalCategoryId($value)
631
    {
632
        if (is_array($value) && isset($value[0])) {
633
            $value = $value[0];
634
        }
635
        $this->globalCategoryId = (int) $value;
636
    }
637
638
    /**
639
     * @param int    $start
640
     * @param int    $limit
641
     * @param string $sidx
642
     * @param string $sord
643
     * @param array  $whereCondition
644
     * @param array  $extraFields
645
     *
646
     * @return array
647
     */
648
    public function getQuestionListPagination(
649
        $start,
650
        $limit,
651
        $sidx,
652
        $sord,
653
        $whereCondition = [],
654
        $extraFields = []
655
    ) {
656
        if (!empty($this->id)) {
657
            $category_list = TestCategory::getListOfCategoriesNameForTest(
658
                $this->id,
659
                false
660
            );
661
            $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
662
            $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
663
664
            $sql = "SELECT q.iid
665
                    FROM $TBL_EXERCICE_QUESTION e
666
                    INNER JOIN $TBL_QUESTIONS  q
667
                    ON (e.question_id = q.iid)
668
					WHERE e.quiz_id	= '".$this->id."' ";
669
670
            $orderCondition = ' ORDER BY question_order ';
671
672
            if (!empty($sidx) && !empty($sord)) {
673
                if ('question' === $sidx) {
674
                    if (in_array(strtolower($sord), ['desc', 'asc'])) {
675
                        $orderCondition = " ORDER BY `q.$sidx` $sord";
676
                    }
677
                }
678
            }
679
680
            $sql .= $orderCondition;
681
            $limitCondition = null;
682
            if (isset($start) && isset($limit)) {
683
                $start = (int) $start;
684
                $limit = (int) $limit;
685
                $limitCondition = " LIMIT $start, $limit";
686
            }
687
            $sql .= $limitCondition;
688
            $result = Database::query($sql);
689
            $questions = [];
690
            if (Database::num_rows($result)) {
691
                if (!empty($extraFields)) {
692
                    $extraFieldValue = new ExtraFieldValue('question');
693
                }
694
                while ($question = Database::fetch_array($result, 'ASSOC')) {
695
                    /** @var Question $objQuestionTmp */
696
                    $objQuestionTmp = Question::read($question['iid']);
697
                    $category_labels = '';
698
                    // @todo not implemented in 1.11.x
699
                    /*$category_labels = TestCategory::return_category_labels(
700
                        $objQuestionTmp->category_list,
701
                        $category_list
702
                    );*/
703
704
                    if (empty($category_labels)) {
705
                        $category_labels = '-';
706
                    }
707
708
                    // Question type
709
                    $typeImg = $objQuestionTmp->getTypePicture();
710
                    $typeExpl = $objQuestionTmp->getExplanation();
711
712
                    $question_media = null;
713
                    if (!empty($objQuestionTmp->parent_id)) {
714
                        // @todo not implemented in 1.11.x
715
                        //$objQuestionMedia = Question::read($objQuestionTmp->parent_id);
716
                        //$question_media = Question::getMediaLabel($objQuestionMedia->question);
717
                    }
718
719
                    $questionType = Display::tag(
720
                        'div',
721
                        Display::return_icon($typeImg, $typeExpl, [], ICON_SIZE_MEDIUM).$question_media
722
                    );
723
724
                    $question = [
725
                        'id' => $question['iid'],
726
                        'question' => $objQuestionTmp->selectTitle(),
727
                        'type' => $questionType,
728
                        'category' => Display::tag(
729
                            'div',
730
                            '<a href="#" style="padding:0px; margin:0px;">'.$category_labels.'</a>'
731
                        ),
732
                        'score' => $objQuestionTmp->selectWeighting(),
733
                        'level' => $objQuestionTmp->level,
734
                    ];
735
736
                    if (!empty($extraFields)) {
737
                        foreach ($extraFields as $extraField) {
738
                            $value = $extraFieldValue->get_values_by_handler_and_field_id(
739
                                $question['id'],
740
                                $extraField['id']
741
                            );
742
                            $stringValue = null;
743
                            if ($value) {
744
                                $stringValue = $value['field_value'];
745
                            }
746
                            $question[$extraField['field_variable']] = $stringValue;
747
                        }
748
                    }
749
                    $questions[] = $question;
750
                }
751
            }
752
753
            return $questions;
754
        }
755
    }
756
757
    /**
758
     * Get question count per exercise from DB (any special treatment).
759
     *
760
     * @return int
761
     */
762
    public function getQuestionCount()
763
    {
764
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
765
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
766
        $sql = "SELECT count(q.iid) as count
767
                FROM $TBL_EXERCICE_QUESTION e
768
                INNER JOIN $TBL_QUESTIONS q
769
                ON (e.question_id = q.iid)
770
                WHERE
771
                    e.quiz_id = ".$this->getId();
772
        $result = Database::query($sql);
773
774
        $count = 0;
775
        if (Database::num_rows($result)) {
776
            $row = Database::fetch_array($result);
777
            $count = (int) $row['count'];
778
        }
779
780
        return $count;
781
    }
782
783
    /**
784
     * @return array
785
     */
786
    public function getQuestionOrderedListByName()
787
    {
788
        if (empty($this->course_id) || empty($this->getId())) {
789
            return [];
790
        }
791
792
        $exerciseQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
793
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
794
795
        // Getting question list from the order (question list drag n drop interface ).
796
        $sql = "SELECT e.question_id
797
                FROM $exerciseQuestionTable e
798
                INNER JOIN $questionTable q
799
                ON (e.question_id= q.iid)
800
                WHERE
801
                    e.quiz_id = '".$this->getId()."'
802
                ORDER BY q.question";
803
        $result = Database::query($sql);
804
        $list = [];
805
        if (Database::num_rows($result)) {
806
            $list = Database::store_result($result, 'ASSOC');
807
        }
808
809
        return $list;
810
    }
811
812
    /**
813
     * Selecting question list depending in the exercise-category
814
     * relationship (category table in exercise settings).
815
     *
816
     * @param array $questionList
817
     * @param int   $questionSelectionType
818
     *
819
     * @return array
820
     */
821
    public function getQuestionListWithCategoryListFilteredByCategorySettings(
822
        $questionList,
823
        $questionSelectionType
824
    ) {
825
        $result = [
826
            'question_list' => [],
827
            'category_with_questions_list' => [],
828
        ];
829
830
        // Order/random categories
831
        $cat = new TestCategory();
832
833
        // Setting category order.
834
        switch ($questionSelectionType) {
835
            case EX_Q_SELECTION_ORDERED: // 1
836
            case EX_Q_SELECTION_RANDOM:  // 2
837
                // This options are not allowed here.
838
                break;
839
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED: // 3
840
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
841
                    $this,
842
                    'title ASC',
843
                    false,
844
                    true
845
                );
846
847
                $questionsByCategory = TestCategory::getQuestionsByCat(
848
                    $this->getId(),
849
                    $questionList,
850
                    $categoriesAddedInExercise
851
                );
852
853
                $questionList = $this->pickQuestionsPerCategory(
854
                    $categoriesAddedInExercise,
855
                    $questionList,
856
                    $questionsByCategory,
857
                    true,
858
                    false
859
                );
860
861
                break;
862
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED: // 4
863
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7
864
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
865
                    $this,
866
                    null,
867
                    true,
868
                    true
869
                );
870
                $questionsByCategory = TestCategory::getQuestionsByCat(
871
                    $this->getId(),
872
                    $questionList,
873
                    $categoriesAddedInExercise
874
                );
875
                $questionList = $this->pickQuestionsPerCategory(
876
                    $categoriesAddedInExercise,
877
                    $questionList,
878
                    $questionsByCategory,
879
                    true,
880
                    false
881
                );
882
883
                break;
884
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM: // 5
885
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
886
                    $this,
887
                    'title ASC',
888
                    false,
889
                    true
890
                );
891
                $questionsByCategory = TestCategory::getQuestionsByCat(
892
                    $this->getId(),
893
                    $questionList,
894
                    $categoriesAddedInExercise
895
                );
896
                $questionsByCategoryMandatory = [];
897
                if (EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM == $this->getQuestionSelectionType() &&
898
                    ('true' === api_get_setting('exercise.allow_mandatory_question_in_category'))
899
                ) {
900
                    $questionsByCategoryMandatory = TestCategory::getQuestionsByCat(
901
                        $this->id,
902
                        $questionList,
903
                        $categoriesAddedInExercise,
904
                        true
905
                    );
906
                }
907
                $questionList = $this->pickQuestionsPerCategory(
908
                    $categoriesAddedInExercise,
909
                    $questionList,
910
                    $questionsByCategory,
911
                    true,
912
                    true,
913
                    $questionsByCategoryMandatory
914
                );
915
916
                break;
917
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM: // 6
918
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED:
919
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
920
                    $this,
921
                    null,
922
                    true,
923
                    true
924
                );
925
926
                $questionsByCategory = TestCategory::getQuestionsByCat(
927
                    $this->getId(),
928
                    $questionList,
929
                    $categoriesAddedInExercise
930
                );
931
932
                $questionList = $this->pickQuestionsPerCategory(
933
                    $categoriesAddedInExercise,
934
                    $questionList,
935
                    $questionsByCategory,
936
                    true,
937
                    true
938
                );
939
940
                break;
941
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED: // 9
942
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
943
                    $this,
944
                    'root ASC, lft ASC',
945
                    false,
946
                    true
947
                );
948
                $questionsByCategory = TestCategory::getQuestionsByCat(
949
                    $this->getId(),
950
                    $questionList,
951
                    $categoriesAddedInExercise
952
                );
953
                $questionList = $this->pickQuestionsPerCategory(
954
                    $categoriesAddedInExercise,
955
                    $questionList,
956
                    $questionsByCategory,
957
                    true,
958
                    false
959
                );
960
961
                break;
962
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM: // 10
963
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
964
                    $this,
965
                    'root, lft ASC',
966
                    false,
967
                    true
968
                );
969
                $questionsByCategory = TestCategory::getQuestionsByCat(
970
                    $this->getId(),
971
                    $questionList,
972
                    $categoriesAddedInExercise
973
                );
974
                $questionList = $this->pickQuestionsPerCategory(
975
                    $categoriesAddedInExercise,
976
                    $questionList,
977
                    $questionsByCategory,
978
                    true,
979
                    true
980
                );
981
982
                break;
983
        }
984
985
        $result['question_list'] = $questionList ?? [];
986
        $result['category_with_questions_list'] = $questionsByCategory ?? [];
987
        $parentsLoaded = [];
988
        // Adding category info in the category list with question list:
989
        if (!empty($questionsByCategory)) {
990
            $newCategoryList = [];
991
            $em = Database::getManager();
992
            $repo = $em->getRepository(CQuizRelQuestionCategory::class);
993
994
            foreach ($questionsByCategory as $categoryId => $questionList) {
995
                $category = new TestCategory();
996
                $cat = (array) $category->getCategory($categoryId);
997
                if ($cat) {
998
                    $cat['iid'] = $cat['id'];
999
                }
1000
1001
                $categoryParentInfo = null;
1002
                // Parent is not set no loop here
1003
                if (isset($cat['parent_id']) && !empty($cat['parent_id'])) {
1004
                    /** @var CQuizRelQuestionCategory $categoryEntity */
1005
                    if (!isset($parentsLoaded[$cat['parent_id']])) {
1006
                        $categoryEntity = $em->find(CQuizRelQuestionCategory::class, $cat['parent_id']);
1007
                        $parentsLoaded[$cat['parent_id']] = $categoryEntity;
1008
                    } else {
1009
                        $categoryEntity = $parentsLoaded[$cat['parent_id']];
1010
                    }
1011
                    $path = $repo->getPath($categoryEntity);
1012
1013
                    $index = 0;
1014
                    if ($this->categoryMinusOne) {
1015
                        //$index = 1;
1016
                    }
1017
1018
                    /** @var CQuizRelQuestionCategory $categoryParent */
1019
                    // @todo not implemented in 1.11.x
1020
                    /*foreach ($path as $categoryParent) {
1021
                        $visibility = $categoryParent->getVisibility();
1022
                        if (0 == $visibility) {
1023
                            $categoryParentId = $categoryId;
1024
                            $categoryTitle = $cat['title'];
1025
                            if (count($path) > 1) {
1026
                                continue;
1027
                            }
1028
                        } else {
1029
                            $categoryParentId = $categoryParent->getIid();
1030
                            $categoryTitle = $categoryParent->getTitle();
1031
                        }
1032
1033
                        $categoryParentInfo['id'] = $categoryParentId;
1034
                        $categoryParentInfo['iid'] = $categoryParentId;
1035
                        $categoryParentInfo['parent_path'] = null;
1036
                        $categoryParentInfo['title'] = $categoryTitle;
1037
                        $categoryParentInfo['name'] = $categoryTitle;
1038
                        $categoryParentInfo['parent_id'] = null;
1039
1040
                        break;
1041
                    }*/
1042
                }
1043
                $cat['parent_info'] = $categoryParentInfo;
1044
                $newCategoryList[$categoryId] = [
1045
                    'category' => $cat,
1046
                    'question_list' => $questionList,
1047
                ];
1048
            }
1049
1050
            $result['category_with_questions_list'] = $newCategoryList;
1051
        }
1052
1053
        return $result;
1054
    }
1055
1056
    /**
1057
     * returns the array with the question ID list.
1058
     *
1059
     * @param bool $fromDatabase Whether the results should be fetched in the database or just from memory
1060
     * @param bool $adminView    Whether we should return all questions (admin view) or
1061
     *                           just a list limited by the max number of random questions
1062
     *
1063
     * @return array - question ID list
1064
     */
1065
    public function selectQuestionList($fromDatabase = false, $adminView = false)
1066
    {
1067
        //var_dump($this->getId());exit;
1068
        if ($fromDatabase && !empty($this->getId())) {
1069
            $nbQuestions = $this->getQuestionCount();
1070
1071
            $questionSelectionType = $this->getQuestionSelectionType();
1072
1073
            switch ($questionSelectionType) {
1074
                case EX_Q_SELECTION_ORDERED:
1075
                    $questionList = $this->getQuestionOrderedList($adminView);
1076
1077
                    break;
1078
                case EX_Q_SELECTION_RANDOM:
1079
                    // Not a random exercise, or if there are not at least 2 questions
1080
                    if (0 == $this->random || $nbQuestions < 2) {
1081
                        $questionList = $this->getQuestionOrderedList($adminView);
1082
                    } else {
1083
                        $questionList = $this->getRandomList($adminView);
1084
                    }
1085
1086
                    break;
1087
                default:
1088
                    $questionList = $this->getQuestionOrderedList($adminView);
1089
                    $result = $this->getQuestionListWithCategoryListFilteredByCategorySettings(
1090
                        $questionList,
1091
                        $questionSelectionType
1092
                    );
1093
                    $this->categoryWithQuestionList = $result['category_with_questions_list'];
1094
                    $questionList = $result['question_list'];
1095
1096
                    break;
1097
            }
1098
1099
            return $questionList;
1100
        }
1101
1102
        return $this->questionList;
1103
    }
1104
1105
    /**
1106
     * returns the number of questions in this exercise.
1107
     *
1108
     * @return int - number of questions
1109
     */
1110
    public function selectNbrQuestions()
1111
    {
1112
        return count($this->questionList);
1113
    }
1114
1115
    /**
1116
     * @return int
1117
     */
1118
    public function selectPropagateNeg()
1119
    {
1120
        return $this->propagate_neg;
1121
    }
1122
1123
    /**
1124
     * @return int
1125
     */
1126
    public function getSaveCorrectAnswers()
1127
    {
1128
        return $this->saveCorrectAnswers;
1129
    }
1130
1131
    /**
1132
     * Selects questions randomly in the question list.
1133
     *
1134
     * @param bool $adminView Whether we should return all
1135
     *                        questions (admin view) or just a list limited by the max number of random questions
1136
     *
1137
     * @return array - if the exercise is not set to take questions randomly, returns the question list
1138
     *               without randomizing, otherwise, returns the list with questions selected randomly
1139
     *
1140
     * @author Olivier Brouckaert
1141
     * @author Hubert Borderiou 15 nov 2011
1142
     */
1143
    public function getRandomList($adminView = false)
1144
    {
1145
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1146
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
1147
        $random = isset($this->random) && !empty($this->random) ? $this->random : 0;
1148
1149
        // Random with limit
1150
        $randomLimit = " ORDER BY RAND() LIMIT $random";
1151
1152
        // Random with no limit
1153
        if (-1 == $random) {
1154
            $randomLimit = ' ORDER BY RAND() ';
1155
        }
1156
1157
        // Admin see the list in default order
1158
        if (true === $adminView) {
1159
            // If viewing it as admin for edition, don't show it randomly, use title + id
1160
            $randomLimit = 'ORDER BY e.question_order';
1161
        }
1162
1163
        $sql = "SELECT e.question_id
1164
                FROM $quizRelQuestion e
1165
                INNER JOIN $question q
1166
                ON (e.question_id= q.iid)
1167
                WHERE
1168
                    e.quiz_id = '".$this->getId()."'
1169
                    $randomLimit ";
1170
        $result = Database::query($sql);
1171
        $questionList = [];
1172
        while ($row = Database::fetch_object($result)) {
1173
            $questionList[] = $row->question_id;
1174
        }
1175
1176
        return $questionList;
1177
    }
1178
1179
    /**
1180
     * returns 'true' if the question ID is in the question list.
1181
     *
1182
     * @param int $questionId - question ID
1183
     *
1184
     * @return bool - true if in the list, otherwise false
1185
     *
1186
     * @author Olivier Brouckaert
1187
     */
1188
    public function isInList($questionId)
1189
    {
1190
        $inList = false;
1191
        if (is_array($this->questionList)) {
1192
            $inList = in_array($questionId, $this->questionList);
1193
        }
1194
1195
        return $inList;
1196
    }
1197
1198
    /**
1199
     * If current exercise has a question.
1200
     *
1201
     * @param int $questionId
1202
     *
1203
     * @return int
1204
     */
1205
    public function hasQuestion($questionId)
1206
    {
1207
        $questionId = (int) $questionId;
1208
1209
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1210
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1211
        $sql = "SELECT q.iid
1212
                FROM $TBL_EXERCICE_QUESTION e
1213
                INNER JOIN $TBL_QUESTIONS q
1214
                ON (e.question_id = q.iid)
1215
                WHERE
1216
                    q.iid = $questionId AND
1217
                    e.quiz_id = ".$this->getId();
1218
1219
        $result = Database::query($sql);
1220
1221
        return Database::num_rows($result) > 0;
1222
    }
1223
1224
    public function hasQuestionWithType($type)
1225
    {
1226
        $type = (int) $type;
1227
1228
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1229
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
1230
        $sql = "SELECT q.iid
1231
                FROM $table e
1232
                INNER JOIN $tableQuestion q
1233
                ON (e.question_id = q.iid)
1234
                WHERE
1235
                    q.type = $type AND
1236
                    e.quiz_id = ".$this->getId();
1237
1238
        $result = Database::query($sql);
1239
1240
        return Database::num_rows($result) > 0;
1241
    }
1242
1243
    public function hasQuestionWithTypeNotInList(array $questionTypeList)
1244
    {
1245
        if (empty($questionTypeList)) {
1246
            return false;
1247
        }
1248
1249
        $questionTypeToString = implode("','", array_map('intval', $questionTypeList));
1250
1251
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1252
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
1253
        $sql = "SELECT q.iid
1254
                FROM $table e
1255
                INNER JOIN $tableQuestion q
1256
                ON (e.question_id = q.iid)
1257
                WHERE
1258
                    q.type NOT IN ('$questionTypeToString')  AND
1259
1260
                    e.quiz_id = ".$this->getId();
1261
1262
        $result = Database::query($sql);
1263
1264
        return Database::num_rows($result) > 0;
1265
    }
1266
1267
    /**
1268
     * changes the exercise title.
1269
     *
1270
     * @param string $title - exercise title
1271
     *
1272
     * @author Olivier Brouckaert
1273
     */
1274
    public function updateTitle($title)
1275
    {
1276
        $this->title = $this->exercise = $title;
1277
    }
1278
1279
    /**
1280
     * changes the exercise max attempts.
1281
     *
1282
     * @param int $attempts - exercise max attempts
1283
     */
1284
    public function updateAttempts($attempts)
1285
    {
1286
        $this->attempts = $attempts;
1287
    }
1288
1289
    /**
1290
     * changes the exercise feedback type.
1291
     *
1292
     * @param int $feedback_type
1293
     */
1294
    public function updateFeedbackType($feedback_type)
1295
    {
1296
        $this->feedback_type = $feedback_type;
1297
    }
1298
1299
    /**
1300
     * changes the exercise description.
1301
     *
1302
     * @param string $description - exercise description
1303
     *
1304
     * @author Olivier Brouckaert
1305
     */
1306
    public function updateDescription($description)
1307
    {
1308
        $this->description = $description;
1309
    }
1310
1311
    /**
1312
     * changes the exercise expired_time.
1313
     *
1314
     * @param int $expired_time The expired time of the quiz
1315
     *
1316
     * @author Isaac flores
1317
     */
1318
    public function updateExpiredTime($expired_time)
1319
    {
1320
        $this->expired_time = $expired_time;
1321
    }
1322
1323
    /**
1324
     * @param $value
1325
     */
1326
    public function updatePropagateNegative($value)
1327
    {
1328
        $this->propagate_neg = $value;
1329
    }
1330
1331
    /**
1332
     * @param int $value
1333
     */
1334
    public function updateSaveCorrectAnswers($value)
1335
    {
1336
        $this->saveCorrectAnswers = (int) $value;
1337
    }
1338
1339
    /**
1340
     * @param $value
1341
     */
1342
    public function updateReviewAnswers($value)
1343
    {
1344
        $this->review_answers = isset($value) && $value ? true : false;
1345
    }
1346
1347
    /**
1348
     * @param $value
1349
     */
1350
    public function updatePassPercentage($value)
1351
    {
1352
        $this->pass_percentage = $value;
1353
    }
1354
1355
    /**
1356
     * @param string $text
1357
     */
1358
    public function updateEmailNotificationTemplate($text)
1359
    {
1360
        $this->emailNotificationTemplate = $text;
1361
    }
1362
1363
    /**
1364
     * @param string $text
1365
     */
1366
    public function setEmailNotificationTemplateToUser($text)
1367
    {
1368
        $this->emailNotificationTemplateToUser = $text;
1369
    }
1370
1371
    /**
1372
     * @param string $value
1373
     */
1374
    public function setNotifyUserByEmail($value)
1375
    {
1376
        $this->notifyUserByEmail = $value;
1377
    }
1378
1379
    /**
1380
     * @param int $value
1381
     */
1382
    public function updateEndButton($value)
1383
    {
1384
        $this->endButton = (int) $value;
1385
    }
1386
1387
    /**
1388
     * @param string $value
1389
     */
1390
    public function setOnSuccessMessage($value)
1391
    {
1392
        $this->onSuccessMessage = $value;
1393
    }
1394
1395
    /**
1396
     * @param string $value
1397
     */
1398
    public function setOnFailedMessage($value)
1399
    {
1400
        $this->onFailedMessage = $value;
1401
    }
1402
1403
    /**
1404
     * @param $value
1405
     */
1406
    public function setModelType($value)
1407
    {
1408
        $this->modelType = (int) $value;
1409
    }
1410
1411
    /**
1412
     * @param int $value
1413
     */
1414
    public function setQuestionSelectionType($value)
1415
    {
1416
        $this->questionSelectionType = (int) $value;
1417
    }
1418
1419
    /**
1420
     * @return int
1421
     */
1422
    public function getQuestionSelectionType()
1423
    {
1424
        return (int) $this->questionSelectionType;
1425
    }
1426
1427
    /**
1428
     * @param array $categories
1429
     */
1430
    public function updateCategories($categories)
1431
    {
1432
        if (!empty($categories)) {
1433
            $categories = array_map('intval', $categories);
1434
            $this->categories = $categories;
1435
        }
1436
    }
1437
1438
    /**
1439
     * changes the exercise sound file.
1440
     *
1441
     * @param string $sound  - exercise sound file
1442
     * @param string $delete - ask to delete the file
1443
     *
1444
     * @author Olivier Brouckaert
1445
     */
1446
    public function updateSound($sound, $delete)
1447
    {
1448
        global $audioPath, $documentPath;
1449
        $TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
1450
1451
        if ($sound['size'] &&
1452
            (strstr($sound['type'], 'audio') || strstr($sound['type'], 'video'))
1453
        ) {
1454
            $this->sound = $sound['name'];
1455
1456
            if (@move_uploaded_file($sound['tmp_name'], $audioPath.'/'.$this->sound)) {
1457
                $sql = "SELECT 1 FROM $TBL_DOCUMENT
1458
                        WHERE
1459
                            c_id = ".$this->course_id." AND
1460
                            path = '".str_replace($documentPath, '', $audioPath).'/'.$this->sound."'";
1461
                $result = Database::query($sql);
1462
1463
                if (!Database::num_rows($result)) {
1464
                    DocumentManager::addDocument(
1465
                        $this->course,
1466
                        str_replace($documentPath, '', $audioPath).'/'.$this->sound,
1467
                        'file',
1468
                        $sound['size'],
1469
                        $sound['name']
1470
                    );
1471
                }
1472
            }
1473
        } elseif ($delete && is_file($audioPath.'/'.$this->sound)) {
1474
            $this->sound = '';
1475
        }
1476
    }
1477
1478
    /**
1479
     * changes the exercise type.
1480
     *
1481
     * @param int $type - exercise type
1482
     *
1483
     * @author Olivier Brouckaert
1484
     */
1485
    public function updateType($type)
1486
    {
1487
        $this->type = $type;
1488
    }
1489
1490
    /**
1491
     * sets to 0 if questions are not selected randomly
1492
     * if questions are selected randomly, sets the draws.
1493
     *
1494
     * @param int $random - 0 if not random, otherwise the draws
1495
     *
1496
     * @author Olivier Brouckaert
1497
     */
1498
    public function setRandom($random)
1499
    {
1500
        $this->random = $random;
1501
    }
1502
1503
    /**
1504
     * sets to 0 if answers are not selected randomly
1505
     * if answers are selected randomly.
1506
     *
1507
     * @param int $random_answers - random answers
1508
     *
1509
     * @author Juan Carlos Rana
1510
     */
1511
    public function updateRandomAnswers($random_answers)
1512
    {
1513
        $this->random_answers = $random_answers;
1514
    }
1515
1516
    /**
1517
     * enables the exercise.
1518
     *
1519
     * @author Olivier Brouckaert
1520
     */
1521
    public function enable()
1522
    {
1523
        $this->active = 1;
1524
    }
1525
1526
    /**
1527
     * disables the exercise.
1528
     *
1529
     * @author Olivier Brouckaert
1530
     */
1531
    public function disable()
1532
    {
1533
        $this->active = 0;
1534
    }
1535
1536
    /**
1537
     * Set disable results.
1538
     */
1539
    public function disable_results()
1540
    {
1541
        $this->results_disabled = true;
1542
    }
1543
1544
    /**
1545
     * Enable results.
1546
     */
1547
    public function enable_results()
1548
    {
1549
        $this->results_disabled = false;
1550
    }
1551
1552
    /**
1553
     * @param int $results_disabled
1554
     */
1555
    public function updateResultsDisabled($results_disabled)
1556
    {
1557
        $this->results_disabled = (int) $results_disabled;
1558
    }
1559
1560
    /**
1561
     * updates the exercise in the data base.
1562
     *
1563
     * @author Olivier Brouckaert
1564
     */
1565
    public function save()
1566
    {
1567
        $id = $this->getId();
1568
        $title = $this->exercise;
1569
        $description = $this->description;
1570
        $sound = $this->sound;
1571
        $type = $this->type;
1572
        $attempts = isset($this->attempts) ? (int) $this->attempts : 0;
1573
        $feedback_type = isset($this->feedback_type) ? (int) $this->feedback_type : 0;
1574
        $random = $this->random;
1575
        $random_answers = $this->random_answers;
1576
        $active = $this->active;
1577
        $propagate_neg = (int) $this->propagate_neg;
1578
        $saveCorrectAnswers = isset($this->saveCorrectAnswers) ? (int) $this->saveCorrectAnswers : 0;
1579
        $review_answers = isset($this->review_answers) && $this->review_answers ? 1 : 0;
1580
        $randomByCat = (int) $this->randomByCat;
1581
        $text_when_finished = $this->text_when_finished;
1582
        $text_when_finished_failure = $this->text_when_finished_failure;
1583
        $display_category_name = (int) $this->display_category_name;
1584
        $pass_percentage = (int) $this->pass_percentage;
1585
1586
        // If direct we do not show results
1587
        $results_disabled = (int) $this->results_disabled;
1588
        if (in_array($feedback_type, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1589
            $results_disabled = 0;
1590
        }
1591
        $expired_time = (int) $this->expired_time;
1592
1593
        $repo = Container::getQuizRepository();
1594
        $repoCategory = Container::getExerciseCategoryRepository();
1595
1596
        // we prepare date in the database using the api_get_utc_datetime() function
1597
        $start_time = null;
1598
        if (!empty($this->start_time)) {
1599
            $start_time = $this->start_time;
1600
        }
1601
1602
        $end_time = null;
1603
        if (!empty($this->end_time)) {
1604
            $end_time = $this->end_time;
1605
        }
1606
1607
        // Exercise already exists
1608
        if ($id) {
1609
            /** @var CQuiz $exercise */
1610
            $exercise = $repo->find($id);
1611
        } else {
1612
            $exercise = new CQuiz();
1613
        }
1614
1615
        $exercise
1616
            ->setStartTime($start_time)
1617
            ->setEndTime($end_time)
1618
            ->setTitle($title)
1619
            ->setDescription($description)
1620
            ->setSound($sound)
1621
            ->setType($type)
1622
            ->setRandom((int) $random)
1623
            ->setRandomAnswers((bool) $random_answers)
1624
            ->setActive((int) $active)
1625
            ->setResultsDisabled($results_disabled)
1626
            ->setMaxAttempt($attempts)
1627
            ->setFeedbackType($feedback_type)
1628
            ->setExpiredTime($expired_time)
1629
            ->setReviewAnswers($review_answers)
1630
            ->setRandomByCategory($randomByCat)
1631
            ->setTextWhenFinished($text_when_finished)
1632
            ->setTextWhenFinishedFailure($text_when_finished_failure)
1633
            ->setDisplayCategoryName($display_category_name)
1634
            ->setPassPercentage($pass_percentage)
1635
            ->setSaveCorrectAnswers($saveCorrectAnswers)
1636
            ->setPropagateNeg($propagate_neg)
1637
            ->setHideQuestionTitle(1 === (int) $this->getHideQuestionTitle())
1638
            ->setQuestionSelectionType($this->getQuestionSelectionType())
1639
            ->setHideQuestionNumber((int) $this->hideQuestionNumber)
1640
        ;
1641
1642
        $allow = ('true' === api_get_setting('exercise.allow_exercise_categories'));
1643
        if (true === $allow && !empty($this->getExerciseCategoryId())) {
1644
            $exercise->setExerciseCategory($repoCategory->find($this->getExerciseCategoryId()));
1645
        }
1646
1647
        $exercise->setPreventBackwards($this->getPreventBackwards());
1648
1649
        $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
1650
        if (true === $allow) {
1651
            $exercise->setShowPreviousButton($this->showPreviousButton());
1652
        }
1653
1654
        $allow = ('true' === api_get_setting('exercise.allow_notification_setting_per_exercise'));
1655
        if (true === $allow) {
1656
            $notifications = $this->getNotifications();
1657
            if (!empty($notifications)) {
1658
                $notifications = implode(',', $notifications);
1659
                $exercise->setNotifications($notifications);
1660
            }
1661
        }
1662
1663
        if (!empty($this->pageResultConfiguration)) {
1664
            $exercise->setPageResultConfiguration($this->pageResultConfiguration);
1665
        }
1666
1667
        $em = Database::getManager();
1668
1669
        if ($id) {
1670
            $repo->updateNodeForResource($exercise);
1671
1672
            if ('true' === api_get_setting('search_enabled')) {
1673
                $this->search_engine_edit();
1674
            }
1675
            $em->persist($exercise);
1676
            $em->flush();
1677
        } else {
1678
            // Creates a new exercise
1679
            $courseEntity = api_get_course_entity($this->course_id);
1680
            $exercise
1681
                ->setParent($courseEntity)
1682
                ->addCourseLink($courseEntity, api_get_session_entity());
1683
            $em->persist($exercise);
1684
            $em->flush();
1685
            $id = $exercise->getIid();
1686
            $this->iId = $this->id = $id;
1687
            if ($id) {
1688
                if ('true' === api_get_setting('search_enabled') && extension_loaded('xapian')) {
1689
                    $this->search_engine_save();
1690
                }
1691
            }
1692
        }
1693
1694
        $this->saveCategoriesInExercise($this->categories);
1695
1696
        return $id;
1697
    }
1698
1699
    /**
1700
     * Updates question position.
1701
     *
1702
     * @return bool
1703
     */
1704
    public function update_question_positions()
1705
    {
1706
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1707
        // Fixes #3483 when updating order
1708
        $questionList = $this->selectQuestionList(true);
1709
1710
        if (empty($this->getId())) {
1711
            return false;
1712
        }
1713
1714
        if (!empty($questionList)) {
1715
            foreach ($questionList as $position => $questionId) {
1716
                $position = (int) $position;
1717
                $questionId = (int) $questionId;
1718
                $sql = "UPDATE $table SET
1719
                            question_order = $position
1720
                        WHERE
1721
                            question_id = $questionId AND
1722
                            quiz_id= ".$this->getId();
1723
                Database::query($sql);
1724
            }
1725
        }
1726
1727
        return true;
1728
    }
1729
1730
    /**
1731
     * Adds a question into the question list.
1732
     *
1733
     * @param int $questionId - question ID
1734
     *
1735
     * @return bool - true if the question has been added, otherwise false
1736
     *
1737
     * @author Olivier Brouckaert
1738
     */
1739
    public function addToList($questionId)
1740
    {
1741
        // checks if the question ID is not in the list
1742
        if (!$this->isInList($questionId)) {
1743
            // selects the max position
1744
            if (!$this->selectNbrQuestions()) {
1745
                $pos = 1;
1746
            } else {
1747
                if (is_array($this->questionList)) {
1748
                    $pos = max(array_keys($this->questionList)) + 1;
1749
                }
1750
            }
1751
            $this->questionList[$pos] = $questionId;
1752
1753
            return true;
1754
        }
1755
1756
        return false;
1757
    }
1758
1759
    /**
1760
     * removes a question from the question list.
1761
     *
1762
     * @param int $questionId - question ID
1763
     *
1764
     * @return bool - true if the question has been removed, otherwise false
1765
     *
1766
     * @author Olivier Brouckaert
1767
     */
1768
    public function removeFromList($questionId)
1769
    {
1770
        // searches the position of the question ID in the list
1771
        $pos = array_search($questionId, $this->questionList);
1772
        // question not found
1773
        if (false === $pos) {
1774
            return false;
1775
        } else {
1776
            // dont reduce the number of random question if we use random by category option, or if
1777
            // random all questions
1778
            if ($this->isRandom() && 0 == $this->isRandomByCat()) {
1779
                if (count($this->questionList) >= $this->random && $this->random > 0) {
1780
                    $this->random--;
1781
                    $this->save();
1782
                }
1783
            }
1784
            // deletes the position from the array containing the wanted question ID
1785
            unset($this->questionList[$pos]);
1786
1787
            return true;
1788
        }
1789
    }
1790
1791
    /**
1792
     * deletes the exercise from the database
1793
     * Notice : leaves the question in the data base.
1794
     *
1795
     * @author Olivier Brouckaert
1796
     */
1797
    public function delete()
1798
    {
1799
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
1800
1801
        if ($limitTeacherAccess && !api_is_platform_admin()) {
1802
            return false;
1803
        }
1804
1805
        $exerciseId = $this->iId;
1806
1807
        $repo = Container::getQuizRepository();
1808
        $exercise = $repo->find($exerciseId);
1809
1810
        if (null === $exercise) {
1811
            return false;
1812
        }
1813
1814
        $locked = api_resource_is_locked_by_gradebook(
1815
            $exerciseId,
1816
            LINK_EXERCISE
1817
        );
1818
1819
        if ($locked) {
1820
            return false;
1821
        }
1822
1823
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
1824
        $sql = "UPDATE $table SET active='-1'
1825
                WHERE iid = $exerciseId";
1826
        Database::query($sql);
1827
1828
        $repo->softDelete($exercise);
1829
1830
        SkillModel::deleteSkillsFromItem($exerciseId, ITEM_TYPE_EXERCISE);
1831
1832
        if ('true' === api_get_setting('search_enabled') &&
1833
            extension_loaded('xapian')
1834
        ) {
1835
            $this->search_engine_delete();
1836
        }
1837
1838
        $linkInfo = GradebookUtils::isResourceInCourseGradebook(
1839
            $this->course['code'],
1840
            LINK_EXERCISE,
1841
            $exerciseId,
1842
            $this->sessionId
1843
        );
1844
        if (false !== $linkInfo) {
1845
            GradebookUtils::remove_resource_from_course_gradebook($linkInfo['id']);
1846
        }
1847
1848
        return true;
1849
    }
1850
1851
    /**
1852
     * Creates the form to create / edit an exercise.
1853
     *
1854
     * @param FormValidator $form
1855
     * @param string|array        $type
1856
     */
1857
    public function createForm($form, $type = 'full')
1858
    {
1859
        if (empty($type)) {
1860
            $type = 'full';
1861
        }
1862
1863
        // Form title
1864
        $form_title = get_lang('Create a new test');
1865
        if (!empty($_GET['id'])) {
1866
            $form_title = get_lang('Edit test name and settings');
1867
        }
1868
1869
        $form->addHeader($form_title);
1870
1871
        // Title.
1872
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
1873
            $form->addHtmlEditor(
1874
                'exerciseTitle',
1875
                get_lang('Test name'),
1876
                false,
1877
                false,
1878
                ['ToolbarSet' => 'TitleAsHtml']
1879
            );
1880
        } else {
1881
            $form->addElement(
1882
                'text',
1883
                'exerciseTitle',
1884
                get_lang('Test name'),
1885
                ['id' => 'exercise_title']
1886
            );
1887
        }
1888
1889
        $form->addElement('advanced_settings', 'advanced_params', get_lang('Advanced settings'));
1890
        $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
1891
1892
        if ('true' === api_get_setting('exercise.allow_exercise_categories')) {
1893
            $categoryManager = new ExerciseCategoryManager();
1894
            $categories = $categoryManager->getCategories(api_get_course_int_id());
1895
            $options = [];
1896
            if (!empty($categories)) {
1897
                /** @var CExerciseCategory $category */
1898
                foreach ($categories as $category) {
1899
                    $options[$category->getId()] = $category->getTitle();
1900
                }
1901
            }
1902
1903
            $form->addSelect(
1904
                'exercise_category_id',
1905
                get_lang('Category'),
1906
                $options,
1907
                ['placeholder' => get_lang('Please select an option')]
1908
            );
1909
        }
1910
1911
        $editor_config = [
1912
            'ToolbarSet' => 'TestQuestionDescription',
1913
            'Width' => '100%',
1914
            'Height' => '150',
1915
        ];
1916
1917
        if (is_array($type)) {
1918
            $editor_config = array_merge($editor_config, $type);
1919
        }
1920
1921
        $form->addHtmlEditor(
1922
            'exerciseDescription',
1923
            get_lang('Give a context to the test'),
1924
            false,
1925
            false,
1926
            $editor_config
1927
        );
1928
1929
        $skillList = [];
1930
        if ('full' === $type) {
1931
            // Can't modify a DirectFeedback question.
1932
            if (!in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1933
                $this->setResultFeedbackGroup($form);
1934
1935
                // Type of results display on the final page
1936
                $this->setResultDisabledGroup($form);
1937
1938
                // Type of questions disposition on page
1939
                $radios = [];
1940
                $radios[] = $form->createElement(
1941
                    'radio',
1942
                    'exerciseType',
1943
                    null,
1944
                    get_lang('All questions on one page'),
1945
                    '1',
1946
                    [
1947
                        'onclick' => 'check_per_page_all()',
1948
                        'id' => 'option_page_all',
1949
                    ]
1950
                );
1951
                $radios[] = $form->createElement(
1952
                    'radio',
1953
                    'exerciseType',
1954
                    null,
1955
                    get_lang('One question by page'),
1956
                    '2',
1957
                    [
1958
                        'onclick' => 'check_per_page_one()',
1959
                        'id' => 'option_page_one',
1960
                    ]
1961
                );
1962
1963
                $form->addGroup($radios, null, get_lang('Questions per page'));
1964
            } else {
1965
                // if is Direct feedback but has not questions we can allow to modify the question type
1966
                if (empty($this->iId) || 0 === $this->getQuestionCount()) {
1967
                    $this->setResultFeedbackGroup($form);
1968
                    $this->setResultDisabledGroup($form);
1969
1970
                    // Type of questions disposition on page
1971
                    $radios = [];
1972
                    $radios[] = $form->createElement(
1973
                        'radio',
1974
                        'exerciseType',
1975
                        null,
1976
                        get_lang('All questions on one page'),
1977
                        '1'
1978
                    );
1979
                    $radios[] = $form->createElement(
1980
                        'radio',
1981
                        'exerciseType',
1982
                        null,
1983
                        get_lang('One question by page'),
1984
                        '2'
1985
                    );
1986
                    $form->addGroup($radios, null, get_lang('Sequential'));
1987
                } else {
1988
                    $this->setResultFeedbackGroup($form, true);
1989
                    $group = $this->setResultDisabledGroup($form);
1990
                    $group->freeze();
1991
1992
                    // we force the options to the DirectFeedback exercisetype
1993
                    //$form->addElement('hidden', 'exerciseFeedbackType', $this->getFeedbackType());
1994
                    //$form->addElement('hidden', 'exerciseType', ONE_PER_PAGE);
1995
1996
                    // Type of questions disposition on page
1997
                    $radios[] = $form->createElement(
1998
                        'radio',
1999
                        'exerciseType',
2000
                        null,
2001
                        get_lang('All questions on one page'),
2002
                        '1',
2003
                        [
2004
                            'onclick' => 'check_per_page_all()',
2005
                            'id' => 'option_page_all',
2006
                        ]
2007
                    );
2008
                    $radios[] = $form->createElement(
2009
                        'radio',
2010
                        'exerciseType',
2011
                        null,
2012
                        get_lang('One question by page'),
2013
                        '2',
2014
                        [
2015
                            'onclick' => 'check_per_page_one()',
2016
                            'id' => 'option_page_one',
2017
                        ]
2018
                    );
2019
2020
                    $type_group = $form->addGroup($radios, null, get_lang('Questions per page'));
2021
                    $type_group->freeze();
2022
                }
2023
            }
2024
2025
            $option = [
2026
                EX_Q_SELECTION_ORDERED => get_lang('Ordered by user'),
2027
                //  Defined by user
2028
                EX_Q_SELECTION_RANDOM => get_lang('Random'),
2029
                // 1-10, All
2030
                'per_categories' => '--------'.get_lang('Using categories').'----------',
2031
                // Base (A 123 {3} B 456 {3} C 789{2} D 0{0}) --> Matrix {3, 3, 2, 0}
2032
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED => get_lang(
2033
                    'Ordered categories alphabetically with questions ordered'
2034
                ),
2035
                // A 123 B 456 C 78 (0, 1, all)
2036
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED => get_lang(
2037
                    'Random categories with questions ordered'
2038
                ),
2039
                // C 78 B 456 A 123
2040
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM => get_lang(
2041
                    'Ordered categories alphabetically with random questions'
2042
                ),
2043
                // A 321 B 654 C 87
2044
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM => get_lang(
2045
                    'Random categories with random questions'
2046
                ),
2047
                // C 87 B 654 A 321
2048
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED => get_lang('RandomCategoriesWithQuestionsOrderedNoQuestionGrouped'),
2049
                /*    B 456 C 78 A 123
2050
                        456 78 123
2051
                        123 456 78
2052
                */
2053
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED => get_lang('RandomCategoriesWithRandomQuestionsNoQuestionGrouped'),
2054
                /*
2055
                    A 123 B 456 C 78
2056
                    B 456 C 78 A 123
2057
                    B 654 C 87 A 321
2058
                    654 87 321
2059
                    165 842 73
2060
                */
2061
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED => get_lang('OrderedCategoriesByParentWithQuestionsOrdered'),
2062
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM => get_lang('OrderedCategoriesByParentWithQuestionsRandom'),
2063
            ];
2064
2065
            $form->addSelect(
2066
                'question_selection_type',
2067
                [get_lang('Question selection type')],
2068
                $option,
2069
                [
2070
                    'id' => 'questionSelection',
2071
                    'onchange' => 'checkQuestionSelection()',
2072
                ]
2073
            );
2074
2075
            $group = [
2076
                $form->createElement(
2077
                    'checkbox',
2078
                    'hide_expected_answer',
2079
                    null,
2080
                    get_lang('Hide expected answers column')
2081
                ),
2082
                $form->createElement(
2083
                    'checkbox',
2084
                    'hide_total_score',
2085
                    null,
2086
                    get_lang('Hide total score')
2087
                ),
2088
                $form->createElement(
2089
                    'checkbox',
2090
                    'hide_question_score',
2091
                    null,
2092
                    get_lang('Hide question score')
2093
                ),
2094
                $form->createElement(
2095
                    'checkbox',
2096
                    'hide_category_table',
2097
                    null,
2098
                    get_lang('Hide category table')
2099
                ),
2100
                $form->createElement(
2101
                    'checkbox',
2102
                    'hide_correct_answered_questions',
2103
                    null,
2104
                    get_lang('Hide correct answered questions')
2105
                ),
2106
            ];
2107
            $form->addGroup($group, null, get_lang('Results and feedback page configuration'));
2108
2109
            $group = [
2110
                $form->createElement('radio', 'hide_question_number', null, get_lang('Yes'), '1'),
2111
                $form->createElement('radio', 'hide_question_number', null, get_lang('No'), '0'),
2112
            ];
2113
            $form->addGroup($group, null, get_lang('Hide question numbering'));
2114
2115
            $displayMatrix = 'none';
2116
            $displayRandom = 'none';
2117
            $selectionType = $this->getQuestionSelectionType();
2118
            switch ($selectionType) {
2119
                case EX_Q_SELECTION_RANDOM:
2120
                    $displayRandom = 'block';
2121
2122
                    break;
2123
                case $selectionType >= EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED:
2124
                    $displayMatrix = 'block';
2125
2126
                    break;
2127
            }
2128
2129
            $form->addHtml('<div id="hidden_random" style="display:'.$displayRandom.'">');
2130
            // Number of random question.
2131
            $max = $this->getId() > 0 ? $this->getQuestionCount() : 10;
2132
            $option = range(0, $max);
2133
            $option[0] = get_lang('No');
2134
            $option[-1] = get_lang('All');
2135
            $form->addSelect(
2136
                'randomQuestions',
2137
                [
2138
                    get_lang('Random questions'),
2139
                    get_lang('Random questionsHelp'),
2140
                ],
2141
                $option,
2142
                ['id' => 'randomQuestions']
2143
            );
2144
            $form->addHtml('</div>');
2145
            $form->addHtml('<div id="hidden_matrix" style="display:'.$displayMatrix.'">');
2146
2147
            // Category selection.
2148
            $cat = new TestCategory();
2149
            $cat_form = $cat->returnCategoryForm($this);
2150
            if (empty($cat_form)) {
2151
                $cat_form = '<span class="label label-warning">'.get_lang('No categories defined').'</span>';
2152
            }
2153
            $form->addElement('label', null, $cat_form);
2154
            $form->addHtml('</div>');
2155
2156
            // Random answers.
2157
            $radios_random_answers = [
2158
                $form->createElement('radio', 'randomAnswers', null, get_lang('Yes'), '1'),
2159
                $form->createElement('radio', 'randomAnswers', null, get_lang('No'), '0'),
2160
            ];
2161
            $form->addGroup($radios_random_answers, null, get_lang('Shuffle answers'));
2162
2163
            // Category name.
2164
            $radio_display_cat_name = [
2165
                $form->createElement('radio', 'display_category_name', null, get_lang('Yes'), '1'),
2166
                $form->createElement('radio', 'display_category_name', null, get_lang('No'), '0'),
2167
            ];
2168
            $form->addGroup($radio_display_cat_name, null, get_lang('Display questions category'));
2169
2170
            // Hide question title.
2171
            $group = [
2172
                $form->createElement('radio', 'hide_question_title', null, get_lang('Yes'), '1'),
2173
                $form->createElement('radio', 'hide_question_title', null, get_lang('No'), '0'),
2174
            ];
2175
            $form->addGroup($group, null, get_lang('Hide question title'));
2176
2177
            $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
2178
2179
            if (true === $allow) {
2180
                // Hide question title.
2181
                $group = [
2182
                    $form->createElement(
2183
                        'radio',
2184
                        'show_previous_button',
2185
                        null,
2186
                        get_lang('Yes'),
2187
                        '1'
2188
                    ),
2189
                    $form->createElement(
2190
                        'radio',
2191
                        'show_previous_button',
2192
                        null,
2193
                        get_lang('No'),
2194
                        '0'
2195
                    ),
2196
                ];
2197
                $form->addGroup($group, null, get_lang('Show previous button'));
2198
            }
2199
2200
            $form->addElement(
2201
                'number',
2202
                'exerciseAttempts',
2203
                get_lang('max. 20 characters, e.g. <i>INNOV21</i> number of attempts'),
2204
                null,
2205
                ['id' => 'exerciseAttempts']
2206
            );
2207
2208
            // Exercise time limit
2209
            $form->addElement(
2210
                'checkbox',
2211
                'activate_start_date_check',
2212
                null,
2213
                get_lang('Enable start time'),
2214
                ['onclick' => 'activate_start_date()']
2215
            );
2216
2217
            if (!empty($this->start_time)) {
2218
                $form->addElement('html', '<div id="start_date_div" style="display:block;">');
2219
            } else {
2220
                $form->addElement('html', '<div id="start_date_div" style="display:none;">');
2221
            }
2222
2223
            $form->addElement('date_time_picker', 'start_time');
2224
            $form->addElement('html', '</div>');
2225
            $form->addElement(
2226
                'checkbox',
2227
                'activate_end_date_check',
2228
                null,
2229
                get_lang('Enable end time'),
2230
                ['onclick' => 'activate_end_date()']
2231
            );
2232
2233
            if (!empty($this->end_time)) {
2234
                $form->addHtml('<div id="end_date_div" style="display:block;">');
2235
            } else {
2236
                $form->addHtml('<div id="end_date_div" style="display:none;">');
2237
            }
2238
2239
            $form->addElement('date_time_picker', 'end_time');
2240
            $form->addElement('html', '</div>');
2241
2242
            $display = 'block';
2243
            $form->addElement(
2244
                'checkbox',
2245
                'propagate_neg',
2246
                null,
2247
                get_lang('Propagate negative results between questions')
2248
            );
2249
2250
            $options = [
2251
                '' => get_lang('Please select an option'),
2252
                1 => get_lang('Save the correct answer for the next attempt'),
2253
                2 => get_lang('Pre-fill with answers from previous attempt'),
2254
            ];
2255
            $form->addSelect(
2256
                'save_correct_answers',
2257
                get_lang('Save answers'),
2258
                $options
2259
            );
2260
2261
            $form->addElement('html', '<div class="clear">&nbsp;</div>');
2262
            $form->addCheckBox('review_answers', null, get_lang('Review my answers'));
2263
            $form->addElement('html', '<div id="divtimecontrol"  style="display:'.$display.';">');
2264
2265
            // Timer control
2266
            $form->addElement(
2267
                'checkbox',
2268
                'enabletimercontrol',
2269
                null,
2270
                get_lang('Enable time control'),
2271
                [
2272
                    'onclick' => 'option_time_expired()',
2273
                    'id' => 'enabletimercontrol',
2274
                    'onload' => 'check_load_time()',
2275
                ]
2276
            );
2277
2278
            $expired_date = (int) $this->selectExpiredTime();
2279
2280
            if (('0' != $expired_date)) {
2281
                $form->addElement('html', '<div id="timercontrol" style="display:block;">');
2282
            } else {
2283
                $form->addElement('html', '<div id="timercontrol" style="display:none;">');
2284
            }
2285
            $form->addText(
2286
                'enabletimercontroltotalminutes',
2287
                get_lang('Total duration in minutes of the test'),
2288
                false,
2289
                [
2290
                    'id' => 'enabletimercontroltotalminutes',
2291
                    'cols-size' => [2, 2, 8],
2292
                ]
2293
            );
2294
            $form->addElement('html', '</div>');
2295
            $form->addCheckBox('prevent_backwards', null, get_lang('Prevent moving backwards between questions'));
2296
            $form->addElement(
2297
                'text',
2298
                'pass_percentage',
2299
                [get_lang('Pass percentage'), null, '%'],
2300
                ['id' => 'pass_percentage']
2301
            );
2302
2303
            $form->addRule('pass_percentage', get_lang('Numeric'), 'numeric');
2304
            $form->addRule('pass_percentage', get_lang('Value is too small.'), 'min_numeric_length', 0);
2305
            $form->addRule('pass_percentage', get_lang('Value is too big.'), 'max_numeric_length', 100);
2306
2307
            // add the text_when_finished textbox
2308
            $form->addHtmlEditor(
2309
                'text_when_finished',
2310
                get_lang('Text appearing at the end of the test when the user has succeeded or if no pass percentage was set.'),
2311
                false,
2312
                false,
2313
                $editor_config
2314
            );
2315
            $form->addHtmlEditor(
2316
                'text_when_finished_failure',
2317
                get_lang('Text appearing at the end of the test when the user has failed.'),
2318
                false,
2319
                false,
2320
                $editor_config
2321
            );
2322
2323
            $allow = ('true' === api_get_setting('exercise.allow_notification_setting_per_exercise'));
2324
            if (true === $allow) {
2325
                $settings = ExerciseLib::getNotificationSettings();
2326
                $group = [];
2327
                foreach ($settings as $itemId => $label) {
2328
                    $group[] = $form->createElement(
2329
                        'checkbox',
2330
                        'notifications[]',
2331
                        null,
2332
                        $label,
2333
                        ['value' => $itemId]
2334
                    );
2335
                }
2336
                $form->addGroup($group, '', [get_lang('E-mail notifications')]);
2337
            }
2338
2339
            $form->addCheckBox('update_title_in_lps', null, get_lang('Update this title in learning paths'));
2340
2341
            $defaults = [];
2342
            if ('true' === api_get_setting('search_enabled')) {
2343
                $form->addCheckBox('index_document', '', get_lang('Index document text?'));
2344
                $form->addSelectLanguage('language', get_lang('Document language for indexation'));
2345
                $specific_fields = get_specific_field_list();
2346
2347
                foreach ($specific_fields as $specific_field) {
2348
                    $form->addElement('text', $specific_field['code'], $specific_field['name']);
2349
                    $filter = [
2350
                        'c_id' => api_get_course_int_id(),
2351
                        'field_id' => $specific_field['id'],
2352
                        'ref_id' => $this->getId(),
2353
                        'tool_id' => "'".TOOL_QUIZ."'",
2354
                    ];
2355
                    $values = get_specific_field_values_list($filter, ['value']);
2356
                    if (!empty($values)) {
2357
                        $arr_str_values = [];
2358
                        foreach ($values as $value) {
2359
                            $arr_str_values[] = $value['value'];
2360
                        }
2361
                        $defaults[$specific_field['code']] = implode(', ', $arr_str_values);
2362
                    }
2363
                }
2364
            }
2365
2366
            $skillList = SkillModel::addSkillsToForm($form, ITEM_TYPE_EXERCISE, $this->iId);
2367
2368
            $extraField = new ExtraField('exercise');
2369
            $extraField->addElements(
2370
                $form,
2371
                $this->iId,
2372
                ['notifications'], //exclude
2373
                false, // filter
2374
                false, // tag as select
2375
                [], //show only fields
2376
                [], // order fields
2377
                [] // extra data
2378
            );
2379
            $settings = api_get_configuration_value('exercise_finished_notification_settings');
2380
            if (!empty($settings)) {
2381
                $options = [];
2382
                foreach ($settings as $name => $data) {
2383
                    $options[$name] = $name;
2384
                }
2385
                $form->addSelect(
2386
                    'extra_notifications',
2387
                    get_lang('Notifications'),
2388
                    $options,
2389
                    ['placeholder' => get_lang('SelectAnOption')]
2390
                );
2391
            }
2392
            $form->addElement('html', '</div>'); //End advanced setting
2393
            $form->addElement('html', '</div>');
2394
        }
2395
2396
        // submit
2397
        if (isset($_GET['id'])) {
2398
            $form->addButtonSave(get_lang('Edit test name and settings'), 'submitExercise');
2399
        } else {
2400
            $form->addButtonUpdate(get_lang('Proceed to questions'), 'submitExercise');
2401
        }
2402
2403
        $form->addRule('exerciseTitle', get_lang('Name'), 'required');
2404
2405
        // defaults
2406
        if ('full' == $type) {
2407
            // rules
2408
            $form->addRule('exerciseAttempts', get_lang('Numeric'), 'numeric');
2409
            $form->addRule('start_time', get_lang('Invalid date'), 'datetime');
2410
            $form->addRule('end_time', get_lang('Invalid date'), 'datetime');
2411
2412
            if ($this->getId() > 0) {
2413
                $defaults['randomQuestions'] = $this->random;
2414
                $defaults['randomAnswers'] = $this->getRandomAnswers();
2415
                $defaults['exerciseType'] = $this->selectType();
2416
                $defaults['exerciseTitle'] = $this->get_formated_title();
2417
                $defaults['exerciseDescription'] = $this->selectDescription();
2418
                $defaults['exerciseAttempts'] = $this->selectAttempts();
2419
                $defaults['exerciseFeedbackType'] = $this->getFeedbackType();
2420
                $defaults['results_disabled'] = $this->selectResultsDisabled();
2421
                $defaults['propagate_neg'] = $this->selectPropagateNeg();
2422
                $defaults['save_correct_answers'] = $this->getSaveCorrectAnswers();
2423
                $defaults['review_answers'] = $this->review_answers;
2424
                $defaults['randomByCat'] = $this->getRandomByCategory();
2425
                $defaults['text_when_finished'] = $this->getTextWhenFinished();
2426
                $defaults['text_when_finished_failure'] = $this->getTextWhenFinishedFailure();
2427
                $defaults['display_category_name'] = $this->selectDisplayCategoryName();
2428
                $defaults['pass_percentage'] = $this->selectPassPercentage();
2429
                $defaults['question_selection_type'] = $this->getQuestionSelectionType();
2430
                $defaults['hide_question_title'] = $this->getHideQuestionTitle();
2431
                $defaults['show_previous_button'] = $this->showPreviousButton();
2432
                $defaults['exercise_category_id'] = $this->getExerciseCategoryId();
2433
                $defaults['prevent_backwards'] = $this->getPreventBackwards();
2434
                $defaults['hide_question_number'] = $this->getHideQuestionNumber();
2435
2436
                if (!empty($this->start_time)) {
2437
                    $defaults['activate_start_date_check'] = 1;
2438
                }
2439
                if (!empty($this->end_time)) {
2440
                    $defaults['activate_end_date_check'] = 1;
2441
                }
2442
2443
                $defaults['start_time'] = !empty($this->start_time) ? api_get_local_time($this->start_time) : date(
2444
                    'Y-m-d 12:00:00'
2445
                );
2446
                $defaults['end_time'] = !empty($this->end_time) ? api_get_local_time($this->end_time) : date(
2447
                    'Y-m-d 12:00:00',
2448
                    time() + 84600
2449
                );
2450
2451
                // Get expired time
2452
                if ('0' != $this->expired_time) {
2453
                    $defaults['enabletimercontrol'] = 1;
2454
                    $defaults['enabletimercontroltotalminutes'] = $this->expired_time;
2455
                } else {
2456
                    $defaults['enabletimercontroltotalminutes'] = 0;
2457
                }
2458
                $defaults['skills'] = array_keys($skillList);
2459
                $defaults['notifications'] = $this->getNotifications();
2460
            } else {
2461
                $defaults['exerciseType'] = 2;
2462
                $defaults['exerciseAttempts'] = 0;
2463
                $defaults['randomQuestions'] = 0;
2464
                $defaults['randomAnswers'] = 0;
2465
                $defaults['exerciseDescription'] = '';
2466
                $defaults['exerciseFeedbackType'] = 0;
2467
                $defaults['results_disabled'] = 0;
2468
                $defaults['randomByCat'] = 0;
2469
                $defaults['text_when_finished'] = '';
2470
                $defaults['text_when_finished_failure'] = '';
2471
                $defaults['start_time'] = date('Y-m-d 12:00:00');
2472
                $defaults['display_category_name'] = 1;
2473
                $defaults['end_time'] = date('Y-m-d 12:00:00', time() + 84600);
2474
                $defaults['pass_percentage'] = '';
2475
                $defaults['end_button'] = $this->selectEndButton();
2476
                $defaults['question_selection_type'] = 1;
2477
                $defaults['hide_question_title'] = 0;
2478
                $defaults['show_previous_button'] = 1;
2479
                $defaults['on_success_message'] = null;
2480
                $defaults['on_failed_message'] = null;
2481
            }
2482
        } else {
2483
            $defaults['exerciseTitle'] = $this->selectTitle();
2484
            $defaults['exerciseDescription'] = $this->selectDescription();
2485
        }
2486
2487
        if ('true' === api_get_setting('search_enabled')) {
2488
            $defaults['index_document'] = 'checked="checked"';
2489
        }
2490
2491
        $this->setPageResultConfigurationDefaults($defaults);
2492
        $form->setDefaults($defaults);
2493
2494
        // Freeze some elements.
2495
        if (0 != $this->getId() && false == $this->edit_exercise_in_lp) {
2496
            $elementsToFreeze = [
2497
                'randomQuestions',
2498
                //'randomByCat',
2499
                'exerciseAttempts',
2500
                'propagate_neg',
2501
                'enabletimercontrol',
2502
                'review_answers',
2503
            ];
2504
2505
            foreach ($elementsToFreeze as $elementName) {
2506
                /** @var HTML_QuickForm_element $element */
2507
                $element = $form->getElement($elementName);
2508
                $element->freeze();
2509
            }
2510
        }
2511
    }
2512
2513
    public function setResultFeedbackGroup(FormValidator $form, $checkFreeze = true)
2514
    {
2515
        // Feedback type.
2516
        $feedback = [];
2517
        $warning = sprintf(
2518
            get_lang('TheSettingXWillChangeToX'),
2519
            get_lang('ShowResultsToStudents'),
2520
            get_lang('ShowScoreAndRightAnswer')
2521
        );
2522
        $endTest = $form->createElement(
2523
            'radio',
2524
            'exerciseFeedbackType',
2525
            null,
2526
            get_lang('At end of test'),
2527
            EXERCISE_FEEDBACK_TYPE_END,
2528
            [
2529
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_END,
2530
                //'onclick' => 'if confirm() check_feedback()',
2531
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_feedback(); } else { return false;} ',
2532
            ]
2533
        );
2534
2535
        $noFeedBack = $form->createElement(
2536
            'radio',
2537
            'exerciseFeedbackType',
2538
            null,
2539
            get_lang('Exam (no feedback)'),
2540
            EXERCISE_FEEDBACK_TYPE_EXAM,
2541
            [
2542
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_EXAM,
2543
            ]
2544
        );
2545
2546
        $feedback[] = $endTest;
2547
        $feedback[] = $noFeedBack;
2548
2549
        $scenarioEnabled = 'true' === api_get_setting('enable_quiz_scenario');
2550
        $freeze = true;
2551
        if ($scenarioEnabled) {
2552
            if ($this->getQuestionCount() > 0) {
2553
                $hasDifferentQuestion = $this->hasQuestionWithTypeNotInList([UNIQUE_ANSWER, HOT_SPOT_DELINEATION]);
2554
                if (false === $hasDifferentQuestion) {
2555
                    $freeze = false;
2556
                }
2557
            } else {
2558
                $freeze = false;
2559
            }
2560
            // Can't convert a question from one feedback to another
2561
            $direct = $form->createElement(
2562
                'radio',
2563
                'exerciseFeedbackType',
2564
                null,
2565
                get_lang('Adaptative test with immediate feedback'),
2566
                EXERCISE_FEEDBACK_TYPE_DIRECT,
2567
                [
2568
                    'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_DIRECT,
2569
                    'onclick' => 'check_direct_feedback()',
2570
                ]
2571
            );
2572
2573
            $directPopUp = $form->createElement(
2574
                'radio',
2575
                'exerciseFeedbackType',
2576
                null,
2577
                get_lang('Direct pop-up mode'),
2578
                EXERCISE_FEEDBACK_TYPE_POPUP,
2579
                ['id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_POPUP, 'onclick' => 'check_direct_feedback()']
2580
            );
2581
            if ($freeze) {
2582
                $direct->freeze();
2583
                $directPopUp->freeze();
2584
            }
2585
2586
            // If has delineation freeze all.
2587
            $hasDelineation = $this->hasQuestionWithType(HOT_SPOT_DELINEATION);
2588
            if ($hasDelineation) {
2589
                $endTest->freeze();
2590
                $noFeedBack->freeze();
2591
                $direct->freeze();
2592
                $directPopUp->freeze();
2593
            }
2594
2595
            $feedback[] = $direct;
2596
            $feedback[] = $directPopUp;
2597
        }
2598
2599
        $form->addGroup(
2600
            $feedback,
2601
            null,
2602
            [
2603
                get_lang('Feedback'),
2604
                get_lang(
2605
                    '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.'
2606
                ),
2607
            ]
2608
        );
2609
    }
2610
2611
    /**
2612
     * function which process the creation of exercises.
2613
     *
2614
     * @param FormValidator $form
2615
     *
2616
     * @return int c_quiz.iid
2617
     */
2618
    public function processCreation($form)
2619
    {
2620
        $this->updateTitle(self::format_title_variable($form->getSubmitValue('exerciseTitle')));
2621
        $this->updateDescription($form->getSubmitValue('exerciseDescription'));
2622
        $this->updateAttempts($form->getSubmitValue('exerciseAttempts'));
2623
        $this->updateFeedbackType($form->getSubmitValue('exerciseFeedbackType'));
2624
        $this->updateType($form->getSubmitValue('exerciseType'));
2625
2626
        // If direct feedback then force to One per page
2627
        if (EXERCISE_FEEDBACK_TYPE_DIRECT == $form->getSubmitValue('exerciseFeedbackType')) {
2628
            $this->updateType(ONE_PER_PAGE);
2629
        }
2630
2631
        $this->setRandom($form->getSubmitValue('randomQuestions'));
2632
        $this->updateRandomAnswers($form->getSubmitValue('randomAnswers'));
2633
        $this->updateResultsDisabled($form->getSubmitValue('results_disabled'));
2634
        $this->updateExpiredTime($form->getSubmitValue('enabletimercontroltotalminutes'));
2635
        $this->updatePropagateNegative($form->getSubmitValue('propagate_neg'));
2636
        $this->updateSaveCorrectAnswers($form->getSubmitValue('save_correct_answers'));
2637
        $this->updateRandomByCat($form->getSubmitValue('randomByCat'));
2638
        $this->setTextWhenFinished($form->getSubmitValue('text_when_finished'));
2639
        $this->setTextWhenFinishedFailure($form->getSubmitValue('text_when_finished_failure'));
2640
        $this->updateDisplayCategoryName($form->getSubmitValue('display_category_name'));
2641
        $this->updateReviewAnswers($form->getSubmitValue('review_answers'));
2642
        $this->updatePassPercentage($form->getSubmitValue('pass_percentage'));
2643
        $this->updateCategories($form->getSubmitValue('category'));
2644
        $this->updateEndButton($form->getSubmitValue('end_button'));
2645
        $this->setOnSuccessMessage($form->getSubmitValue('on_success_message'));
2646
        $this->setOnFailedMessage($form->getSubmitValue('on_failed_message'));
2647
        $this->updateEmailNotificationTemplate($form->getSubmitValue('email_notification_template'));
2648
        $this->setEmailNotificationTemplateToUser($form->getSubmitValue('email_notification_template_to_user'));
2649
        $this->setNotifyUserByEmail($form->getSubmitValue('notify_user_by_email'));
2650
        $this->setModelType($form->getSubmitValue('model_type'));
2651
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2652
        $this->setHideQuestionTitle($form->getSubmitValue('hide_question_title'));
2653
        $this->sessionId = api_get_session_id();
2654
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2655
        $this->setScoreTypeModel($form->getSubmitValue('score_type_model'));
2656
        $this->setGlobalCategoryId($form->getSubmitValue('global_category_id'));
2657
        $this->setShowPreviousButton($form->getSubmitValue('show_previous_button'));
2658
        $this->setNotifications($form->getSubmitValue('notifications'));
2659
        $this->setExerciseCategoryId($form->getSubmitValue('exercise_category_id'));
2660
        $this->setPageResultConfiguration($form->getSubmitValues());
2661
        $this->setHideQuestionNumber($form->getSubmitValue('hide_question_number'));
2662
        $this->preventBackwards = (int) $form->getSubmitValue('prevent_backwards');
2663
2664
        $this->start_time = null;
2665
        if (1 == $form->getSubmitValue('activate_start_date_check')) {
2666
            $start_time = $form->getSubmitValue('start_time');
2667
            $this->start_time = api_get_utc_datetime($start_time);
2668
        }
2669
2670
        $this->end_time = null;
2671
        if (1 == $form->getSubmitValue('activate_end_date_check')) {
2672
            $end_time = $form->getSubmitValue('end_time');
2673
            $this->end_time = api_get_utc_datetime($end_time);
2674
        }
2675
2676
        $this->expired_time = 0;
2677
        if (1 == $form->getSubmitValue('enabletimercontrol')) {
2678
            $expired_total_time = $form->getSubmitValue('enabletimercontroltotalminutes');
2679
            if (0 == $this->expired_time) {
2680
                $this->expired_time = $expired_total_time;
2681
            }
2682
        }
2683
2684
        $this->random_answers = 0;
2685
        if (1 == $form->getSubmitValue('randomAnswers')) {
2686
            $this->random_answers = 1;
2687
        }
2688
2689
        // Update title in all LPs that have this quiz added
2690
        if (1 == $form->getSubmitValue('update_title_in_lps')) {
2691
            $table = Database::get_course_table(TABLE_LP_ITEM);
2692
            $sql = "SELECT iid FROM $table
2693
                    WHERE
2694
                        item_type = 'quiz' AND
2695
                        path = '".$this->getId()."'
2696
                    ";
2697
            $result = Database::query($sql);
2698
            $items = Database::store_result($result);
2699
            if (!empty($items)) {
2700
                foreach ($items as $item) {
2701
                    $itemId = $item['iid'];
2702
                    $sql = "UPDATE $table
2703
                            SET title = '".$this->title."'
2704
                            WHERE iid = $itemId ";
2705
                    Database::query($sql);
2706
                }
2707
            }
2708
        }
2709
2710
        $iId = $this->save();
2711
        if (!empty($iId)) {
2712
            $values = $form->getSubmitValues();
2713
            $values['item_id'] = $iId;
2714
            $extraFieldValue = new ExtraFieldValue('exercise');
2715
            $extraFieldValue->saveFieldValues($values);
2716
2717
            SkillModel::saveSkills($form, ITEM_TYPE_EXERCISE, $iId);
2718
        }
2719
    }
2720
2721
    public function search_engine_save()
2722
    {
2723
        if (1 != $_POST['index_document']) {
2724
            return;
2725
        }
2726
        $course_id = api_get_course_id();
2727
        $specific_fields = get_specific_field_list();
2728
        $ic_slide = new IndexableChunk();
2729
2730
        $all_specific_terms = '';
2731
        foreach ($specific_fields as $specific_field) {
2732
            if (isset($_REQUEST[$specific_field['code']])) {
2733
                $sterms = trim($_REQUEST[$specific_field['code']]);
2734
                if (!empty($sterms)) {
2735
                    $all_specific_terms .= ' '.$sterms;
2736
                    $sterms = explode(',', $sterms);
2737
                    foreach ($sterms as $sterm) {
2738
                        $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2739
                        add_specific_field_value($specific_field['id'], $course_id, TOOL_QUIZ, $this->getId(), $sterm);
2740
                    }
2741
                }
2742
            }
2743
        }
2744
2745
        // build the chunk to index
2746
        $ic_slide->addValue('title', $this->exercise);
2747
        $ic_slide->addCourseId($course_id);
2748
        $ic_slide->addToolId(TOOL_QUIZ);
2749
        $xapian_data = [
2750
            SE_COURSE_ID => $course_id,
2751
            SE_TOOL_ID => TOOL_QUIZ,
2752
            SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->getId()],
2753
            SE_USER => (int) api_get_user_id(),
2754
        ];
2755
        $ic_slide->xapian_data = serialize($xapian_data);
2756
        $exercise_description = $all_specific_terms.' '.$this->description;
2757
        $ic_slide->addValue('content', $exercise_description);
2758
2759
        $di = new ChamiloIndexer();
2760
        isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
2761
        $di->connectDb(null, null, $lang);
2762
        $di->addChunk($ic_slide);
2763
2764
        //index and return search engine document id
2765
        $did = $di->index();
2766
        if ($did) {
2767
            // save it to db
2768
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2769
            $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2770
			    VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2771
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId(), $did);
2772
            Database::query($sql);
2773
        }
2774
    }
2775
2776
    public function search_engine_edit()
2777
    {
2778
        // update search enchine and its values table if enabled
2779
        if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
2780
            $course_id = api_get_course_id();
2781
2782
            // actually, it consists on delete terms from db,
2783
            // insert new ones, create a new search engine document, and remove the old one
2784
            // get search_did
2785
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2786
            $sql = 'SELECT * FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s LIMIT 1';
2787
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2788
            $res = Database::query($sql);
2789
2790
            if (Database::num_rows($res) > 0) {
2791
                $se_ref = Database::fetch_array($res);
2792
                $specific_fields = get_specific_field_list();
2793
                $ic_slide = new IndexableChunk();
2794
2795
                $all_specific_terms = '';
2796
                foreach ($specific_fields as $specific_field) {
2797
                    delete_all_specific_field_value($course_id, $specific_field['id'], TOOL_QUIZ, $this->getId());
2798
                    if (isset($_REQUEST[$specific_field['code']])) {
2799
                        $sterms = trim($_REQUEST[$specific_field['code']]);
2800
                        $all_specific_terms .= ' '.$sterms;
2801
                        $sterms = explode(',', $sterms);
2802
                        foreach ($sterms as $sterm) {
2803
                            $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2804
                            add_specific_field_value(
2805
                                $specific_field['id'],
2806
                                $course_id,
2807
                                TOOL_QUIZ,
2808
                                $this->getId(),
2809
                                $sterm
2810
                            );
2811
                        }
2812
                    }
2813
                }
2814
2815
                // build the chunk to index
2816
                $ic_slide->addValue('title', $this->exercise);
2817
                $ic_slide->addCourseId($course_id);
2818
                $ic_slide->addToolId(TOOL_QUIZ);
2819
                $xapian_data = [
2820
                    SE_COURSE_ID => $course_id,
2821
                    SE_TOOL_ID => TOOL_QUIZ,
2822
                    SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->getId()],
2823
                    SE_USER => (int) api_get_user_id(),
2824
                ];
2825
                $ic_slide->xapian_data = serialize($xapian_data);
2826
                $exercise_description = $all_specific_terms.' '.$this->description;
2827
                $ic_slide->addValue('content', $exercise_description);
2828
2829
                $di = new ChamiloIndexer();
2830
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
2831
                $di->connectDb(null, null, $lang);
2832
                $di->remove_document($se_ref['search_did']);
2833
                $di->addChunk($ic_slide);
2834
2835
                //index and return search engine document id
2836
                $did = $di->index();
2837
                if ($did) {
2838
                    // save it to db
2839
                    $sql = 'DELETE FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=\'%s\'';
2840
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2841
                    Database::query($sql);
2842
                    $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2843
                        VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2844
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId(), $did);
2845
                    Database::query($sql);
2846
                }
2847
            } else {
2848
                $this->search_engine_save();
2849
            }
2850
        }
2851
    }
2852
2853
    public function search_engine_delete()
2854
    {
2855
        // remove from search engine if enabled
2856
        if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
2857
            $course_id = api_get_course_id();
2858
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2859
            $sql = 'SELECT * FROM %s
2860
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
2861
                    LIMIT 1';
2862
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2863
            $res = Database::query($sql);
2864
            if (Database::num_rows($res) > 0) {
2865
                $row = Database::fetch_array($res);
2866
                $di = new ChamiloIndexer();
2867
                $di->remove_document($row['search_did']);
2868
                unset($di);
2869
                $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2870
                foreach ($this->questionList as $question_i) {
2871
                    $sql = 'SELECT type FROM %s WHERE id=%s';
2872
                    $sql = sprintf($sql, $tbl_quiz_question, $question_i);
2873
                    $qres = Database::query($sql);
2874
                    if (Database::num_rows($qres) > 0) {
2875
                        $qrow = Database::fetch_array($qres);
2876
                        $objQuestion = Question::getInstance($qrow['type']);
2877
                        $objQuestion = Question::read((int) $question_i);
2878
                        $objQuestion->search_engine_edit($this->getId(), false, true);
2879
                        unset($objQuestion);
2880
                    }
2881
                }
2882
            }
2883
            $sql = 'DELETE FROM %s
2884
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
2885
                    LIMIT 1';
2886
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2887
            Database::query($sql);
2888
2889
            // remove terms from db
2890
            delete_all_values_for_item($course_id, TOOL_QUIZ, $this->getId());
2891
        }
2892
    }
2893
2894
    public function selectExpiredTime()
2895
    {
2896
        return $this->expired_time;
2897
    }
2898
2899
    /**
2900
     * Cleans the student's results only for the Exercise tool (Not from the LP)
2901
     * The LP results are NOT deleted by default, otherwise put $cleanLpTests = true
2902
     * Works with exercises in sessions.
2903
     *
2904
     * @param bool   $cleanLpTests
2905
     * @param string $cleanResultBeforeDate
2906
     *
2907
     * @return int quantity of user's exercises deleted
2908
     */
2909
    public function cleanResults($cleanLpTests = false, $cleanResultBeforeDate = null)
2910
    {
2911
        $sessionId = api_get_session_id();
2912
        $table_track_e_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2913
        $table_track_e_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
2914
2915
        $sql_where = '  AND
2916
                        orig_lp_id = 0 AND
2917
                        orig_lp_item_id = 0';
2918
2919
        // if we want to delete results from LP too
2920
        if ($cleanLpTests) {
2921
            $sql_where = '';
2922
        }
2923
2924
        // if we want to delete attempts before date $cleanResultBeforeDate
2925
        // $cleanResultBeforeDate must be a valid UTC-0 date yyyy-mm-dd
2926
        if (!empty($cleanResultBeforeDate)) {
2927
            $cleanResultBeforeDate = Database::escape_string($cleanResultBeforeDate);
2928
            if (api_is_valid_date($cleanResultBeforeDate)) {
2929
                $sql_where .= "  AND exe_date <= '$cleanResultBeforeDate' ";
2930
            } else {
2931
                return 0;
2932
            }
2933
        }
2934
2935
        $sessionCondition = api_get_session_condition($sessionId);
2936
        $sql = "SELECT exe_id
2937
                FROM $table_track_e_exercises
2938
                WHERE
2939
                    c_id = ".api_get_course_int_id().' AND
2940
                    exe_exo_id = '.$this->getId()."
2941
                    $sessionCondition
2942
                    $sql_where";
2943
2944
        $result = Database::query($sql);
2945
        $exe_list = Database::store_result($result);
2946
2947
        // deleting TRACK_E_ATTEMPT table
2948
        // check if exe in learning path or not
2949
        $i = 0;
2950
        if (is_array($exe_list) && count($exe_list) > 0) {
2951
            foreach ($exe_list as $item) {
2952
                $sql = "DELETE FROM $table_track_e_attempt
2953
                        WHERE exe_id = '".$item['exe_id']."'";
2954
                Database::query($sql);
2955
                $i++;
2956
            }
2957
        }
2958
2959
        // delete TRACK_E_EXERCISES table
2960
        $sql = "DELETE FROM $table_track_e_exercises
2961
                WHERE
2962
                  c_id = ".api_get_course_int_id().' AND
2963
                  exe_exo_id = '.$this->getId()." $sql_where $sessionCondition";
2964
        Database::query($sql);
2965
2966
        $this->generateStats($this->getId(), api_get_course_info(), $sessionId);
2967
2968
        Event::addEvent(
2969
            LOG_EXERCISE_RESULT_DELETE,
2970
            LOG_EXERCISE_ID,
2971
            $this->getId(),
2972
            null,
2973
            null,
2974
            api_get_course_int_id(),
2975
            $sessionId
2976
        );
2977
2978
        return $i;
2979
    }
2980
2981
    /**
2982
     * Copies an exercise (duplicate all questions and answers).
2983
     */
2984
    public function copyExercise()
2985
    {
2986
        $exerciseObject = $this;
2987
        $categories = $exerciseObject->getCategoriesInExercise(true);
2988
        // Get all questions no matter the order/category settings
2989
        $questionList = $exerciseObject->getQuestionOrderedList();
2990
        $sourceId = $exerciseObject->iId;
2991
        // Force the creation of a new exercise
2992
        $exerciseObject->updateTitle($exerciseObject->selectTitle().' - '.get_lang('Copy'));
2993
        // Hides the new exercise
2994
        $exerciseObject->updateStatus(false);
2995
        $exerciseObject->iId = 0;
2996
        $exerciseObject->sessionId = api_get_session_id();
2997
        $courseId = api_get_course_int_id();
2998
        $exerciseObject->save();
2999
        $newId = $exerciseObject->getId();
3000
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
3001
3002
        $count = 1;
3003
        $batchSize = 20;
3004
        $em = Database::getManager();
3005
        if ($newId && !empty($questionList)) {
3006
            $extraField = new ExtraFieldValue('exercise');
3007
            $extraField->copy($sourceId, $newId);
3008
            // Question creation
3009
            foreach ($questionList as $oldQuestionId) {
3010
                $oldQuestionObj = Question::read($oldQuestionId, null, false);
3011
                $newQuestionId = $oldQuestionObj->duplicate();
3012
                if ($newQuestionId) {
3013
                    $newQuestionObj = Question::read($newQuestionId, null, false);
3014
                    if (isset($newQuestionObj) && $newQuestionObj) {
3015
                        $sql = "INSERT INTO $exerciseRelQuestionTable (c_id, question_id, quiz_id, question_order)
3016
                                VALUES ($courseId, ".$newQuestionId.", ".$newId.", '$count')";
3017
                        Database::query($sql);
3018
                        $count++;
3019
                        if (!empty($oldQuestionObj->category)) {
3020
                            $newQuestionObj->saveCategory($oldQuestionObj->category);
3021
                        }
3022
3023
                        // This should be moved to the duplicate function
3024
                        $newAnswerObj = new Answer($oldQuestionId, $courseId, $exerciseObject);
3025
                        $newAnswerObj->read();
3026
                        $newAnswerObj->duplicate($newQuestionObj);
3027
                        if (($count % $batchSize) === 0) {
3028
                            $em->clear(); // Detaches all objects from Doctrine!
3029
                        }
3030
                    }
3031
                }
3032
            }
3033
            if (!empty($categories)) {
3034
                $newCategoryList = [];
3035
                foreach ($categories as $category) {
3036
                    $newCategoryList[$category['category_id']] = $category['count_questions'];
3037
                }
3038
                $exerciseObject->saveCategoriesInExercise($newCategoryList);
3039
            }
3040
        }
3041
    }
3042
3043
    /**
3044
     * Changes the exercise status.
3045
     *
3046
     * @param string $status - exercise status
3047
     */
3048
    public function updateStatus($status)
3049
    {
3050
        $this->active = $status;
3051
    }
3052
3053
    /**
3054
     * @param int    $lp_id
3055
     * @param int    $lp_item_id
3056
     * @param int    $lp_item_view_id
3057
     * @param string $status
3058
     *
3059
     * @return array
3060
     */
3061
    public function get_stat_track_exercise_info(
3062
        $lp_id = 0,
3063
        $lp_item_id = 0,
3064
        $lp_item_view_id = 0,
3065
        $status = 'incomplete'
3066
    ) {
3067
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3068
        $lp_id = (int) $lp_id;
3069
        $lp_item_id = (int) $lp_item_id;
3070
        $lp_item_view_id = (int) $lp_item_view_id;
3071
3072
        $sessionCondition = api_get_session_condition(api_get_session_id());
3073
        $condition = " WHERE exe_exo_id 	= ".$this->getId()." AND
3074
					   exe_user_id 			= '".api_get_user_id()."' AND
3075
					   c_id                 = ".api_get_course_int_id()." AND
3076
					   status 				= '".Database::escape_string($status)."' AND
3077
					   orig_lp_id 			= $lp_id AND
3078
					   orig_lp_item_id 		= $lp_item_id AND
3079
                       orig_lp_item_view_id =  $lp_item_view_id
3080
					   ";
3081
3082
        $sql_track = " SELECT * FROM  $track_exercises $condition $sessionCondition LIMIT 1 ";
3083
3084
        $result = Database::query($sql_track);
3085
        $new_array = [];
3086
        if (Database::num_rows($result) > 0) {
3087
            $new_array = Database::fetch_array($result, 'ASSOC');
3088
            $new_array['num_exe'] = Database::num_rows($result);
3089
        }
3090
3091
        return $new_array;
3092
    }
3093
3094
    /**
3095
     * Saves a test attempt.
3096
     *
3097
     * @param int   $clock_expired_time   clock_expired_time
3098
     * @param int   $safe_lp_id           lp id
3099
     * @param int   $safe_lp_item_id      lp item id
3100
     * @param int   $safe_lp_item_view_id lp item_view id
3101
     * @param array $questionList
3102
     * @param float $weight
3103
     *
3104
     * @throws \Doctrine\ORM\ORMException
3105
     * @throws \Doctrine\ORM\OptimisticLockException
3106
     * @throws \Doctrine\ORM\TransactionRequiredException
3107
     *
3108
     * @return int
3109
     */
3110
    public function save_stat_track_exercise_info(
3111
        $clock_expired_time,
3112
        $safe_lp_id = 0,
3113
        $safe_lp_item_id = 0,
3114
        $safe_lp_item_view_id = 0,
3115
        $questionList = [],
3116
        $weight = 0
3117
    ) {
3118
        $safe_lp_id = (int) $safe_lp_id;
3119
        $safe_lp_item_id = (int) $safe_lp_item_id;
3120
        $safe_lp_item_view_id = (int) $safe_lp_item_view_id;
3121
3122
        if (empty($clock_expired_time)) {
3123
            $clock_expired_time = null;
3124
        }
3125
3126
        $questionList = array_map('intval', $questionList);
3127
        $em = Database::getManager();
3128
3129
        $quiz = $em->find(CQuiz::class, $this->getId());
3130
3131
        $trackExercise = (new TrackEExercise())
3132
            ->setSession(api_get_session_entity())
3133
            ->setCourse(api_get_course_entity())
3134
            ->setMaxScore($weight)
3135
            ->setDataTracking(implode(',', $questionList))
3136
            ->setUser(api_get_user_entity())
3137
            ->setUserIp(api_get_real_ip())
3138
            ->setOrigLpId($safe_lp_id)
3139
            ->setOrigLpItemId($safe_lp_item_id)
3140
            ->setOrigLpItemViewId($safe_lp_item_view_id)
3141
            ->setExpiredTimeControl($clock_expired_time)
3142
            ->setQuiz($quiz)
3143
        ;
3144
        $em->persist($trackExercise);
3145
        $em->flush();
3146
3147
        return $trackExercise->getExeId();
3148
    }
3149
3150
    /**
3151
     * @param int    $question_id
3152
     * @param int    $questionNum
3153
     * @param array  $questions_in_media
3154
     * @param string $currentAnswer
3155
     * @param array  $myRemindList
3156
     * @param bool   $showPreviousButton
3157
     *
3158
     * @return string
3159
     */
3160
    public function show_button(
3161
        $question_id,
3162
        $questionNum,
3163
        $questions_in_media = [],
3164
        $currentAnswer = '',
3165
        $myRemindList = [],
3166
        $showPreviousButton = true
3167
    ) {
3168
        global $safe_lp_id, $safe_lp_item_id, $safe_lp_item_view_id;
3169
        $nbrQuestions = $this->countQuestionsInExercise();
3170
        $buttonList = [];
3171
        $html = $label = '';
3172
        $hotspotGet = isset($_POST['hotspot']) ? Security::remove_XSS($_POST['hotspot']) : null;
3173
3174
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]) &&
3175
            ONE_PER_PAGE == $this->type
3176
        ) {
3177
            $urlTitle = get_lang('Proceed with the test');
3178
            if ($questionNum == count($this->questionList)) {
3179
                $urlTitle = get_lang('End test');
3180
            }
3181
3182
            $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit_modal.php?'.api_get_cidreq();
3183
            $url .= '&'.http_build_query(
3184
                    [
3185
                        'learnpath_id' => $safe_lp_id,
3186
                        'learnpath_item_id' => $safe_lp_item_id,
3187
                        'learnpath_item_view_id' => $safe_lp_item_view_id,
3188
                        'hotspot' => $hotspotGet,
3189
                        'nbrQuestions' => $nbrQuestions,
3190
                        'num' => $questionNum,
3191
                        'exerciseType' => $this->type,
3192
                        'exerciseId' => $this->getId(),
3193
                        'reminder' => empty($myRemindList) ? null : 2,
3194
                        'tryagain' => isset($_REQUEST['tryagain']) && 1 === (int) $_REQUEST['tryagain'] ? 1 : 0,
3195
                    ]
3196
                );
3197
3198
            $params = [
3199
                'class' => 'ajax btn btn--plain no-close-button',
3200
                'data-title' => Security::remove_XSS(get_lang('Comment')),
3201
                'data-size' => 'md',
3202
                'id' => "button_$question_id",
3203
            ];
3204
3205
            if (EXERCISE_FEEDBACK_TYPE_POPUP === $this->getFeedbackType()) {
3206
                //$params['data-block-div-after-closing'] = "question_div_$question_id";
3207
                $params['data-block-closing'] = 'true';
3208
                $params['class'] .= ' no-header ';
3209
            }
3210
3211
            $html .= Display::url($urlTitle, $url, $params);
3212
            $html .= '<br />';
3213
3214
            return $html;
3215
        }
3216
3217
        if (!api_is_allowed_to_session_edit()) {
3218
            return '';
3219
        }
3220
3221
        $isReviewingAnswers = isset($_REQUEST['reminder']) && 2 == $_REQUEST['reminder'];
3222
3223
        // User
3224
        $endReminderValue = false;
3225
        if (!empty($myRemindList) && $isReviewingAnswers) {
3226
            $endValue = end($myRemindList);
3227
            if ($endValue == $question_id) {
3228
                $endReminderValue = true;
3229
            }
3230
        }
3231
        $endTest = false;
3232
        if (ALL_ON_ONE_PAGE == $this->type || $nbrQuestions == $questionNum || $endReminderValue) {
3233
            if ($this->review_answers) {
3234
                $label = get_lang('ReviewQuestions');
3235
                $class = 'btn btn--success';
3236
            } else {
3237
                $endTest = true;
3238
                $label = get_lang('End Test');
3239
                $class = 'btn btn--warning';
3240
            }
3241
        } else {
3242
            $label = get_lang('Next question');
3243
            $class = 'btn btn--primary';
3244
        }
3245
        // used to select it with jquery
3246
        $class .= ' question-validate-btn';
3247
        if (ONE_PER_PAGE == $this->type) {
3248
            if (1 != $questionNum && $this->showPreviousButton()) {
3249
                $prev_question = $questionNum - 2;
3250
                $showPreview = true;
3251
                if (!empty($myRemindList) && $isReviewingAnswers) {
3252
                    $beforeId = null;
3253
                    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...
3254
                        if (isset($myRemindList[$i]) && $myRemindList[$i] == $question_id) {
3255
                            $beforeId = isset($myRemindList[$i - 1]) ? $myRemindList[$i - 1] : null;
3256
3257
                            break;
3258
                        }
3259
                    }
3260
3261
                    if (empty($beforeId)) {
3262
                        $showPreview = false;
3263
                    } else {
3264
                        $num = 0;
3265
                        foreach ($this->questionList as $originalQuestionId) {
3266
                            if ($originalQuestionId == $beforeId) {
3267
                                break;
3268
                            }
3269
                            $num++;
3270
                        }
3271
                        $prev_question = $num;
3272
                    }
3273
                }
3274
3275
                if ($showPreviousButton && $showPreview && 0 === $this->getPreventBackwards()) {
3276
                    $buttonList[] = Display::button(
3277
                        'previous_question_and_save',
3278
                        get_lang('Previous question'),
3279
                        [
3280
                            'type' => 'button',
3281
                            'class' => 'btn btn--plain',
3282
                            'data-prev' => $prev_question,
3283
                            'data-question' => $question_id,
3284
                        ]
3285
                    );
3286
                }
3287
            }
3288
3289
            // Next question
3290
            if (!empty($questions_in_media)) {
3291
                $buttonList[] = Display::button(
3292
                    'save_question_list',
3293
                    $label,
3294
                    [
3295
                        'type' => 'button',
3296
                        'class' => $class,
3297
                        'data-list' => implode(',', $questions_in_media),
3298
                    ]
3299
                );
3300
            } else {
3301
                $attributes = ['type' => 'button', 'class' => $class, 'data-question' => $question_id];
3302
                $name = 'save_now';
3303
                if ($endTest && api_get_configuration_value('quiz_check_all_answers_before_end_test')) {
3304
                    $name = 'check_answers';
3305
                }
3306
                $buttonList[] = Display::button(
3307
                    $name,
3308
                    $label,
3309
                    $attributes
3310
                );
3311
            }
3312
            $buttonList[] = '<span id="save_for_now_'.$question_id.'" class="exercise_save_mini_message"></span>';
3313
3314
            $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3315
3316
            return $html;
3317
        }
3318
3319
        if ($this->review_answers) {
3320
            $all_label = get_lang('Review selected questions');
3321
            $class = 'btn btn--success';
3322
        } else {
3323
            $all_label = get_lang('End test');
3324
            $class = 'btn btn--warning';
3325
        }
3326
        // used to select it with jquery
3327
        $class .= ' question-validate-btn';
3328
        $buttonList[] = Display::button(
3329
            'validate_all',
3330
            $all_label,
3331
            ['type' => 'button', 'class' => $class]
3332
        );
3333
        $buttonList[] = Display::span(null, ['id' => 'save_all_response']);
3334
        $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3335
3336
        return $html;
3337
    }
3338
3339
    /**
3340
     * @param int    $timeLeft in seconds
3341
     * @param string $url
3342
     *
3343
     * @return string
3344
     */
3345
    public function showSimpleTimeControl($timeLeft, $url = '')
3346
    {
3347
        $timeLeft = (int) $timeLeft;
3348
3349
        return "<script>
3350
            function openClockWarning() {
3351
                $('#clock_warning').dialog({
3352
                    modal:true,
3353
                    height:320,
3354
                    width:550,
3355
                    closeOnEscape: false,
3356
                    resizable: false,
3357
                    buttons: {
3358
                        '".addslashes(get_lang('Close'))."': function() {
3359
                            $('#clock_warning').dialog('close');
3360
                        }
3361
                    },
3362
                    close: function() {
3363
                        window.location.href = '$url';
3364
                    }
3365
                });
3366
                $('#clock_warning').dialog('open');
3367
                $('#counter_to_redirect').epiclock({
3368
                    mode: $.epiclock.modes.countdown,
3369
                    offset: {seconds: 5},
3370
                    format: 's'
3371
                }).bind('timer', function () {
3372
                    window.location.href = '$url';
3373
                });
3374
            }
3375
3376
            function onExpiredTimeExercise() {
3377
                $('#wrapper-clock').hide();
3378
                $('#expired-message-id').show();
3379
                // Fixes bug #5263
3380
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3381
                openClockWarning();
3382
            }
3383
3384
			$(function() {
3385
				// time in seconds when using minutes there are some seconds lost
3386
                var time_left = parseInt(".$timeLeft.");
3387
                $('#exercise_clock_warning').epiclock({
3388
                    mode: $.epiclock.modes.countdown,
3389
                    offset: {seconds: time_left},
3390
                    format: 'x:i:s',
3391
                    renderer: 'minute'
3392
                }).bind('timer', function () {
3393
                    onExpiredTimeExercise();
3394
                });
3395
	       		$('#submit_save').click(function () {});
3396
	        });
3397
	    </script>";
3398
    }
3399
3400
    /**
3401
     * So the time control will work.
3402
     *
3403
     * @param int    $timeLeft
3404
     * @param string $redirectToUrl
3405
     *
3406
     * @return string
3407
     */
3408
    public function showTimeControlJS($timeLeft, $redirectToUrl = '')
3409
    {
3410
        $timeLeft = (int) $timeLeft;
3411
        $script = 'redirectExerciseToResult();';
3412
        if (ALL_ON_ONE_PAGE == $this->type) {
3413
            $script = "save_now_all('validate');";
3414
        } elseif (ONE_PER_PAGE == $this->type) {
3415
            $script = 'window.quizTimeEnding = true;
3416
                $(\'[name="save_now"]\').trigger(\'click\');';
3417
        }
3418
3419
        $exerciseSubmitRedirect = '';
3420
        if (!empty($redirectToUrl)) {
3421
            $exerciseSubmitRedirect = "window.location = '$redirectToUrl'";
3422
        }
3423
3424
        return "<script>
3425
            function openClockWarning() {
3426
                $('#clock_warning').dialog({
3427
                    modal:true,
3428
                    height:320,
3429
                    width:550,
3430
                    closeOnEscape: false,
3431
                    resizable: false,
3432
                    buttons: {
3433
                        '".addslashes(get_lang('End test'))."': function() {
3434
                            $('#clock_warning').dialog('close');
3435
                        }
3436
                    },
3437
                    close: function() {
3438
                        send_form();
3439
                    }
3440
                });
3441
3442
                $('#clock_warning').dialog('open');
3443
                $('#counter_to_redirect').epiclock({
3444
                    mode: $.epiclock.modes.countdown,
3445
                    offset: {seconds: 5},
3446
                    format: 's'
3447
                }).bind('timer', function () {
3448
                    send_form();
3449
                });
3450
            }
3451
3452
            function send_form() {
3453
                if ($('#exercise_form').length) {
3454
                    $script
3455
                } else {
3456
                    $exerciseSubmitRedirect
3457
                    // In exercise_reminder.php
3458
                    final_submit();
3459
                }
3460
            }
3461
3462
            function onExpiredTimeExercise() {
3463
                $('#wrapper-clock').hide();
3464
                $('#expired-message-id').show();
3465
                // Fixes bug #5263
3466
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3467
                openClockWarning();
3468
            }
3469
3470
			$(function() {
3471
				// time in seconds when using minutes there are some seconds lost
3472
                var time_left = parseInt(".$timeLeft.");
3473
                $('#exercise_clock_warning').epiclock({
3474
                    mode: $.epiclock.modes.countdown,
3475
                    offset: {seconds: time_left},
3476
                    format: 'x:C:s',
3477
                    renderer: 'minute'
3478
                }).bind('timer', function () {
3479
                    onExpiredTimeExercise();
3480
                });
3481
	       		$('#submit_save').click(function () {});
3482
	        });
3483
	    </script>";
3484
    }
3485
3486
    /**
3487
     * This function was originally found in the exercise_show.php.
3488
     *
3489
     * @param int    $exeId
3490
     * @param int    $questionId
3491
     * @param mixed  $choice                                    the user-selected option
3492
     * @param string $from                                      function is called from 'exercise_show' or
3493
     *                                                          'exercise_result'
3494
     * @param array  $exerciseResultCoordinates                 the hotspot coordinates $hotspot[$question_id] =
3495
     *                                                          coordinates
3496
     * @param bool   $save_results                              save results in the DB or just show the response
3497
     * @param bool   $from_database                             gets information from DB or from the current selection
3498
     * @param bool   $show_result                               show results or not
3499
     * @param int    $propagate_neg
3500
     * @param array  $hotspot_delineation_result
3501
     * @param bool   $showTotalScoreAndUserChoicesInLastAttempt
3502
     * @param bool   $updateResults
3503
     * @param bool   $showHotSpotDelineationTable
3504
     * @param int    $questionDuration                          seconds
3505
     *
3506
     * @return string html code
3507
     *
3508
     * @todo    reduce parameters of this function
3509
     */
3510
    public function manage_answer(
3511
        $exeId,
3512
        $questionId,
3513
        $choice,
3514
        $from = 'exercise_show',
3515
        $exerciseResultCoordinates = [],
3516
        $save_results = true,
3517
        $from_database = false,
3518
        $show_result = true,
3519
        $propagate_neg = 0,
3520
        $hotspot_delineation_result = [],
3521
        $showTotalScoreAndUserChoicesInLastAttempt = true,
3522
        $updateResults = false,
3523
        $showHotSpotDelineationTable = false,
3524
        $questionDuration = 0
3525
    ) {
3526
        $debug = false;
3527
        //needed in order to use in the exercise_attempt() for the time
3528
        global $learnpath_id, $learnpath_item_id;
3529
        require_once api_get_path(LIBRARY_PATH).'geometry.lib.php';
3530
        $em = Database::getManager();
3531
        $feedback_type = $this->getFeedbackType();
3532
        $results_disabled = $this->selectResultsDisabled();
3533
        $questionDuration = (int) $questionDuration;
3534
3535
        if ($debug) {
3536
            error_log('<------ manage_answer ------> ');
3537
            error_log('exe_id: '.$exeId);
3538
            error_log('$from:  '.$from);
3539
            error_log('$save_results: '.(int) $save_results);
3540
            error_log('$from_database: '.(int) $from_database);
3541
            error_log('$show_result: '.(int) $show_result);
3542
            error_log('$propagate_neg: '.$propagate_neg);
3543
            error_log('$exerciseResultCoordinates: '.print_r($exerciseResultCoordinates, 1));
3544
            error_log('$hotspot_delineation_result: '.print_r($hotspot_delineation_result, 1));
3545
            error_log('$learnpath_id: '.$learnpath_id);
3546
            error_log('$learnpath_item_id: '.$learnpath_item_id);
3547
            error_log('$choice: '.print_r($choice, 1));
3548
            error_log('-----------------------------');
3549
        }
3550
3551
        $final_overlap = 0;
3552
        $final_missing = 0;
3553
        $final_excess = 0;
3554
        $overlap_color = 0;
3555
        $missing_color = 0;
3556
        $excess_color = 0;
3557
        $threadhold1 = 0;
3558
        $threadhold2 = 0;
3559
        $threadhold3 = 0;
3560
        $arrques = null;
3561
        $arrans = null;
3562
        $studentChoice = null;
3563
        $expectedAnswer = '';
3564
        $calculatedChoice = '';
3565
        $calculatedStatus = '';
3566
        $questionId = (int) $questionId;
3567
        $exeId = (int) $exeId;
3568
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3569
        $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
3570
        $studentChoiceDegree = null;
3571
3572
        // Creates a temporary Question object
3573
        $course_id = $this->course_id;
3574
        $objQuestionTmp = Question::read($questionId, $this->course);
3575
3576
        if (false === $objQuestionTmp) {
3577
            return false;
3578
        }
3579
3580
        $questionName = $objQuestionTmp->selectTitle();
3581
        $questionWeighting = $objQuestionTmp->selectWeighting();
3582
        $answerType = $objQuestionTmp->selectType();
3583
        $quesId = $objQuestionTmp->getId();
3584
        $extra = $objQuestionTmp->extra;
3585
        $next = 1; //not for now
3586
        $totalWeighting = 0;
3587
        $totalScore = 0;
3588
3589
        // Extra information of the question
3590
        if ((
3591
                MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
3592
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
3593
            )
3594
            && !empty($extra)
3595
        ) {
3596
            $extra = explode(':', $extra);
3597
            // Fixes problems with negatives values using intval
3598
            $true_score = (float) trim($extra[0]);
3599
            $false_score = (float) trim($extra[1]);
3600
            $doubt_score = (float) trim($extra[2]);
3601
        }
3602
3603
        // Construction of the Answer object
3604
        $objAnswerTmp = new Answer($questionId, $course_id);
3605
        $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
3606
3607
        if ($debug) {
3608
            error_log('Count of possible answers: '.$nbrAnswers);
3609
            error_log('$answerType: '.$answerType);
3610
        }
3611
3612
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
3613
            $choiceTmp = $choice;
3614
            $choice = isset($choiceTmp['choice']) ? $choiceTmp['choice'] : '';
3615
            $choiceDegreeCertainty = isset($choiceTmp['choiceDegreeCertainty']) ? $choiceTmp['choiceDegreeCertainty'] : '';
3616
        }
3617
3618
        if (FREE_ANSWER == $answerType ||
3619
            ORAL_EXPRESSION == $answerType ||
3620
            CALCULATED_ANSWER == $answerType ||
3621
            ANNOTATION == $answerType
3622
        ) {
3623
            $nbrAnswers = 1;
3624
        }
3625
3626
        $user_answer = '';
3627
        // Get answer list for matching
3628
        $sql = "SELECT iid, answer
3629
                FROM $table_ans
3630
                WHERE question_id = $questionId";
3631
        $res_answer = Database::query($sql);
3632
3633
        $answerMatching = [];
3634
        while ($real_answer = Database::fetch_array($res_answer)) {
3635
            $answerMatching[$real_answer['iid']] = $real_answer['answer'];
3636
        }
3637
3638
        // Get first answer needed for global question, no matter the answer shuffle option;
3639
        $firstAnswer = [];
3640
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
3641
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
3642
        ) {
3643
            $sql = "SELECT *
3644
                    FROM $table_ans
3645
                    WHERE question_id = $questionId
3646
                    ORDER BY position
3647
                    LIMIT 1";
3648
            $result = Database::query($sql);
3649
            if (Database::num_rows($result)) {
3650
                $firstAnswer = Database::fetch_array($result);
3651
            }
3652
        }
3653
3654
        $real_answers = [];
3655
        $quiz_question_options = Question::readQuestionOption($questionId, $course_id);
3656
3657
        $organs_at_risk_hit = 0;
3658
        $questionScore = 0;
3659
        $orderedHotSpots = [];
3660
        if (HOT_SPOT == $answerType || ANNOTATION == $answerType) {
3661
            $orderedHotSpots = $em->getRepository(TrackEHotspot::class)->findBy(
3662
                [
3663
                    'hotspotQuestionId' => $questionId,
3664
                    'course' => $course_id,
3665
                    'hotspotExeId' => $exeId,
3666
                ],
3667
                ['hotspotAnswerId' => 'ASC']
3668
            );
3669
        }
3670
3671
        if ($debug) {
3672
            error_log('-- Start answer loop --');
3673
        }
3674
3675
        $answerDestination = null;
3676
        $userAnsweredQuestion = false;
3677
        $correctAnswerId = [];
3678
        for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
3679
            $answer = $objAnswerTmp->selectAnswer($answerId);
3680
            $answerComment = $objAnswerTmp->selectComment($answerId);
3681
            $answerCorrect = $objAnswerTmp->isCorrect($answerId);
3682
            $answerWeighting = (float) $objAnswerTmp->selectWeighting($answerId);
3683
            $answerAutoId = $objAnswerTmp->selectAutoId($answerId);
3684
            $answerIid = isset($objAnswerTmp->iid[$answerId]) ? (int) $objAnswerTmp->iid[$answerId] : 0;
3685
3686
            if ($debug) {
3687
                error_log("c_quiz_answer.id_auto: $answerAutoId ");
3688
                error_log("Answer marked as correct in db (0/1)?: $answerCorrect ");
3689
                error_log("answerWeighting: $answerWeighting");
3690
            }
3691
3692
            // Delineation
3693
            $delineation_cord = $objAnswerTmp->selectHotspotCoordinates(1);
3694
            $answer_delineation_destination = $objAnswerTmp->selectDestination(1);
3695
3696
            switch ($answerType) {
3697
                case UNIQUE_ANSWER:
3698
                case UNIQUE_ANSWER_IMAGE:
3699
                case UNIQUE_ANSWER_NO_OPTION:
3700
                case READING_COMPREHENSION:
3701
                    if ($from_database) {
3702
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3703
                                WHERE
3704
                                    exe_id = $exeId AND
3705
                                    question_id = $questionId";
3706
                        $result = Database::query($sql);
3707
                        $choice = Database::result($result, 0, 'answer');
3708
3709
                        if (false === $userAnsweredQuestion) {
3710
                            $userAnsweredQuestion = !empty($choice);
3711
                        }
3712
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3713
                        if ($studentChoice) {
3714
                            $questionScore += $answerWeighting;
3715
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3716
                            $correctAnswerId[] = $answerId;
3717
                        }
3718
                    } else {
3719
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3720
                        if ($studentChoice) {
3721
                            $questionScore += $answerWeighting;
3722
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3723
                            $correctAnswerId[] = $answerId;
3724
                        }
3725
                    }
3726
3727
                    break;
3728
                case MULTIPLE_ANSWER_TRUE_FALSE:
3729
                    if ($from_database) {
3730
                        $choice = [];
3731
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3732
                                WHERE
3733
                                    exe_id = $exeId AND
3734
                                    question_id = ".$questionId;
3735
3736
                        $result = Database::query($sql);
3737
                        while ($row = Database::fetch_array($result)) {
3738
                            $values = explode(':', $row['answer']);
3739
                            $my_answer_id = isset($values[0]) ? $values[0] : '';
3740
                            $option = isset($values[1]) ? $values[1] : '';
3741
                            $choice[$my_answer_id] = $option;
3742
                        }
3743
                        $userAnsweredQuestion = !empty($choice);
3744
                    }
3745
3746
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3747
                    if (!empty($studentChoice)) {
3748
                        $correctAnswerId[] = $answerAutoId;
3749
                        if ($studentChoice == $answerCorrect) {
3750
                            $questionScore += $true_score;
3751
                        } else {
3752
                            if (isset($quiz_question_options[$studentChoice])
3753
                                && in_array($quiz_question_options[$studentChoice]['name'], ["Don't know", 'DoubtScore'])
3754
                            ) {
3755
                                $questionScore += $doubt_score;
3756
                            } else {
3757
                                $questionScore += $false_score;
3758
                            }
3759
                        }
3760
                    } else {
3761
                        // If no result then the user just hit don't know
3762
                        $studentChoice = 3;
3763
                        $questionScore += $doubt_score;
3764
                    }
3765
                    $totalScore = $questionScore;
3766
3767
                    break;
3768
                case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
3769
                    if ($from_database) {
3770
                        $choice = [];
3771
                        $choiceDegreeCertainty = [];
3772
                        $sql = "SELECT answer
3773
                                FROM $TBL_TRACK_ATTEMPT
3774
                                WHERE exe_id = $exeId AND question_id = $questionId";
3775
3776
                        $result = Database::query($sql);
3777
                        while ($row = Database::fetch_array($result)) {
3778
                            $ind = $row['answer'];
3779
                            $values = explode(':', $ind);
3780
                            $myAnswerId = $values[0] ?? null;
3781
                            $option = $values[1] ?? null;
3782
                            $percent = $values[2] ?? null;
3783
                            $choice[$myAnswerId] = $option;
3784
                            $choiceDegreeCertainty[$myAnswerId] = $percent;
3785
                        }
3786
                    }
3787
3788
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3789
                    $studentChoiceDegree = isset($choiceDegreeCertainty[$answerAutoId]) ? $choiceDegreeCertainty[$answerAutoId] : null;
3790
3791
                    // student score update
3792
                    if (!empty($studentChoice)) {
3793
                        if ($studentChoice == $answerCorrect) {
3794
                            // correct answer and student is Unsure or PrettySur
3795
                            if (isset($quiz_question_options[$studentChoiceDegree]) &&
3796
                                $quiz_question_options[$studentChoiceDegree]['position'] >= 3 &&
3797
                                $quiz_question_options[$studentChoiceDegree]['position'] < 9
3798
                            ) {
3799
                                $questionScore += $true_score;
3800
                            } else {
3801
                                // student ignore correct answer
3802
                                $questionScore += $doubt_score;
3803
                            }
3804
                        } else {
3805
                            // false answer and student is Unsure or PrettySur
3806
                            if (isset($quiz_question_options[$studentChoiceDegree]) && $quiz_question_options[$studentChoiceDegree]['position'] >= 3
3807
                                && $quiz_question_options[$studentChoiceDegree]['position'] < 9) {
3808
                                $questionScore += $false_score;
3809
                            } else {
3810
                                // student ignore correct answer
3811
                                $questionScore += $doubt_score;
3812
                            }
3813
                        }
3814
                    }
3815
                    $totalScore = $questionScore;
3816
3817
                    break;
3818
                case MULTIPLE_ANSWER:
3819
                    if ($from_database) {
3820
                        $choice = [];
3821
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3822
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3823
                        $resultans = Database::query($sql);
3824
                        while ($row = Database::fetch_array($resultans)) {
3825
                            $choice[$row['answer']] = 1;
3826
                        }
3827
3828
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3829
                        $real_answers[$answerId] = (bool) $studentChoice;
3830
3831
                        if ($studentChoice) {
3832
                            $questionScore += $answerWeighting;
3833
                        }
3834
                    } else {
3835
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3836
                        $real_answers[$answerId] = (bool) $studentChoice;
3837
3838
                        if (isset($studentChoice)) {
3839
                            $correctAnswerId[] = $answerAutoId;
3840
                            $questionScore += $answerWeighting;
3841
                        }
3842
                    }
3843
                    $totalScore += $answerWeighting;
3844
3845
                    break;
3846
                case GLOBAL_MULTIPLE_ANSWER:
3847
                    if ($from_database) {
3848
                        $choice = [];
3849
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3850
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3851
                        $resultans = Database::query($sql);
3852
                        while ($row = Database::fetch_array($resultans)) {
3853
                            $choice[$row['answer']] = 1;
3854
                        }
3855
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3856
                        $real_answers[$answerId] = (bool) $studentChoice;
3857
                        if ($studentChoice) {
3858
                            $questionScore += $answerWeighting;
3859
                        }
3860
                    } else {
3861
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3862
                        if (isset($studentChoice)) {
3863
                            $questionScore += $answerWeighting;
3864
                        }
3865
                        $real_answers[$answerId] = (bool) $studentChoice;
3866
                    }
3867
                    $totalScore += $answerWeighting;
3868
                    if ($debug) {
3869
                        error_log("studentChoice: $studentChoice");
3870
                    }
3871
3872
                    break;
3873
                case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
3874
                    if ($from_database) {
3875
                        $choice = [];
3876
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3877
                                WHERE exe_id = $exeId AND question_id = $questionId";
3878
                        $resultans = Database::query($sql);
3879
                        while ($row = Database::fetch_array($resultans)) {
3880
                            $result = explode(':', $row['answer']);
3881
                            if (isset($result[0])) {
3882
                                $my_answer_id = isset($result[0]) ? $result[0] : '';
3883
                                $option = isset($result[1]) ? $result[1] : '';
3884
                                $choice[$my_answer_id] = $option;
3885
                            }
3886
                        }
3887
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3888
3889
                        $real_answers[$answerId] = false;
3890
                        if ($answerCorrect == $studentChoice) {
3891
                            $real_answers[$answerId] = true;
3892
                        }
3893
                    } else {
3894
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3895
                        $real_answers[$answerId] = false;
3896
                        if ($answerCorrect == $studentChoice) {
3897
                            $real_answers[$answerId] = true;
3898
                        }
3899
                    }
3900
3901
                    break;
3902
                case MULTIPLE_ANSWER_COMBINATION:
3903
                    if ($from_database) {
3904
                        $choice = [];
3905
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3906
                                WHERE exe_id = $exeId AND question_id = $questionId";
3907
                        $resultans = Database::query($sql);
3908
                        while ($row = Database::fetch_array($resultans)) {
3909
                            $choice[$row['answer']] = 1;
3910
                        }
3911
3912
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3913
                        if (1 == $answerCorrect) {
3914
                            $real_answers[$answerId] = false;
3915
                            if ($studentChoice) {
3916
                                $real_answers[$answerId] = true;
3917
                            }
3918
                        } else {
3919
                            $real_answers[$answerId] = true;
3920
                            if ($studentChoice) {
3921
                                $real_answers[$answerId] = false;
3922
                            }
3923
                        }
3924
                    } else {
3925
                        $studentChoice = $choice[$answerAutoId] ?? null;
3926
                        if (1 == $answerCorrect) {
3927
                            $real_answers[$answerId] = false;
3928
                            if ($studentChoice) {
3929
                                $real_answers[$answerId] = true;
3930
                            }
3931
                        } else {
3932
                            $real_answers[$answerId] = true;
3933
                            if ($studentChoice) {
3934
                                $real_answers[$answerId] = false;
3935
                            }
3936
                        }
3937
                    }
3938
3939
                    break;
3940
                case FILL_IN_BLANKS:
3941
                    $str = '';
3942
                    $answerFromDatabase = '';
3943
                    if ($from_database) {
3944
                        $sql = "SELECT answer
3945
                                FROM $TBL_TRACK_ATTEMPT
3946
                                WHERE
3947
                                    exe_id = $exeId AND
3948
                                    question_id= $questionId ";
3949
                        $result = Database::query($sql);
3950
                        $str = $answerFromDatabase = Database::result($result, 0, 'answer');
3951
                    }
3952
3953
                    // if ($saved_results == false && strpos($answerFromDatabase, 'font color') !== false) {
3954
                    if (false) {
3955
                        // the question is encoded like this
3956
                        // [A] B [C] D [E] F::10,10,10@1
3957
                        // number 1 before the "@" means that is a switchable fill in blank question
3958
                        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
3959
                        // means that is a normal fill blank question
3960
                        // first we explode the "::"
3961
                        $pre_array = explode('::', $answer);
3962
3963
                        // is switchable fill blank or not
3964
                        $last = count($pre_array) - 1;
3965
                        $is_set_switchable = explode('@', $pre_array[$last]);
3966
                        $switchable_answer_set = false;
3967
                        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
3968
                            $switchable_answer_set = true;
3969
                        }
3970
                        $answer = '';
3971
                        for ($k = 0; $k < $last; $k++) {
3972
                            $answer .= $pre_array[$k];
3973
                        }
3974
                        // splits weightings that are joined with a comma
3975
                        $answerWeighting = explode(',', $is_set_switchable[0]);
3976
                        // we save the answer because it will be modified
3977
                        $temp = $answer;
3978
                        $answer = '';
3979
                        $j = 0;
3980
                        //initialise answer tags
3981
                        $user_tags = $correct_tags = $real_text = [];
3982
                        // the loop will stop at the end of the text
3983
                        while (1) {
3984
                            // quits the loop if there are no more blanks (detect '[')
3985
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
3986
                                // adds the end of the text
3987
                                $answer = $temp;
3988
                                $real_text[] = $answer;
3989
3990
                                break; //no more "blanks", quit the loop
3991
                            }
3992
                            // adds the piece of text that is before the blank
3993
                            //and ends with '[' into a general storage array
3994
                            $real_text[] = api_substr($temp, 0, $pos + 1);
3995
                            $answer .= api_substr($temp, 0, $pos + 1);
3996
                            //take the string remaining (after the last "[" we found)
3997
                            $temp = api_substr($temp, $pos + 1);
3998
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
3999
                            if (false === ($pos = api_strpos($temp, ']'))) {
4000
                                // adds the end of the text
4001
                                $answer .= $temp;
4002
4003
                                break;
4004
                            }
4005
                            if ($from_database) {
4006
                                $str = $answerFromDatabase;
4007
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4008
                                $str = str_replace('\r\n', '', $str);
4009
4010
                                $choice = $arr[1];
4011
                                if (isset($choice[$j])) {
4012
                                    $tmp = api_strrpos($choice[$j], ' / ');
4013
                                    $choice[$j] = api_substr($choice[$j], 0, $tmp);
4014
                                    $choice[$j] = trim($choice[$j]);
4015
                                    // Needed to let characters ' and " to work as part of an answer
4016
                                    $choice[$j] = stripslashes($choice[$j]);
4017
                                } else {
4018
                                    $choice[$j] = null;
4019
                                }
4020
                            } else {
4021
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4022
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4023
                            }
4024
4025
                            $user_tags[] = $choice[$j];
4026
                            // Put the contents of the [] answer tag into correct_tags[]
4027
                            $correct_tags[] = api_substr($temp, 0, $pos);
4028
                            $j++;
4029
                            $temp = api_substr($temp, $pos + 1);
4030
                        }
4031
                        $answer = '';
4032
                        $real_correct_tags = $correct_tags;
4033
                        $chosen_list = [];
4034
4035
                        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...
4036
                            if (0 == $i) {
4037
                                $answer .= $real_text[0];
4038
                            }
4039
                            if (!$switchable_answer_set) {
4040
                                // Needed to parse ' and " characters
4041
                                $user_tags[$i] = stripslashes($user_tags[$i]);
4042
                                if ($correct_tags[$i] == $user_tags[$i]) {
4043
                                    // gives the related weighting to the student
4044
                                    $questionScore += $answerWeighting[$i];
4045
                                    // increments total score
4046
                                    $totalScore += $answerWeighting[$i];
4047
                                    // adds the word in green at the end of the string
4048
                                    $answer .= $correct_tags[$i];
4049
                                } elseif (!empty($user_tags[$i])) {
4050
                                    // else if the word entered by the student IS NOT the same as
4051
                                    // the one defined by the professor
4052
                                    // adds the word in red at the end of the string, and strikes it
4053
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4054
                                } else {
4055
                                    // adds a tabulation if no word has been typed by the student
4056
                                    $answer .= ''; // remove &nbsp; that causes issue
4057
                                }
4058
                            } else {
4059
                                // switchable fill in the blanks
4060
                                if (in_array($user_tags[$i], $correct_tags)) {
4061
                                    $chosen_list[] = $user_tags[$i];
4062
                                    $correct_tags = array_diff($correct_tags, $chosen_list);
4063
                                    // gives the related weighting to the student
4064
                                    $questionScore += $answerWeighting[$i];
4065
                                    // increments total score
4066
                                    $totalScore += $answerWeighting[$i];
4067
                                    // adds the word in green at the end of the string
4068
                                    $answer .= $user_tags[$i];
4069
                                } elseif (!empty($user_tags[$i])) {
4070
                                    // else if the word entered by the student IS NOT the same
4071
                                    // as the one defined by the professor
4072
                                    // adds the word in red at the end of the string, and strikes it
4073
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4074
                                } else {
4075
                                    // adds a tabulation if no word has been typed by the student
4076
                                    $answer .= ''; // remove &nbsp; that causes issue
4077
                                }
4078
                            }
4079
4080
                            // adds the correct word, followed by ] to close the blank
4081
                            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4082
                            if (isset($real_text[$i + 1])) {
4083
                                $answer .= $real_text[$i + 1];
4084
                            }
4085
                        }
4086
                    } else {
4087
                        // insert the student result in the track_e_attempt table, field answer
4088
                        // $answer is the answer like in the c_quiz_answer table for the question
4089
                        // student data are choice[]
4090
                        $listCorrectAnswers = FillBlanks::getAnswerInfo($answer);
4091
                        $switchableAnswerSet = $listCorrectAnswers['switchable'];
4092
                        $answerWeighting = $listCorrectAnswers['weighting'];
4093
                        // user choices is an array $choice
4094
4095
                        // get existing user data in n the BDD
4096
                        if ($from_database) {
4097
                            $listStudentResults = FillBlanks::getAnswerInfo(
4098
                                $answerFromDatabase,
4099
                                true
4100
                            );
4101
                            $choice = $listStudentResults['student_answer'];
4102
                        }
4103
4104
                        // loop other all blanks words
4105
                        if (!$switchableAnswerSet) {
4106
                            // not switchable answer, must be in the same place than teacher order
4107
                            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...
4108
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
4109
                                $correctAnswer = $listCorrectAnswers['words'][$i];
4110
4111
                                if ($debug) {
4112
                                    error_log("Student answer: $i");
4113
                                    error_log($studentAnswer);
4114
                                }
4115
4116
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4117
                                // Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
4118
                                // ENT_QUOTES is used in order to transform ' to &#039;
4119
                                if (!$from_database) {
4120
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4121
                                    if ($debug) {
4122
                                        error_log('Student answer cleaned:');
4123
                                        error_log($studentAnswer);
4124
                                    }
4125
                                }
4126
4127
                                $isAnswerCorrect = 0;
4128
                                if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
4129
                                    // gives the related weighting to the student
4130
                                    $questionScore += $answerWeighting[$i];
4131
                                    // increments total score
4132
                                    $totalScore += $answerWeighting[$i];
4133
                                    $isAnswerCorrect = 1;
4134
                                }
4135
                                if ($debug) {
4136
                                    error_log("isAnswerCorrect $i: $isAnswerCorrect");
4137
                                }
4138
4139
                                $studentAnswerToShow = $studentAnswer;
4140
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4141
                                if ($debug) {
4142
                                    error_log("Fill in blank type: $type");
4143
                                }
4144
                                if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4145
                                    $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4146
                                    if ('' != $studentAnswer) {
4147
                                        foreach ($listMenu as $item) {
4148
                                            if (sha1($item) == $studentAnswer) {
4149
                                                $studentAnswerToShow = $item;
4150
                                            }
4151
                                        }
4152
                                    }
4153
                                }
4154
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4155
                                $listCorrectAnswers['student_score'][$i] = $isAnswerCorrect;
4156
                            }
4157
                        } else {
4158
                            // switchable answer
4159
                            $listStudentAnswerTemp = $choice;
4160
                            $listTeacherAnswerTemp = $listCorrectAnswers['words'];
4161
4162
                            // for every teacher answer, check if there is a student answer
4163
                            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...
4164
                                $studentAnswer = trim($listStudentAnswerTemp[$i]);
4165
                                $studentAnswerToShow = $studentAnswer;
4166
4167
                                if (empty($studentAnswer)) {
4168
                                    break;
4169
                                }
4170
4171
                                if ($debug) {
4172
                                    error_log("Student answer: $i");
4173
                                    error_log($studentAnswer);
4174
                                }
4175
4176
                                if (!$from_database) {
4177
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4178
                                    if ($debug) {
4179
                                        error_log("Student answer cleaned:");
4180
                                        error_log($studentAnswer);
4181
                                    }
4182
                                }
4183
4184
                                $found = false;
4185
                                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...
4186
                                    $correctAnswer = $listTeacherAnswerTemp[$j];
4187
4188
                                    if (!$found) {
4189
                                        if (FillBlanks::isStudentAnswerGood(
4190
                                            $studentAnswer,
4191
                                            $correctAnswer,
4192
                                            $from_database
4193
                                        )) {
4194
                                            $questionScore += $answerWeighting[$i];
4195
                                            $totalScore += $answerWeighting[$i];
4196
                                            $listTeacherAnswerTemp[$j] = '';
4197
                                            $found = true;
4198
                                        }
4199
                                    }
4200
4201
                                    $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4202
                                    if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4203
                                        $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4204
                                        if (!empty($studentAnswer)) {
4205
                                            foreach ($listMenu as $key => $item) {
4206
                                                if ($key == $correctAnswer) {
4207
                                                    $studentAnswerToShow = $item;
4208
                                                    break;
4209
                                                }
4210
                                            }
4211
                                        }
4212
                                    }
4213
                                }
4214
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4215
                                $listCorrectAnswers['student_score'][$i] = $found ? 1 : 0;
4216
                            }
4217
                        }
4218
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
4219
                    }
4220
4221
                    break;
4222
                case CALCULATED_ANSWER:
4223
                    $calculatedAnswerList = Session::read('calculatedAnswerId');
4224
                    if (!empty($calculatedAnswerList)) {
4225
                        $answer = $objAnswerTmp->selectAnswer($calculatedAnswerList[$questionId]);
4226
                        $preArray = explode('@@', $answer);
4227
                        $last = count($preArray) - 1;
4228
                        $answer = '';
4229
                        for ($k = 0; $k < $last; $k++) {
4230
                            $answer .= $preArray[$k];
4231
                        }
4232
                        $answerWeighting = [$answerWeighting];
4233
                        // we save the answer because it will be modified
4234
                        $temp = $answer;
4235
                        $answer = '';
4236
                        $j = 0;
4237
                        // initialise answer tags
4238
                        $userTags = $correctTags = $realText = [];
4239
                        // the loop will stop at the end of the text
4240
                        while (1) {
4241
                            // quits the loop if there are no more blanks (detect '[')
4242
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
4243
                                // adds the end of the text
4244
                                $answer = $temp;
4245
                                $realText[] = $answer;
4246
4247
                                break; //no more "blanks", quit the loop
4248
                            }
4249
                            // adds the piece of text that is before the blank
4250
                            // and ends with '[' into a general storage array
4251
                            $realText[] = api_substr($temp, 0, $pos + 1);
4252
                            $answer .= api_substr($temp, 0, $pos + 1);
4253
                            // take the string remaining (after the last "[" we found)
4254
                            $temp = api_substr($temp, $pos + 1);
4255
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4256
                            if (false === ($pos = api_strpos($temp, ']'))) {
4257
                                // adds the end of the text
4258
                                $answer .= $temp;
4259
4260
                                break;
4261
                            }
4262
4263
                            if ($from_database) {
4264
                                $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4265
                                        WHERE
4266
                                            exe_id = $exeId AND
4267
                                            question_id = $questionId ";
4268
                                $result = Database::query($sql);
4269
                                $str = Database::result($result, 0, 'answer');
4270
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4271
                                $str = str_replace('\r\n', '', $str);
4272
                                $choice = $arr[1];
4273
                                if (isset($choice[$j])) {
4274
                                    $tmp = api_strrpos($choice[$j], ' / ');
4275
                                    if ($tmp) {
4276
                                        $choice[$j] = api_substr($choice[$j], 0, $tmp);
4277
                                    } else {
4278
                                        $tmp = ltrim($tmp, '[');
4279
                                        $tmp = rtrim($tmp, ']');
4280
                                    }
4281
                                    $choice[$j] = trim($choice[$j]);
4282
                                    // Needed to let characters ' and " to work as part of an answer
4283
                                    $choice[$j] = stripslashes($choice[$j]);
4284
                                } else {
4285
                                    $choice[$j] = null;
4286
                                }
4287
                            } else {
4288
                                // This value is the user input not escaped while correct answer is escaped by ckeditor
4289
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4290
                            }
4291
                            $userTags[] = $choice[$j];
4292
                            // put the contents of the [] answer tag into correct_tags[]
4293
                            $correctTags[] = api_substr($temp, 0, $pos);
4294
                            $j++;
4295
                            $temp = api_substr($temp, $pos + 1);
4296
                        }
4297
                        $answer = '';
4298
                        $realCorrectTags = $correctTags;
4299
                        $calculatedStatus = Display::label(get_lang('Incorrect'), 'danger');
4300
                        $expectedAnswer = '';
4301
                        $calculatedChoice = '';
4302
4303
                        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...
4304
                            if (0 == $i) {
4305
                                $answer .= $realText[0];
4306
                            }
4307
                            // Needed to parse ' and " characters
4308
                            $userTags[$i] = stripslashes($userTags[$i]);
4309
                            if ($correctTags[$i] == $userTags[$i]) {
4310
                                // gives the related weighting to the student
4311
                                $questionScore += $answerWeighting[$i];
4312
                                // increments total score
4313
                                $totalScore += $answerWeighting[$i];
4314
                                // adds the word in green at the end of the string
4315
                                $answer .= $correctTags[$i];
4316
                                $calculatedChoice = $correctTags[$i];
4317
                            } elseif (!empty($userTags[$i])) {
4318
                                // else if the word entered by the student IS NOT the same as
4319
                                // the one defined by the professor
4320
                                // adds the word in red at the end of the string, and strikes it
4321
                                $answer .= '<font color="red"><s>'.$userTags[$i].'</s></font>';
4322
                                $calculatedChoice = $userTags[$i];
4323
                            } else {
4324
                                // adds a tabulation if no word has been typed by the student
4325
                                $answer .= ''; // remove &nbsp; that causes issue
4326
                            }
4327
                            // adds the correct word, followed by ] to close the blank
4328
                            if (EXERCISE_FEEDBACK_TYPE_EXAM != $this->results_disabled) {
4329
                                $answer .= ' / <font color="green"><b>'.$realCorrectTags[$i].'</b></font>';
4330
                                $calculatedStatus = Display::label(get_lang('Correct'), 'success');
4331
                                $expectedAnswer = $realCorrectTags[$i];
4332
                            }
4333
                            $answer .= ']';
4334
                            if (isset($realText[$i + 1])) {
4335
                                $answer .= $realText[$i + 1];
4336
                            }
4337
                        }
4338
                    } else {
4339
                        if ($from_database) {
4340
                            $sql = "SELECT *
4341
                                    FROM $TBL_TRACK_ATTEMPT
4342
                                    WHERE
4343
                                        exe_id = $exeId AND
4344
                                        question_id = $questionId ";
4345
                            $result = Database::query($sql);
4346
                            $resultData = Database::fetch_array($result, 'ASSOC');
4347
                            $answer = $resultData['answer'];
4348
                            $questionScore = $resultData['marks'];
4349
                        }
4350
                    }
4351
4352
                    break;
4353
                case FREE_ANSWER:
4354
                    if ($from_database) {
4355
                        $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
4356
                                 WHERE
4357
                                    exe_id = $exeId AND
4358
                                    question_id= ".$questionId;
4359
                        $result = Database::query($sql);
4360
                        $data = Database::fetch_array($result);
4361
                        $choice = '';
4362
                        $questionScore = 0;
4363
                        if ($data) {
4364
                            $choice = $data['answer'];
4365
                            $questionScore = $data['marks'];
4366
                        }
4367
4368
                        $choice = str_replace('\r\n', '', $choice);
4369
                        $choice = stripslashes($choice);
4370
4371
                        if (-1 == $questionScore) {
4372
                            $totalScore += 0;
4373
                        } else {
4374
                            $totalScore += $questionScore;
4375
                        }
4376
                        if ('' == $questionScore) {
4377
                            $questionScore = 0;
4378
                        }
4379
                        $arrques = $questionName;
4380
                        $arrans = $choice;
4381
                    } else {
4382
                        $studentChoice = $choice;
4383
                        if ($studentChoice) {
4384
                            //Fixing negative puntation see #2193
4385
                            $questionScore = 0;
4386
                            $totalScore += 0;
4387
                        }
4388
                    }
4389
4390
                    break;
4391
                case ORAL_EXPRESSION:
4392
                    if ($from_database) {
4393
                        $query = "SELECT answer, marks
4394
                                  FROM $TBL_TRACK_ATTEMPT
4395
                                  WHERE
4396
                                        exe_id = $exeId AND
4397
                                        question_id = $questionId
4398
                                 ";
4399
                        $resq = Database::query($query);
4400
                        $row = Database::fetch_assoc($resq);
4401
                        $choice = '';
4402
                        $questionScore = 0;
4403
4404
                        if (is_array($row)) {
4405
                            $choice = $row['answer'];
4406
                            $choice = str_replace('\r\n', '', $choice);
4407
                            $choice = stripslashes($choice);
4408
                            $questionScore = $row['marks'];
4409
                        }
4410
4411
                        if (-1 == $questionScore) {
4412
                            $totalScore += 0;
4413
                        } else {
4414
                            $totalScore += $questionScore;
4415
                        }
4416
                        $arrques = $questionName;
4417
                        $arrans = $choice;
4418
                    } else {
4419
                        $studentChoice = $choice;
4420
                        if ($studentChoice) {
4421
                            //Fixing negative puntation see #2193
4422
                            $questionScore = 0;
4423
                            $totalScore += 0;
4424
                        }
4425
                    }
4426
4427
                    break;
4428
                case DRAGGABLE:
4429
                case MATCHING_DRAGGABLE:
4430
                case MATCHING:
4431
                    if ($from_database) {
4432
                        $sql = "SELECT iid, answer
4433
                                FROM $table_ans
4434
                                WHERE
4435
                                    question_id = $questionId AND
4436
                                    correct = 0
4437
                                ";
4438
                        $result = Database::query($sql);
4439
                        // Getting the real answer
4440
                        $real_list = [];
4441
                        while ($realAnswer = Database::fetch_array($result)) {
4442
                            $real_list[$realAnswer['iid']] = $realAnswer['answer'];
4443
                        }
4444
4445
                        $orderBy = ' ORDER BY iid ';
4446
                        if (DRAGGABLE == $answerType) {
4447
                            $orderBy = ' ORDER BY correct ';
4448
                        }
4449
4450
                        $sql = "SELECT iid, answer, correct, ponderation
4451
                                FROM $table_ans
4452
                                WHERE
4453
                                    question_id = $questionId AND
4454
                                    correct <> 0
4455
                                $orderBy";
4456
                        $result = Database::query($sql);
4457
                        $options = [];
4458
                        $correctAnswers = [];
4459
                        while ($row = Database::fetch_array($result, 'ASSOC')) {
4460
                            $options[] = $row;
4461
                            $correctAnswers[$row['correct']] = $row['answer'];
4462
                        }
4463
4464
                        $questionScore = 0;
4465
                        $counterAnswer = 1;
4466
                        foreach ($options as $a_answers) {
4467
                            $i_answer_id = $a_answers['iid']; //3
4468
                            $s_answer_label = $a_answers['answer']; // your daddy - your mother
4469
                            $i_answer_correct_answer = $a_answers['correct']; //1 - 2
4470
                            $i_answer_id_auto = $a_answers['iid']; // 3 - 4
4471
4472
                            $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4473
                                    WHERE
4474
                                        exe_id = '$exeId' AND
4475
                                        question_id = '$questionId' AND
4476
                                        position = '$i_answer_id_auto'";
4477
                            $result = Database::query($sql);
4478
                            $s_user_answer = 0;
4479
                            if (Database::num_rows($result) > 0) {
4480
                                //  rich - good looking
4481
                                $s_user_answer = Database::result($result, 0, 0);
4482
                            }
4483
                            $i_answerWeighting = $a_answers['ponderation'];
4484
                            $user_answer = '';
4485
                            $status = Display::label(get_lang('Incorrect'), 'danger');
4486
4487
                            if (!empty($s_user_answer)) {
4488
                                if (DRAGGABLE == $answerType) {
4489
                                    if ($s_user_answer == $i_answer_correct_answer) {
4490
                                        $questionScore += $i_answerWeighting;
4491
                                        $totalScore += $i_answerWeighting;
4492
                                        $user_answer = Display::label(get_lang('Correct'), 'success');
4493
                                        if ($this->showExpectedChoice() && !empty($i_answer_id_auto)) {
4494
                                            $user_answer = $answerMatching[$i_answer_id_auto];
4495
                                        }
4496
                                        $status = Display::label(get_lang('Correct'), 'success');
4497
                                    } else {
4498
                                        $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4499
                                        if ($this->showExpectedChoice() && !empty($s_user_answer)) {
4500
                                            /*$data = $options[$real_list[$s_user_answer] - 1];
4501
                                            $user_answer = $data['answer'];*/
4502
                                            $user_answer = $correctAnswers[$s_user_answer] ?? '';
4503
                                        }
4504
                                    }
4505
                                } else {
4506
                                    if ($s_user_answer == $i_answer_correct_answer) {
4507
                                        $questionScore += $i_answerWeighting;
4508
                                        $totalScore += $i_answerWeighting;
4509
                                        $status = Display::label(get_lang('Correct'), 'success');
4510
4511
                                        // Try with id
4512
                                        if (isset($real_list[$i_answer_id])) {
4513
                                            $user_answer = Display::span(
4514
                                                $real_list[$i_answer_id],
4515
                                                ['style' => 'color: #008000; font-weight: bold;']
4516
                                            );
4517
                                        }
4518
4519
                                        // Try with $i_answer_id_auto
4520
                                        if (empty($user_answer)) {
4521
                                            if (isset($real_list[$i_answer_id_auto])) {
4522
                                                $user_answer = Display::span(
4523
                                                    $real_list[$i_answer_id_auto],
4524
                                                    ['style' => 'color: #008000; font-weight: bold;']
4525
                                                );
4526
                                            }
4527
                                        }
4528
4529
                                        if (isset($real_list[$i_answer_correct_answer])) {
4530
                                            $user_answer = Display::span(
4531
                                                $real_list[$i_answer_correct_answer],
4532
                                                ['style' => 'color: #008000; font-weight: bold;']
4533
                                            );
4534
                                        }
4535
                                    } else {
4536
                                        $user_answer = Display::span(
4537
                                            $real_list[$s_user_answer],
4538
                                            ['style' => 'color: #FF0000; text-decoration: line-through;']
4539
                                        );
4540
                                        if ($this->showExpectedChoice()) {
4541
                                            if (isset($real_list[$s_user_answer])) {
4542
                                                $user_answer = Display::span($real_list[$s_user_answer]);
4543
                                            }
4544
                                        }
4545
                                    }
4546
                                }
4547
                            } elseif (DRAGGABLE == $answerType) {
4548
                                $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4549
                                if ($this->showExpectedChoice()) {
4550
                                    $user_answer = '';
4551
                                }
4552
                            } else {
4553
                                $user_answer = Display::span(
4554
                                    get_lang('Incorrect').' &nbsp;',
4555
                                    ['style' => 'color: #FF0000; text-decoration: line-through;']
4556
                                );
4557
                                if ($this->showExpectedChoice()) {
4558
                                    $user_answer = '';
4559
                                }
4560
                            }
4561
4562
                            if ($show_result) {
4563
                                if (false === $this->showExpectedChoice() &&
4564
                                    false === $showTotalScoreAndUserChoicesInLastAttempt
4565
                                ) {
4566
                                    $user_answer = '';
4567
                                }
4568
                                switch ($answerType) {
4569
                                    case MATCHING:
4570
                                    case MATCHING_DRAGGABLE:
4571
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4572
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4573
                                                break;
4574
                                            }
4575
                                        }
4576
                                        echo '<tr>';
4577
                                        if (!in_array(
4578
                                            $this->results_disabled,
4579
                                            [
4580
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4581
                                            ]
4582
                                        )
4583
                                        ) {
4584
                                            echo '<td>'.$s_answer_label.'</td>';
4585
                                            echo '<td>'.$user_answer.'</td>';
4586
                                        } else {
4587
                                            echo '<td>'.$s_answer_label.'</td>';
4588
                                            $status = Display::label(get_lang('Correct'), 'success');
4589
                                        }
4590
4591
                                        if ($this->showExpectedChoice()) {
4592
                                            if ($this->showExpectedChoiceColumn()) {
4593
                                                echo '<td>';
4594
                                                if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4595
                                                    if (isset($real_list[$i_answer_correct_answer]) &&
4596
                                                        true == $showTotalScoreAndUserChoicesInLastAttempt
4597
                                                    ) {
4598
                                                        echo Display::span(
4599
                                                            $real_list[$i_answer_correct_answer]
4600
                                                        );
4601
                                                    }
4602
                                                }
4603
                                                echo '</td>';
4604
                                            }
4605
                                            echo '<td class="text-center">'.$status.'</td>';
4606
                                        } else {
4607
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4608
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4609
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4610
                                                ) {
4611
                                                    if ($this->showExpectedChoiceColumn()) {
4612
                                                        echo '<td>';
4613
                                                        echo Display::span(
4614
                                                            $real_list[$i_answer_correct_answer],
4615
                                                            ['style' => 'color: #008000; font-weight: bold;']
4616
                                                        );
4617
                                                        echo '</td>';
4618
                                                    }
4619
                                                }
4620
                                            }
4621
                                        }
4622
                                        echo '</tr>';
4623
4624
                                        break;
4625
                                    case DRAGGABLE:
4626
                                        if (false == $showTotalScoreAndUserChoicesInLastAttempt) {
4627
                                            $s_answer_label = '';
4628
                                        }
4629
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4630
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4631
                                                break;
4632
                                            }
4633
                                        }
4634
                                        echo '<tr>';
4635
                                        if ($this->showExpectedChoice()) {
4636
                                            if (!in_array(
4637
                                                $this->results_disabled,
4638
                                                [
4639
                                                    RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4640
                                                    //RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4641
                                                ]
4642
                                            )
4643
                                            ) {
4644
                                                echo '<td>'.$user_answer.'</td>';
4645
                                            } else {
4646
                                                $status = Display::label(get_lang('Correct'), 'success');
4647
                                            }
4648
                                            echo '<td>'.$s_answer_label.'</td>';
4649
                                            echo '<td class="text-center">'.$status.'</td>';
4650
                                        } else {
4651
                                            echo '<td>'.$s_answer_label.'</td>';
4652
                                            echo '<td>'.$user_answer.'</td>';
4653
                                            echo '<td>';
4654
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4655
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4656
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4657
                                                ) {
4658
                                                    echo Display::span(
4659
                                                        $real_list[$i_answer_correct_answer],
4660
                                                        ['style' => 'color: #008000; font-weight: bold;']
4661
                                                    );
4662
                                                }
4663
                                            }
4664
                                            echo '</td>';
4665
                                        }
4666
                                        echo '</tr>';
4667
4668
                                        break;
4669
                                }
4670
                            }
4671
                            $counterAnswer++;
4672
                        }
4673
4674
                        break 2; // break the switch and the "for" condition
4675
                    } else {
4676
                        if ($answerCorrect) {
4677
                            if (isset($choice[$answerAutoId]) &&
4678
                                $answerCorrect == $choice[$answerAutoId]
4679
                            ) {
4680
                                $correctAnswerId[] = $answerAutoId;
4681
                                $questionScore += $answerWeighting;
4682
                                $totalScore += $answerWeighting;
4683
                                $user_answer = Display::span($answerMatching[$choice[$answerAutoId]]);
4684
                            } else {
4685
                                if (isset($answerMatching[$choice[$answerAutoId]])) {
4686
                                    $user_answer = Display::span(
4687
                                        $answerMatching[$choice[$answerAutoId]],
4688
                                        ['style' => 'color: #FF0000; text-decoration: line-through;']
4689
                                    );
4690
                                }
4691
                            }
4692
                            $matching[$answerAutoId] = $choice[$answerAutoId];
4693
                        }
4694
                    }
4695
4696
                    break;
4697
                case HOT_SPOT:
4698
                    if ($from_database) {
4699
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4700
                        // Check auto id
4701
                        $foundAnswerId = $answerAutoId;
4702
                        $sql = "SELECT hotspot_correct
4703
                                FROM $TBL_TRACK_HOTSPOT
4704
                                WHERE
4705
                                    hotspot_exe_id = $exeId AND
4706
                                    hotspot_question_id= $questionId AND
4707
                                    hotspot_answer_id = $answerAutoId
4708
                                ORDER BY hotspot_id ASC";
4709
                        $result = Database::query($sql);
4710
                        if (Database::num_rows($result)) {
4711
                            $studentChoice = Database::result(
4712
                                $result,
4713
                                0,
4714
                                'hotspot_correct'
4715
                            );
4716
4717
                            if ($studentChoice) {
4718
                                $questionScore += $answerWeighting;
4719
                                $totalScore += $answerWeighting;
4720
                            }
4721
                        } else {
4722
                            // If answer.id is different:
4723
                            $sql = "SELECT hotspot_correct
4724
                                FROM $TBL_TRACK_HOTSPOT
4725
                                WHERE
4726
                                    hotspot_exe_id = $exeId AND
4727
                                    hotspot_question_id= $questionId AND
4728
                                    hotspot_answer_id = ".(int) $answerId.'
4729
                                ORDER BY hotspot_id ASC';
4730
                            $result = Database::query($sql);
4731
4732
                            $foundAnswerId = $answerId;
4733
                            if (Database::num_rows($result)) {
4734
                                $studentChoice = Database::result(
4735
                                    $result,
4736
                                    0,
4737
                                    'hotspot_correct'
4738
                                );
4739
4740
                                if ($studentChoice) {
4741
                                    $questionScore += $answerWeighting;
4742
                                    $totalScore += $answerWeighting;
4743
                                }
4744
                            } else {
4745
                                // check answer.iid
4746
                                if (!empty($answerIid)) {
4747
                                    $sql = "SELECT hotspot_correct
4748
                                            FROM $TBL_TRACK_HOTSPOT
4749
                                            WHERE
4750
                                                hotspot_exe_id = $exeId AND
4751
                                                hotspot_question_id= $questionId AND
4752
                                                hotspot_answer_id = $answerIid
4753
                                            ORDER BY hotspot_id ASC";
4754
                                    $result = Database::query($sql);
4755
4756
                                    $foundAnswerId = $answerIid;
4757
                                    $studentChoice = Database::result(
4758
                                        $result,
4759
                                        0,
4760
                                        'hotspot_correct'
4761
                                    );
4762
4763
                                    if ($studentChoice) {
4764
                                        $questionScore += $answerWeighting;
4765
                                        $totalScore += $answerWeighting;
4766
                                    }
4767
                                }
4768
                            }
4769
                        }
4770
                    } else {
4771
                        if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
4772
                            $choice[$answerAutoId] = 0;
4773
                            $choice[$answerIid] = 0;
4774
                        } else {
4775
                            $studentChoice = $choice[$answerAutoId];
4776
                            if (empty($studentChoice)) {
4777
                                $studentChoice = $choice[$answerIid];
4778
                            }
4779
                            $choiceIsValid = false;
4780
                            if (!empty($studentChoice)) {
4781
                                $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
4782
                                $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
4783
                                $choicePoint = Geometry::decodePoint($studentChoice);
4784
4785
                                switch ($hotspotType) {
4786
                                    case 'square':
4787
                                        $hotspotProperties = Geometry::decodeSquare($hotspotCoordinates);
4788
                                        $choiceIsValid = Geometry::pointIsInSquare($hotspotProperties, $choicePoint);
4789
4790
                                        break;
4791
                                    case 'circle':
4792
                                        $hotspotProperties = Geometry::decodeEllipse($hotspotCoordinates);
4793
                                        $choiceIsValid = Geometry::pointIsInEllipse($hotspotProperties, $choicePoint);
4794
4795
                                        break;
4796
                                    case 'poly':
4797
                                        $hotspotProperties = Geometry::decodePolygon($hotspotCoordinates);
4798
                                        $choiceIsValid = Geometry::pointIsInPolygon($hotspotProperties, $choicePoint);
4799
4800
                                        break;
4801
                                }
4802
                            }
4803
4804
                            $choice[$answerAutoId] = 0;
4805
                            if ($choiceIsValid) {
4806
                                $questionScore += $answerWeighting;
4807
                                $totalScore += $answerWeighting;
4808
                                $choice[$answerAutoId] = 1;
4809
                                $choice[$answerIid] = 1;
4810
                            }
4811
                        }
4812
                    }
4813
4814
                    break;
4815
                case HOT_SPOT_ORDER:
4816
                    // @todo never added to chamilo
4817
                    // for hotspot with fixed order
4818
                    $studentChoice = $choice['order'][$answerId];
4819
                    if ($studentChoice == $answerId) {
4820
                        $questionScore += $answerWeighting;
4821
                        $totalScore += $answerWeighting;
4822
                        $studentChoice = true;
4823
                    } else {
4824
                        $studentChoice = false;
4825
                    }
4826
4827
                    break;
4828
                case HOT_SPOT_DELINEATION:
4829
                    // for hotspot with delineation
4830
                    if ($from_database) {
4831
                        // getting the user answer
4832
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4833
                        $query = "SELECT hotspot_correct, hotspot_coordinate
4834
                                    FROM $TBL_TRACK_HOTSPOT
4835
                                    WHERE
4836
                                        hotspot_exe_id = $exeId AND
4837
                                        hotspot_question_id= $questionId AND
4838
                                        hotspot_answer_id = '1'";
4839
                        // By default we take 1 because it's a delineation
4840
                        $resq = Database::query($query);
4841
                        $row = Database::fetch_array($resq, 'ASSOC');
4842
4843
                        $choice = $row['hotspot_correct'];
4844
                        $user_answer = $row['hotspot_coordinate'];
4845
4846
                        // THIS is very important otherwise the poly_compile will throw an error!!
4847
                        // round-up the coordinates
4848
                        $coords = explode('/', $user_answer);
4849
                        $coords = array_filter($coords);
4850
                        $user_array = '';
4851
                        foreach ($coords as $coord) {
4852
                            [$x, $y] = explode(';', $coord);
4853
                            $user_array .= round($x).';'.round($y).'/';
4854
                        }
4855
                        $user_array = substr($user_array, 0, -1) ?: '';
4856
                    } else {
4857
                        if (!empty($studentChoice)) {
4858
                            $correctAnswerId[] = $answerAutoId;
4859
                            $newquestionList[] = $questionId;
4860
                        }
4861
4862
                        if (1 === $answerId) {
4863
                            $studentChoice = $choice[$answerId];
4864
                            $questionScore += $answerWeighting;
4865
                        }
4866
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
4867
                            $user_array = $_SESSION['exerciseResultCoordinates'][$questionId];
4868
                        }
4869
                    }
4870
                    $_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
4871
                    $_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
4872
4873
                    break;
4874
                case ANNOTATION:
4875
                    if ($from_database) {
4876
                        $sql = "SELECT answer, marks
4877
                                FROM $TBL_TRACK_ATTEMPT
4878
                                WHERE
4879
                                  exe_id = $exeId AND
4880
                                  question_id = $questionId ";
4881
                        $resq = Database::query($sql);
4882
                        $data = Database::fetch_array($resq);
4883
4884
                        $questionScore = empty($data['marks']) ? 0 : $data['marks'];
4885
                        $arrques = $questionName;
4886
4887
                        break;
4888
                    }
4889
                    $studentChoice = $choice;
4890
                    if ($studentChoice) {
4891
                        $questionScore = 0;
4892
                    }
4893
4894
                    break;
4895
            }
4896
4897
            if ($show_result) {
4898
                if ('exercise_result' === $from) {
4899
                    // Display answers (if not matching type, or if the answer is correct)
4900
                    if (!in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE]) ||
4901
                        $answerCorrect
4902
                    ) {
4903
                        if (in_array(
4904
                            $answerType,
4905
                            [
4906
                                UNIQUE_ANSWER,
4907
                                UNIQUE_ANSWER_IMAGE,
4908
                                UNIQUE_ANSWER_NO_OPTION,
4909
                                MULTIPLE_ANSWER,
4910
                                MULTIPLE_ANSWER_COMBINATION,
4911
                                GLOBAL_MULTIPLE_ANSWER,
4912
                                READING_COMPREHENSION,
4913
                            ]
4914
                        )) {
4915
                            ExerciseShowFunctions::display_unique_or_multiple_answer(
4916
                                $this,
4917
                                $feedback_type,
4918
                                $answerType,
4919
                                $studentChoice,
4920
                                $answer,
4921
                                $answerComment,
4922
                                $answerCorrect,
4923
                                0,
4924
                                0,
4925
                                0,
4926
                                $results_disabled,
4927
                                $showTotalScoreAndUserChoicesInLastAttempt,
4928
                                $this->export
4929
                            );
4930
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
4931
                            ExerciseShowFunctions::display_multiple_answer_true_false(
4932
                                $this,
4933
                                $feedback_type,
4934
                                $answerType,
4935
                                $studentChoice,
4936
                                $answer,
4937
                                $answerComment,
4938
                                $answerCorrect,
4939
                                0,
4940
                                $questionId,
4941
                                0,
4942
                                $results_disabled,
4943
                                $showTotalScoreAndUserChoicesInLastAttempt
4944
                            );
4945
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
4946
                            ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
4947
                                $this,
4948
                                $feedback_type,
4949
                                $studentChoice,
4950
                                $studentChoiceDegree,
4951
                                $answer,
4952
                                $answerComment,
4953
                                $answerCorrect,
4954
                                $questionId,
4955
                                $results_disabled
4956
                            );
4957
                        } elseif (MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType) {
4958
                            ExerciseShowFunctions::display_multiple_answer_combination_true_false(
4959
                                $this,
4960
                                $feedback_type,
4961
                                $answerType,
4962
                                $studentChoice,
4963
                                $answer,
4964
                                $answerComment,
4965
                                $answerCorrect,
4966
                                0,
4967
                                0,
4968
                                0,
4969
                                $results_disabled,
4970
                                $showTotalScoreAndUserChoicesInLastAttempt
4971
                            );
4972
                        } elseif (FILL_IN_BLANKS == $answerType) {
4973
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
4974
                                $this,
4975
                                $feedback_type,
4976
                                $answer,
4977
                                0,
4978
                                0,
4979
                                $results_disabled,
4980
                                $showTotalScoreAndUserChoicesInLastAttempt,
4981
                                ''
4982
                            );
4983
                        } elseif (CALCULATED_ANSWER == $answerType) {
4984
                            ExerciseShowFunctions::display_calculated_answer(
4985
                                $this,
4986
                                $feedback_type,
4987
                                $answer,
4988
                                0,
4989
                                0,
4990
                                $results_disabled,
4991
                                $showTotalScoreAndUserChoicesInLastAttempt,
4992
                                $expectedAnswer,
4993
                                $calculatedChoice,
4994
                                $calculatedStatus
4995
                            );
4996
                        } elseif (FREE_ANSWER == $answerType) {
4997
                            ExerciseShowFunctions::display_free_answer(
4998
                                $feedback_type,
4999
                                $choice,
5000
                                $exeId,
5001
                                $questionId,
5002
                                $questionScore,
5003
                                $results_disabled
5004
                            );
5005
                        } elseif (ORAL_EXPRESSION == $answerType) {
5006
                            // to store the details of open questions in an array to be used in mail
5007
                            /** @var OralExpression $objQuestionTmp */
5008
                            ExerciseShowFunctions::display_oral_expression_answer(
5009
                                $feedback_type,
5010
                                $choice,
5011
                                $exeId,
5012
                                $questionId,
5013
                                $results_disabled,
5014
                                $questionScore,
5015
                                true
5016
                            );
5017
                        } elseif (HOT_SPOT == $answerType) {
5018
                            $correctAnswerId = 0;
5019
                            /** @var TrackEHotspot $hotspot */
5020
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5021
                                if ($hotspot->getHotspotAnswerId() == $answerAutoId) {
5022
                                    break;
5023
                                }
5024
                            }
5025
5026
                            // force to show whether the choice is correct or not
5027
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
5028
                            ExerciseShowFunctions::display_hotspot_answer(
5029
                                $this,
5030
                                $feedback_type,
5031
                                $answerId,
5032
                                $answer,
5033
                                $studentChoice,
5034
                                $answerComment,
5035
                                $results_disabled,
5036
                                $answerId,
5037
                                $showTotalScoreAndUserChoicesInLastAttempt
5038
                            );
5039
                        } elseif (HOT_SPOT_ORDER == $answerType) {
5040
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5041
                                $feedback_type,
5042
                                $answerId,
5043
                                $answer,
5044
                                $studentChoice,
5045
                                $answerComment
5046
                            );*/
5047
                        } elseif (HOT_SPOT_DELINEATION == $answerType) {
5048
                            $user_answer = $_SESSION['exerciseResultCoordinates'][$questionId];
5049
5050
                            // Round-up the coordinates
5051
                            $coords = explode('/', $user_answer);
5052
                            $coords = array_filter($coords);
5053
                            $user_array = '';
5054
                            foreach ($coords as $coord) {
5055
                                if (!empty($coord)) {
5056
                                    $parts = explode(';', $coord);
5057
                                    if (!empty($parts)) {
5058
                                        $user_array .= round($parts[0]).';'.round($parts[1]).'/';
5059
                                    }
5060
                                }
5061
                            }
5062
                            $user_array = substr($user_array, 0, -1) ?: '';
5063
                            if ($next) {
5064
                                $user_answer = $user_array;
5065
                                // We compare only the delineation not the other points
5066
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5067
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5068
5069
                                // Calculating the area
5070
                                $poly_user = convert_coordinates($user_answer, '/');
5071
                                $poly_answer = convert_coordinates($answer_question, '|');
5072
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5073
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5074
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5075
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5076
5077
                                $overlap = $poly_results['both'];
5078
                                $poly_answer_area = $poly_results['s1'];
5079
                                $poly_user_area = $poly_results['s2'];
5080
                                $missing = $poly_results['s1Only'];
5081
                                $excess = $poly_results['s2Only'];
5082
5083
                                // //this is an area in pixels
5084
                                if ($debug > 0) {
5085
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5086
                                }
5087
5088
                                if ($overlap < 1) {
5089
                                    // Shortcut to avoid complicated calculations
5090
                                    $final_overlap = 0;
5091
                                    $final_missing = 100;
5092
                                    $final_excess = 100;
5093
                                } else {
5094
                                    // the final overlap is the percentage of the initial polygon
5095
                                    // that is overlapped by the user's polygon
5096
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5097
                                    if ($debug > 1) {
5098
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap, 0);
5099
                                    }
5100
                                    // the final missing area is the percentage of the initial polygon
5101
                                    // that is not overlapped by the user's polygon
5102
                                    $final_missing = 100 - $final_overlap;
5103
                                    if ($debug > 1) {
5104
                                        error_log(__LINE__.' - Final missing is '.$final_missing, 0);
5105
                                    }
5106
                                    // the final excess area is the percentage of the initial polygon's size
5107
                                    // that is covered by the user's polygon outside of the initial polygon
5108
                                    $final_excess = round(
5109
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5110
                                    );
5111
                                    if ($debug > 1) {
5112
                                        error_log(__LINE__.' - Final excess is '.$final_excess, 0);
5113
                                    }
5114
                                }
5115
5116
                                // Checking the destination parameters parsing the "@@"
5117
                                $destination_items = explode('@@', $answerDestination);
5118
                                $threadhold_total = $destination_items[0];
5119
                                $threadhold_items = explode(';', $threadhold_total);
5120
                                $threadhold1 = $threadhold_items[0]; // overlap
5121
                                $threadhold2 = $threadhold_items[1]; // excess
5122
                                $threadhold3 = $threadhold_items[2]; // missing
5123
5124
                                // if is delineation
5125
                                if (1 === $answerId) {
5126
                                    //setting colors
5127
                                    if ($final_overlap >= $threadhold1) {
5128
                                        $overlap_color = true;
5129
                                    }
5130
                                    if ($final_excess <= $threadhold2) {
5131
                                        $excess_color = true;
5132
                                    }
5133
                                    if ($final_missing <= $threadhold3) {
5134
                                        $missing_color = true;
5135
                                    }
5136
5137
                                    // if pass
5138
                                    if ($final_overlap >= $threadhold1 &&
5139
                                        $final_missing <= $threadhold3 &&
5140
                                        $final_excess <= $threadhold2
5141
                                    ) {
5142
                                        $next = 1; //go to the oars
5143
                                        $result_comment = get_lang('Acceptable');
5144
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5145
                                    } else {
5146
                                        $next = 0;
5147
                                        $result_comment = get_lang('Unacceptable');
5148
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5149
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5150
                                        // checking the destination parameters parsing the "@@"
5151
                                        $destination_items = explode('@@', $answerDestination);
5152
                                    }
5153
                                } elseif ($answerId > 1) {
5154
                                    if ('noerror' == $objAnswerTmp->selectHotspotType($answerId)) {
5155
                                        if ($debug > 0) {
5156
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5157
                                        }
5158
                                        //type no error shouldn't be treated
5159
                                        $next = 1;
5160
5161
                                        continue;
5162
                                    }
5163
                                    if ($debug > 0) {
5164
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5165
                                    }
5166
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5167
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5168
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5169
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5170
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5171
5172
                                    if (false == $overlap) {
5173
                                        //all good, no overlap
5174
                                        $next = 1;
5175
5176
                                        continue;
5177
                                    } else {
5178
                                        if ($debug > 0) {
5179
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5180
                                        }
5181
                                        $organs_at_risk_hit++;
5182
                                        //show the feedback
5183
                                        $next = 0;
5184
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5185
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5186
5187
                                        $destination_items = explode('@@', $answerDestination);
5188
                                        $try_hotspot = $destination_items[1];
5189
                                        $lp_hotspot = $destination_items[2];
5190
                                        $select_question_hotspot = $destination_items[3];
5191
                                        $url_hotspot = $destination_items[4];
5192
                                    }
5193
                                }
5194
                            } else {
5195
                                // the first delineation feedback
5196
                                if ($debug > 0) {
5197
                                    error_log(__LINE__.' first', 0);
5198
                                }
5199
                            }
5200
                        } elseif (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
5201
                            echo '<tr>';
5202
                            echo Display::tag('td', $answerMatching[$answerId]);
5203
                            echo Display::tag(
5204
                                'td',
5205
                                "$user_answer / ".Display::tag(
5206
                                    'strong',
5207
                                    $answerMatching[$answerCorrect],
5208
                                    ['style' => 'color: #008000; font-weight: bold;']
5209
                                )
5210
                            );
5211
                            echo '</tr>';
5212
                        } elseif (ANNOTATION == $answerType) {
5213
                            ExerciseShowFunctions::displayAnnotationAnswer(
5214
                                $feedback_type,
5215
                                $exeId,
5216
                                $questionId,
5217
                                $questionScore,
5218
                                $results_disabled
5219
                            );
5220
                        }
5221
                    }
5222
                } else {
5223
                    if ($debug) {
5224
                        error_log('Showing questions $from '.$from);
5225
                    }
5226
5227
                    switch ($answerType) {
5228
                        case UNIQUE_ANSWER:
5229
                        case UNIQUE_ANSWER_IMAGE:
5230
                        case UNIQUE_ANSWER_NO_OPTION:
5231
                        case MULTIPLE_ANSWER:
5232
                        case GLOBAL_MULTIPLE_ANSWER:
5233
                        case MULTIPLE_ANSWER_COMBINATION:
5234
                        case READING_COMPREHENSION:
5235
                            if (1 == $answerId) {
5236
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5237
                                    $this,
5238
                                    $feedback_type,
5239
                                    $answerType,
5240
                                    $studentChoice,
5241
                                    $answer,
5242
                                    $answerComment,
5243
                                    $answerCorrect,
5244
                                    $exeId,
5245
                                    $questionId,
5246
                                    $answerId,
5247
                                    $results_disabled,
5248
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5249
                                    $this->export
5250
                                );
5251
                            } else {
5252
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5253
                                    $this,
5254
                                    $feedback_type,
5255
                                    $answerType,
5256
                                    $studentChoice,
5257
                                    $answer,
5258
                                    $answerComment,
5259
                                    $answerCorrect,
5260
                                    $exeId,
5261
                                    $questionId,
5262
                                    '',
5263
                                    $results_disabled,
5264
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5265
                                    $this->export
5266
                                );
5267
                            }
5268
5269
                            break;
5270
                        case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
5271
                            if (1 == $answerId) {
5272
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5273
                                    $this,
5274
                                    $feedback_type,
5275
                                    $answerType,
5276
                                    $studentChoice,
5277
                                    $answer,
5278
                                    $answerComment,
5279
                                    $answerCorrect,
5280
                                    $exeId,
5281
                                    $questionId,
5282
                                    $answerId,
5283
                                    $results_disabled,
5284
                                    $showTotalScoreAndUserChoicesInLastAttempt
5285
                                );
5286
                            } else {
5287
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5288
                                    $this,
5289
                                    $feedback_type,
5290
                                    $answerType,
5291
                                    $studentChoice,
5292
                                    $answer,
5293
                                    $answerComment,
5294
                                    $answerCorrect,
5295
                                    $exeId,
5296
                                    $questionId,
5297
                                    '',
5298
                                    $results_disabled,
5299
                                    $showTotalScoreAndUserChoicesInLastAttempt
5300
                                );
5301
                            }
5302
5303
                            break;
5304
                        case MULTIPLE_ANSWER_TRUE_FALSE:
5305
                            if (1 == $answerId) {
5306
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5307
                                    $this,
5308
                                    $feedback_type,
5309
                                    $answerType,
5310
                                    $studentChoice,
5311
                                    $answer,
5312
                                    $answerComment,
5313
                                    $answerCorrect,
5314
                                    $exeId,
5315
                                    $questionId,
5316
                                    $answerId,
5317
                                    $results_disabled,
5318
                                    $showTotalScoreAndUserChoicesInLastAttempt
5319
                                );
5320
                            } else {
5321
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5322
                                    $this,
5323
                                    $feedback_type,
5324
                                    $answerType,
5325
                                    $studentChoice,
5326
                                    $answer,
5327
                                    $answerComment,
5328
                                    $answerCorrect,
5329
                                    $exeId,
5330
                                    $questionId,
5331
                                    '',
5332
                                    $results_disabled,
5333
                                    $showTotalScoreAndUserChoicesInLastAttempt
5334
                                );
5335
                            }
5336
5337
                            break;
5338
                        case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
5339
                            if (1 == $answerId) {
5340
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5341
                                    $this,
5342
                                    $feedback_type,
5343
                                    $studentChoice,
5344
                                    $studentChoiceDegree,
5345
                                    $answer,
5346
                                    $answerComment,
5347
                                    $answerCorrect,
5348
                                    $questionId,
5349
                                    $results_disabled
5350
                                );
5351
                            } else {
5352
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5353
                                    $this,
5354
                                    $feedback_type,
5355
                                    $studentChoice,
5356
                                    $studentChoiceDegree,
5357
                                    $answer,
5358
                                    $answerComment,
5359
                                    $answerCorrect,
5360
                                    $questionId,
5361
                                    $results_disabled
5362
                                );
5363
                            }
5364
5365
                            break;
5366
                        case FILL_IN_BLANKS:
5367
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5368
                                $this,
5369
                                $feedback_type,
5370
                                $answer,
5371
                                $exeId,
5372
                                $questionId,
5373
                                $results_disabled,
5374
                                $showTotalScoreAndUserChoicesInLastAttempt,
5375
                                $str
5376
                            );
5377
5378
                            break;
5379
                        case CALCULATED_ANSWER:
5380
                            ExerciseShowFunctions::display_calculated_answer(
5381
                                $this,
5382
                                $feedback_type,
5383
                                $answer,
5384
                                $exeId,
5385
                                $questionId,
5386
                                $results_disabled,
5387
                                '',
5388
                                $showTotalScoreAndUserChoicesInLastAttempt
5389
                            );
5390
5391
                            break;
5392
                        case FREE_ANSWER:
5393
                            echo ExerciseShowFunctions::display_free_answer(
5394
                                $feedback_type,
5395
                                $choice,
5396
                                $exeId,
5397
                                $questionId,
5398
                                $questionScore,
5399
                                $results_disabled
5400
                            );
5401
5402
                            break;
5403
                        case ORAL_EXPRESSION:
5404
                            /** @var OralExpression $objQuestionTmp */
5405
                            echo '<tr>
5406
                                <td valign="top">'.
5407
                                ExerciseShowFunctions::display_oral_expression_answer(
5408
                                    $feedback_type,
5409
                                    $choice,
5410
                                    $exeId,
5411
                                    $questionId,
5412
                                    $results_disabled,
5413
                                    $questionScore
5414
                                ).'</td>
5415
                                </tr>
5416
                                </table>';
5417
                            break;
5418
                        case HOT_SPOT:
5419
                            $correctAnswerId = 0;
5420
5421
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5422
                                if ($hotspot->getHotspotAnswerId() == $foundAnswerId) {
5423
                                    break;
5424
                                }
5425
                            }
5426
                            ExerciseShowFunctions::display_hotspot_answer(
5427
                                $this,
5428
                                $feedback_type,
5429
                                $answerId,
5430
                                $answer,
5431
                                $studentChoice,
5432
                                $answerComment,
5433
                                $results_disabled,
5434
                                $answerId,
5435
                                $showTotalScoreAndUserChoicesInLastAttempt
5436
                            );
5437
5438
                            break;
5439
                        case HOT_SPOT_DELINEATION:
5440
                            $user_answer = $user_array;
5441
                            if ($next) {
5442
                                $user_answer = $user_array;
5443
                                // we compare only the delineation not the other points
5444
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5445
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5446
5447
                                // calculating the area
5448
                                $poly_user = convert_coordinates($user_answer, '/');
5449
                                $poly_answer = convert_coordinates($answer_question, '|');
5450
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5451
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5452
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5453
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5454
5455
                                $overlap = $poly_results['both'];
5456
                                $poly_answer_area = $poly_results['s1'];
5457
                                $poly_user_area = $poly_results['s2'];
5458
                                $missing = $poly_results['s1Only'];
5459
                                $excess = $poly_results['s2Only'];
5460
                                if ($debug > 0) {
5461
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5462
                                }
5463
                                if ($overlap < 1) {
5464
                                    //shortcut to avoid complicated calculations
5465
                                    $final_overlap = 0;
5466
                                    $final_missing = 100;
5467
                                    $final_excess = 100;
5468
                                } else {
5469
                                    // the final overlap is the percentage of the initial polygon
5470
                                    // that is overlapped by the user's polygon
5471
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5472
5473
                                    // the final missing area is the percentage of the initial polygon that
5474
                                    // is not overlapped by the user's polygon
5475
                                    $final_missing = 100 - $final_overlap;
5476
                                    // the final excess area is the percentage of the initial polygon's size that is
5477
                                    // covered by the user's polygon outside of the initial polygon
5478
                                    $final_excess = round(
5479
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5480
                                    );
5481
5482
                                    if ($debug > 1) {
5483
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap);
5484
                                        error_log(__LINE__.' - Final excess is '.$final_excess);
5485
                                        error_log(__LINE__.' - Final missing is '.$final_missing);
5486
                                    }
5487
                                }
5488
5489
                                // Checking the destination parameters parsing the "@@"
5490
                                $destination_items = explode('@@', $answerDestination);
5491
                                $threadhold_total = $destination_items[0];
5492
                                $threadhold_items = explode(';', $threadhold_total);
5493
                                $threadhold1 = $threadhold_items[0]; // overlap
5494
                                $threadhold2 = $threadhold_items[1]; // excess
5495
                                $threadhold3 = $threadhold_items[2]; //missing
5496
                                // if is delineation
5497
                                if (1 === $answerId) {
5498
                                    //setting colors
5499
                                    if ($final_overlap >= $threadhold1) {
5500
                                        $overlap_color = true;
5501
                                    }
5502
                                    if ($final_excess <= $threadhold2) {
5503
                                        $excess_color = true;
5504
                                    }
5505
                                    if ($final_missing <= $threadhold3) {
5506
                                        $missing_color = true;
5507
                                    }
5508
5509
                                    // if pass
5510
                                    if ($final_overlap >= $threadhold1 &&
5511
                                        $final_missing <= $threadhold3 &&
5512
                                        $final_excess <= $threadhold2
5513
                                    ) {
5514
                                        $next = 1; //go to the oars
5515
                                        $result_comment = get_lang('Acceptable');
5516
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5517
                                    } else {
5518
                                        $next = 0;
5519
                                        $result_comment = get_lang('Unacceptable');
5520
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5521
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5522
                                        //checking the destination parameters parsing the "@@"
5523
                                        $destination_items = explode('@@', $answerDestination);
5524
                                    }
5525
                                } elseif ($answerId > 1) {
5526
                                    if ('noerror' === $objAnswerTmp->selectHotspotType($answerId)) {
5527
                                        if ($debug > 0) {
5528
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5529
                                        }
5530
                                        //type no error shouldn't be treated
5531
                                        $next = 1;
5532
5533
                                        break;
5534
                                    }
5535
                                    if ($debug > 0) {
5536
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5537
                                    }
5538
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5539
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5540
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5541
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5542
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5543
5544
                                    if (false == $overlap) {
5545
                                        //all good, no overlap
5546
                                        $next = 1;
5547
5548
                                        break;
5549
                                    } else {
5550
                                        if ($debug > 0) {
5551
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5552
                                        }
5553
                                        $organs_at_risk_hit++;
5554
                                        //show the feedback
5555
                                        $next = 0;
5556
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5557
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5558
5559
                                        $destination_items = explode('@@', $answerDestination);
5560
                                        $try_hotspot = $destination_items[1];
5561
                                        $lp_hotspot = $destination_items[2];
5562
                                        $select_question_hotspot = $destination_items[3];
5563
                                        $url_hotspot = $destination_items[4];
5564
                                    }
5565
                                }
5566
                            }
5567
5568
                            break;
5569
                        case HOT_SPOT_ORDER:
5570
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5571
                                $feedback_type,
5572
                                $answerId,
5573
                                $answer,
5574
                                $studentChoice,
5575
                                $answerComment
5576
                            );*/
5577
5578
                            break;
5579
                        case DRAGGABLE:
5580
                        case MATCHING_DRAGGABLE:
5581
                        case MATCHING:
5582
                            echo '<tr>';
5583
                            echo Display::tag('td', $answerMatching[$answerId]);
5584
                            echo Display::tag(
5585
                                'td',
5586
                                "$user_answer / ".Display::tag(
5587
                                    'strong',
5588
                                    $answerMatching[$answerCorrect],
5589
                                    ['style' => 'color: #008000; font-weight: bold;']
5590
                                )
5591
                            );
5592
                            echo '</tr>';
5593
5594
                            break;
5595
                        case ANNOTATION:
5596
                            ExerciseShowFunctions::displayAnnotationAnswer(
5597
                                $feedback_type,
5598
                                $exeId,
5599
                                $questionId,
5600
                                $questionScore,
5601
                                $results_disabled
5602
                            );
5603
5604
                            break;
5605
                    }
5606
                }
5607
            }
5608
        } // end for that loops over all answers of the current question
5609
5610
        if ($debug) {
5611
            error_log('-- End answer loop --');
5612
        }
5613
5614
        $final_answer = true;
5615
5616
        foreach ($real_answers as $my_answer) {
5617
            if (!$my_answer) {
5618
                $final_answer = false;
5619
            }
5620
        }
5621
5622
        //we add the total score after dealing with the answers
5623
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
5624
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
5625
        ) {
5626
            if ($final_answer) {
5627
                //getting only the first score where we save the weight of all the question
5628
                $answerWeighting = $objAnswerTmp->selectWeighting(1);
5629
                if (empty($answerWeighting) && !empty($firstAnswer) && isset($firstAnswer['ponderation'])) {
5630
                    $answerWeighting = $firstAnswer['ponderation'];
5631
                }
5632
                $questionScore += $answerWeighting;
5633
            }
5634
        }
5635
5636
        $extra_data = [
5637
            'final_overlap' => $final_overlap,
5638
            'final_missing' => $final_missing,
5639
            'final_excess' => $final_excess,
5640
            'overlap_color' => $overlap_color,
5641
            'missing_color' => $missing_color,
5642
            'excess_color' => $excess_color,
5643
            'threadhold1' => $threadhold1,
5644
            'threadhold2' => $threadhold2,
5645
            'threadhold3' => $threadhold3,
5646
        ];
5647
5648
        if ('exercise_result' === $from) {
5649
            // if answer is hotspot. To the difference of exercise_show.php,
5650
            //  we use the results from the session (from_db=0)
5651
            // TODO Change this, because it is wrong to show the user
5652
            //  some results that haven't been stored in the database yet
5653
            if (HOT_SPOT == $answerType || HOT_SPOT_ORDER == $answerType || HOT_SPOT_DELINEATION == $answerType) {
5654
                if ($debug) {
5655
                    error_log('$from AND this is a hotspot kind of question ');
5656
                }
5657
                if (HOT_SPOT_DELINEATION === $answerType) {
5658
                    if ($showHotSpotDelineationTable) {
5659
                        if (!is_numeric($final_overlap)) {
5660
                            $final_overlap = 0;
5661
                        }
5662
                        if (!is_numeric($final_missing)) {
5663
                            $final_missing = 0;
5664
                        }
5665
                        if (!is_numeric($final_excess)) {
5666
                            $final_excess = 0;
5667
                        }
5668
5669
                        if ($final_overlap > 100) {
5670
                            $final_overlap = 100;
5671
                        }
5672
5673
                        $overlap = 0;
5674
                        if ($final_overlap > 0) {
5675
                            $overlap = (int) $final_overlap;
5676
                        }
5677
5678
                        $excess = 0;
5679
                        if ($final_excess > 0) {
5680
                            $excess = (int) $final_excess;
5681
                        }
5682
5683
                        $missing = 0;
5684
                        if ($final_missing > 0) {
5685
                            $missing = (int) $final_missing;
5686
                        }
5687
5688
                        $table_resume = '<table class="table table-hover table-striped data_table">
5689
                                <tr class="row_odd" >
5690
                                    <td></td>
5691
                                    <td ><b>'.get_lang('Requirements').'</b></td>
5692
                                    <td><b>'.get_lang('Your answer').'</b></td>
5693
                                </tr>
5694
                                <tr class="row_even">
5695
                                    <td><b>'.get_lang('Overlapping areaping area').'</b></td>
5696
                                    <td>'.get_lang('Minimum').' '.$threadhold1.'</td>
5697
                                    <td class="text-right '.($overlap_color ? 'text-success' : 'text-error').'">'
5698
                                    .$overlap.'</td>
5699
                                </tr>
5700
                                <tr>
5701
                                    <td><b>'.get_lang('Excessive areaive area').'</b></td>
5702
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold2.'</td>
5703
                                    <td class="text-right '.($excess_color ? 'text-success' : 'text-error').'">'
5704
                                    .$excess.'</td>
5705
                                </tr>
5706
                                <tr class="row_even">
5707
                                    <td><b>'.get_lang('Missing area area').'</b></td>
5708
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold3.'</td>
5709
                                    <td class="text-right '.($missing_color ? 'text-success' : 'text-error').'">'
5710
                                    .$missing.'</td>
5711
                                </tr>
5712
                            </table>';
5713
                        if (0 == $next) {
5714
                        } else {
5715
                            $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
5716
                            $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
5717
                        }
5718
5719
                        $message = '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>
5720
                                    <p style="text-align:center">';
5721
                        $message .= '<p>'.get_lang('Your delineation :').'</p>';
5722
                        $message .= $table_resume;
5723
                        $message .= '<br />'.get_lang('Your result is :').' '.$result_comment.'<br />';
5724
                        if ($organs_at_risk_hit > 0) {
5725
                            $message .= '<p><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
5726
                        }
5727
                        $message .= '<p>'.$comment.'</p>';
5728
                        echo $message;
5729
5730
                        $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][0] = $message;
5731
                        $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][1] = $_SESSION['exerciseResultCoordinates'][$questionId];
5732
                    } else {
5733
                        echo $hotspot_delineation_result[0];
5734
                    }
5735
5736
                    // Save the score attempts
5737
                    if (1) {
5738
                        //getting the answer 1 or 0 comes from exercise_submit_modal.php
5739
                        $final_answer = $hotspot_delineation_result[1] ?? '';
5740
                        if (0 == $final_answer) {
5741
                            $questionScore = 0;
5742
                        }
5743
                        // we always insert the answer_id 1 = delineation
5744
                        Event::saveQuestionAttempt($this, $questionScore, 1, $quesId, $exeId, 0);
5745
                        //in delineation mode, get the answer from $hotspot_delineation_result[1]
5746
                        $hotspotValue = isset($hotspot_delineation_result[1]) ? 1 === (int) $hotspot_delineation_result[1] ? 1 : 0 : 0;
5747
                        Event::saveExerciseAttemptHotspot(
5748
                            $this,
5749
                            $exeId,
5750
                            $quesId,
5751
                            1,
5752
                            $hotspotValue,
5753
                            $exerciseResultCoordinates[$quesId],
5754
                            false,
5755
                            0,
5756
                            $learnpath_id,
5757
                            $learnpath_item_id
5758
                        );
5759
                    } else {
5760
                        if (0 == $final_answer) {
5761
                            $questionScore = 0;
5762
                            $answer = 0;
5763
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
5764
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5765
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5766
                                    Event::saveExerciseAttemptHotspot(
5767
                                        $this,
5768
                                        $exeId,
5769
                                        $quesId,
5770
                                        $idx,
5771
                                        0,
5772
                                        $val,
5773
                                        false,
5774
                                        0,
5775
                                        $learnpath_id,
5776
                                        $learnpath_item_id
5777
                                    );
5778
                                }
5779
                            }
5780
                        } else {
5781
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
5782
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5783
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5784
                                    $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
5785
                                    Event::saveExerciseAttemptHotspot(
5786
                                        $this,
5787
                                        $exeId,
5788
                                        $quesId,
5789
                                        $idx,
5790
                                        $hotspotValue,
5791
                                        $val,
5792
                                        false,
5793
                                        0,
5794
                                        $learnpath_id,
5795
                                        $learnpath_item_id
5796
                                    );
5797
                                }
5798
                            }
5799
                        }
5800
                    }
5801
                }
5802
            }
5803
5804
            $relPath = api_get_path(WEB_CODE_PATH);
5805
5806
            if (HOT_SPOT == $answerType || HOT_SPOT_ORDER == $answerType) {
5807
                // We made an extra table for the answers
5808
                if ($show_result) {
5809
                    echo '</table></td></tr>';
5810
                    echo '
5811
                        <tr>
5812
                            <td colspan="2">
5813
                                <p><em>'.get_lang('Image zones')."</em></p>
5814
                                <div id=\"hotspot-solution-$questionId\"></div>
5815
                                <script>
5816
                                    $(function() {
5817
                                        new HotspotQuestion({
5818
                                            questionId: $questionId,
5819
                                            exerciseId: {$this->getId()},
5820
                                            exeId: $exeId,
5821
                                            selector: '#hotspot-solution-$questionId',
5822
                                            for: 'solution',
5823
                                            relPath: '$relPath'
5824
                                        });
5825
                                    });
5826
                                </script>
5827
                            </td>
5828
                        </tr>
5829
                    ";
5830
                }
5831
            } elseif (ANNOTATION == $answerType) {
5832
                if ($show_result) {
5833
                    echo '
5834
                        <p><em>'.get_lang('Annotation').'</em></p>
5835
                        <div id="annotation-canvas-'.$questionId.'"></div>
5836
                        <script>
5837
                            AnnotationQuestion({
5838
                                questionId: parseInt('.$questionId.'),
5839
                                exerciseId: parseInt('.$exeId.'),
5840
                                relPath: \''.$relPath.'\',
5841
                                courseId: parseInt('.$course_id.')
5842
                            });
5843
                        </script>
5844
                    ';
5845
                }
5846
            }
5847
5848
            if ($show_result && ANNOTATION != $answerType) {
5849
                echo '</table>';
5850
            }
5851
        }
5852
        unset($objAnswerTmp);
5853
5854
        $totalWeighting += $questionWeighting;
5855
        // Store results directly in the database
5856
        // For all in one page exercises, the results will be
5857
        // stored by exercise_results.php (using the session)
5858
        if ($save_results) {
5859
            if ($debug) {
5860
                error_log("Save question results $save_results");
5861
                error_log("Question score: $questionScore");
5862
                error_log('choice: ');
5863
                error_log(print_r($choice, 1));
5864
            }
5865
5866
            if (empty($choice)) {
5867
                $choice = 0;
5868
            }
5869
            // with certainty degree
5870
            if (empty($choiceDegreeCertainty)) {
5871
                $choiceDegreeCertainty = 0;
5872
            }
5873
            if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
5874
                MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType ||
5875
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
5876
            ) {
5877
                if (0 != $choice) {
5878
                    $reply = array_keys($choice);
5879
                    $countReply = count($reply);
5880
                    for ($i = 0; $i < $countReply; $i++) {
5881
                        $chosenAnswer = $reply[$i];
5882
                        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
5883
                            if (0 != $choiceDegreeCertainty) {
5884
                                $replyDegreeCertainty = array_keys($choiceDegreeCertainty);
5885
                                $answerDegreeCertainty = isset($replyDegreeCertainty[$i]) ? $replyDegreeCertainty[$i] : '';
5886
                                $answerValue = isset($choiceDegreeCertainty[$answerDegreeCertainty]) ? $choiceDegreeCertainty[$answerDegreeCertainty] : '';
5887
                                Event::saveQuestionAttempt(
5888
                                    $this,
5889
                                    $questionScore,
5890
                                    $chosenAnswer.':'.$choice[$chosenAnswer].':'.$answerValue,
5891
                                    $quesId,
5892
                                    $exeId,
5893
                                    $i,
5894
                                    $this->getId(),
5895
                                    $updateResults,
5896
                                    $questionDuration
5897
                                );
5898
                            }
5899
                        } else {
5900
                            Event::saveQuestionAttempt(
5901
                                $this,
5902
                                $questionScore,
5903
                                $chosenAnswer.':'.$choice[$chosenAnswer],
5904
                                $quesId,
5905
                                $exeId,
5906
                                $i,
5907
                                $this->getId(),
5908
                                $updateResults,
5909
                                $questionDuration
5910
                            );
5911
                        }
5912
                        if ($debug) {
5913
                            error_log('result =>'.$questionScore.' '.$chosenAnswer.':'.$choice[$chosenAnswer]);
5914
                        }
5915
                    }
5916
                } else {
5917
                    Event::saveQuestionAttempt(
5918
                        $this,
5919
                        $questionScore,
5920
                        0,
5921
                        $quesId,
5922
                        $exeId,
5923
                        0,
5924
                        $this->getId(),
5925
                        false,
5926
                        $questionDuration
5927
                    );
5928
                }
5929
            } elseif (MULTIPLE_ANSWER == $answerType || GLOBAL_MULTIPLE_ANSWER == $answerType) {
5930
                if (0 != $choice) {
5931
                    $reply = array_keys($choice);
5932
                    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...
5933
                        $ans = $reply[$i];
5934
                        Event::saveQuestionAttempt(
5935
                            $this,
5936
                            $questionScore,
5937
                            $ans,
5938
                            $quesId,
5939
                            $exeId,
5940
                            $i,
5941
                            $this->id,
5942
                            false,
5943
                            $questionDuration
5944
                        );
5945
                    }
5946
                } else {
5947
                    Event::saveQuestionAttempt(
5948
                        $this,
5949
                        $questionScore,
5950
                        0,
5951
                        $quesId,
5952
                        $exeId,
5953
                        0,
5954
                        $this->id,
5955
                        false,
5956
                        $questionDuration
5957
                    );
5958
                }
5959
            } elseif (MULTIPLE_ANSWER_COMBINATION == $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 (in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE])) {
5990
                if (isset($matching)) {
5991
                    foreach ($matching as $j => $val) {
5992
                        Event::saveQuestionAttempt(
5993
                            $this,
5994
                            $questionScore,
5995
                            $val,
5996
                            $quesId,
5997
                            $exeId,
5998
                            $j,
5999
                            $this->id,
6000
                            false,
6001
                            $questionDuration
6002
                        );
6003
                    }
6004
                }
6005
            } elseif (FREE_ANSWER == $answerType) {
6006
                $answer = $choice;
6007
                Event::saveQuestionAttempt(
6008
                    $this,
6009
                    $questionScore,
6010
                    $answer,
6011
                    $quesId,
6012
                    $exeId,
6013
                    0,
6014
                    $this->id,
6015
                    false,
6016
                    $questionDuration
6017
                );
6018
            } elseif (ORAL_EXPRESSION == $answerType) {
6019
                $answer = $choice;
6020
                /** @var OralExpression $objQuestionTmp */
6021
                $questionAttemptId = Event::saveQuestionAttempt(
6022
                    $this,
6023
                    $questionScore,
6024
                    $answer,
6025
                    $quesId,
6026
                    $exeId,
6027
                    0,
6028
                    $this->id,
6029
                    false,
6030
                    $questionDuration
6031
                );
6032
6033
                if (false !== $questionAttemptId) {
6034
                    OralExpression::saveAssetInQuestionAttempt($questionAttemptId);
6035
                }
6036
            } elseif (
6037
            in_array(
6038
                $answerType,
6039
                [UNIQUE_ANSWER, UNIQUE_ANSWER_IMAGE, UNIQUE_ANSWER_NO_OPTION, READING_COMPREHENSION]
6040
            )
6041
            ) {
6042
                $answer = $choice;
6043
                Event::saveQuestionAttempt(
6044
                    $this,
6045
                    $questionScore,
6046
                    $answer,
6047
                    $quesId,
6048
                    $exeId,
6049
                    0,
6050
                    $this->id,
6051
                    false,
6052
                    $questionDuration
6053
                );
6054
            } elseif (HOT_SPOT == $answerType || ANNOTATION == $answerType) {
6055
                $answer = [];
6056
                if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) {
6057
                    if ($debug) {
6058
                        error_log('Checking result coordinates');
6059
                    }
6060
                    Database::delete(
6061
                        Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
6062
                        [
6063
                            'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
6064
                                $exeId,
6065
                                $questionId,
6066
                                api_get_course_int_id(),
6067
                            ],
6068
                        ]
6069
                    );
6070
6071
                    foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
6072
                        $answer[] = $val;
6073
                        $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
6074
                        if ($debug) {
6075
                            error_log('Hotspot value: '.$hotspotValue);
6076
                        }
6077
                        Event::saveExerciseAttemptHotspot(
6078
                            $this,
6079
                            $exeId,
6080
                            $quesId,
6081
                            $idx,
6082
                            $hotspotValue,
6083
                            $val,
6084
                            false,
6085
                            $this->id,
6086
                            $learnpath_id,
6087
                            $learnpath_item_id
6088
                        );
6089
                    }
6090
                } else {
6091
                    if ($debug) {
6092
                        error_log('Empty: exerciseResultCoordinates');
6093
                    }
6094
                }
6095
                Event::saveQuestionAttempt(
6096
                    $this,
6097
                    $questionScore,
6098
                    implode('|', $answer),
6099
                    $quesId,
6100
                    $exeId,
6101
                    0,
6102
                    $this->id,
6103
                    false,
6104
                    $questionDuration
6105
                );
6106
            } else {
6107
                Event::saveQuestionAttempt(
6108
                    $this,
6109
                    $questionScore,
6110
                    $answer,
6111
                    $quesId,
6112
                    $exeId,
6113
                    0,
6114
                    $this->id,
6115
                    false,
6116
                    $questionDuration
6117
                );
6118
            }
6119
        }
6120
6121
        if (0 == $propagate_neg && $questionScore < 0) {
6122
            $questionScore = 0;
6123
        }
6124
6125
        if ($save_results) {
6126
            $statsTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6127
            $sql = "UPDATE $statsTable SET
6128
                        score = score + ".(float) $questionScore."
6129
                    WHERE exe_id = $exeId";
6130
            Database::query($sql);
6131
        }
6132
6133
        return [
6134
            'score' => $questionScore,
6135
            'weight' => $questionWeighting,
6136
            'extra' => $extra_data,
6137
            'open_question' => $arrques,
6138
            'open_answer' => $arrans,
6139
            'answer_type' => $answerType,
6140
            'generated_oral_file' => '',
6141
            'user_answered' => $userAnsweredQuestion,
6142
            'correct_answer_id' => $correctAnswerId,
6143
            'answer_destination' => $answerDestination,
6144
        ];
6145
    }
6146
6147
    /**
6148
     * Sends a notification when a user ends an examn.
6149
     *
6150
     * @param string $type                  'start' or 'end' of an exercise
6151
     * @param array  $question_list_answers
6152
     * @param string $origin
6153
     * @param int    $exe_id
6154
     * @param float  $score
6155
     * @param float  $weight
6156
     *
6157
     * @return bool
6158
     */
6159
    public function send_mail_notification_for_exam(
6160
        $type,
6161
        $question_list_answers,
6162
        $origin,
6163
        $exe_id,
6164
        $score = null,
6165
        $weight = null
6166
    ) {
6167
        $setting = api_get_course_setting('email_alert_manager_on_new_quiz');
6168
6169
        if (empty($setting) && empty($this->getNotifications())) {
6170
            return false;
6171
        }
6172
6173
        $settingFromExercise = $this->getNotifications();
6174
        if (!empty($settingFromExercise)) {
6175
            $setting = $settingFromExercise;
6176
        }
6177
6178
        // Email configuration settings
6179
        $courseCode = api_get_course_id();
6180
        $courseInfo = api_get_course_info($courseCode);
6181
6182
        if (empty($courseInfo)) {
6183
            return false;
6184
        }
6185
6186
        $sessionId = api_get_session_id();
6187
6188
        $sessionData = '';
6189
        if (!empty($sessionId)) {
6190
            $sessionInfo = api_get_session_info($sessionId);
6191
            if (!empty($sessionInfo)) {
6192
                $sessionData = '<tr>'
6193
                    .'<td>'.get_lang('Session name').'</td>'
6194
                    .'<td>'.$sessionInfo['name'].'</td>'
6195
                    .'</tr>';
6196
            }
6197
        }
6198
6199
        $sendStart = false;
6200
        $sendEnd = false;
6201
        $sendEndOpenQuestion = false;
6202
        $sendEndOralQuestion = false;
6203
6204
        foreach ($setting as $option) {
6205
            switch ($option) {
6206
                case 0:
6207
                    return false;
6208
6209
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

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

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

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

Loading history...
6210
                case 1: // End
6211
                    if ('end' == $type) {
6212
                        $sendEnd = true;
6213
                    }
6214
6215
                    break;
6216
                case 2: // start
6217
                    if ('start' == $type) {
6218
                        $sendStart = true;
6219
                    }
6220
6221
                    break;
6222
                case 3: // end + open
6223
                    if ('end' == $type) {
6224
                        $sendEndOpenQuestion = true;
6225
                    }
6226
6227
                    break;
6228
                case 4: // end + oral
6229
                    if ('end' == $type) {
6230
                        $sendEndOralQuestion = true;
6231
                    }
6232
6233
                    break;
6234
            }
6235
        }
6236
6237
        $user_info = api_get_user_info(api_get_user_id());
6238
        $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_show.php?'.
6239
            api_get_cidreq(true, true, 'qualify').'&id='.$exe_id.'&action=qualify';
6240
6241
        if (!empty($sessionId)) {
6242
            $addGeneralCoach = true;
6243
            $setting = ('true' === api_get_setting('exercise.block_quiz_mail_notification_general_coach'));
6244
            if (true === $setting) {
6245
                $addGeneralCoach = false;
6246
            }
6247
            $teachers = CourseManager::get_coach_list_from_course_code(
6248
                $courseCode,
6249
                $sessionId,
6250
                $addGeneralCoach
6251
            );
6252
        } else {
6253
            $teachers = CourseManager::get_teacher_list_from_course_code($courseCode);
6254
        }
6255
6256
        if ($sendEndOpenQuestion) {
6257
            $this->sendNotificationForOpenQuestions(
6258
                $question_list_answers,
6259
                $origin,
6260
                $user_info,
6261
                $url,
6262
                $teachers
6263
            );
6264
        }
6265
6266
        if ($sendEndOralQuestion) {
6267
            $this->sendNotificationForOralQuestions(
6268
                $question_list_answers,
6269
                $origin,
6270
                $exe_id,
6271
                $user_info,
6272
                $url,
6273
                $teachers
6274
            );
6275
        }
6276
6277
        if (!$sendEnd && !$sendStart) {
6278
            return false;
6279
        }
6280
6281
        $scoreLabel = '';
6282
        if ($sendEnd &&
6283
            ('true' === api_get_setting('exercise.send_score_in_exam_notification_mail_to_manager'))
6284
        ) {
6285
            $notificationPercentage = ('true' === api_get_setting('mail.send_notification_score_in_percentage'));
6286
            $scoreLabel = ExerciseLib::show_score($score, $weight, $notificationPercentage, true);
6287
            $scoreLabel = '<tr>
6288
                            <td>'.get_lang('Score')."</td>
6289
                            <td>&nbsp;$scoreLabel</td>
6290
                        </tr>";
6291
        }
6292
6293
        if ($sendEnd) {
6294
            $msg = get_lang('A learner attempted an exercise').'<br /><br />';
6295
        } else {
6296
            $msg = get_lang('Student just started an exercise').'<br /><br />';
6297
        }
6298
6299
        $msg .= get_lang('Attempt details').' : <br /><br />
6300
                    <table>
6301
                        <tr>
6302
                            <td>'.get_lang('Course name').'</td>
6303
                            <td>#course#</td>
6304
                        </tr>
6305
                        '.$sessionData.'
6306
                        <tr>
6307
                            <td>'.get_lang('Test').'</td>
6308
                            <td>&nbsp;#exercise#</td>
6309
                        </tr>
6310
                        <tr>
6311
                            <td>'.get_lang('Learner name').'</td>
6312
                            <td>&nbsp;#student_complete_name#</td>
6313
                        </tr>
6314
                        <tr>
6315
                            <td>'.get_lang('Learner e-mail').'</td>
6316
                            <td>&nbsp;#email#</td>
6317
                        </tr>
6318
                        '.$scoreLabel.'
6319
                    </table>';
6320
6321
        $variables = [
6322
            '#email#' => $user_info['email'],
6323
            '#exercise#' => $this->exercise,
6324
            '#student_complete_name#' => $user_info['complete_name'],
6325
            '#course#' => Display::url(
6326
                $courseInfo['title'],
6327
                $courseInfo['course_public_url'].'?sid='.$sessionId
6328
            ),
6329
        ];
6330
6331
        if ($sendEnd) {
6332
            $msg .= '<br /><a href="#url#">'.get_lang(
6333
                    'Click this link to check the answer and/or give feedback'
6334
                ).'</a>';
6335
            $variables['#url#'] = $url;
6336
        }
6337
6338
        $content = str_replace(array_keys($variables), array_values($variables), $msg);
6339
6340
        if ($sendEnd) {
6341
            $subject = get_lang('A learner attempted an exercise');
6342
        } else {
6343
            $subject = get_lang('Student just started an exercise');
6344
        }
6345
6346
        if (!empty($teachers)) {
6347
            foreach ($teachers as $user_id => $teacher_data) {
6348
                MessageManager::send_message_simple(
6349
                    $user_id,
6350
                    $subject,
6351
                    $content
6352
                );
6353
            }
6354
        }
6355
    }
6356
6357
    /**
6358
     * @param array $user_data         result of api_get_user_info()
6359
     * @param array $trackExerciseInfo result of get_stat_track_exercise_info
6360
     * @param bool  $saveUserResult
6361
     * @param bool  $allowSignature
6362
     * @param bool  $allowExportPdf
6363
     *
6364
     * @return string
6365
     */
6366
    public function showExerciseResultHeader(
6367
        $user_data,
6368
        $trackExerciseInfo,
6369
        $saveUserResult,
6370
        $allowSignature = false,
6371
        $allowExportPdf = false
6372
    ) {
6373
        if ('true' === api_get_setting('exercise.hide_user_info_in_quiz_result')) {
6374
            return '';
6375
        }
6376
6377
        $start_date = null;
6378
        if (isset($trackExerciseInfo['start_date'])) {
6379
            $start_date = api_convert_and_format_date($trackExerciseInfo['start_date']);
6380
        }
6381
        $duration = isset($trackExerciseInfo['duration_formatted']) ? $trackExerciseInfo['duration_formatted'] : null;
6382
        $ip = isset($trackExerciseInfo['user_ip']) ? $trackExerciseInfo['user_ip'] : null;
6383
6384
        if (!empty($user_data)) {
6385
            $userFullName = $user_data['complete_name'];
6386
            if (api_is_teacher() || api_is_platform_admin(true, true)) {
6387
                $userFullName = '<a href="'.$user_data['profile_url'].'" title="'.get_lang('GoToStudentDetails').'">'.
6388
                    $user_data['complete_name'].'</a>';
6389
            }
6390
6391
            $data = [
6392
                'name_url' => $userFullName,
6393
                'complete_name' => $user_data['complete_name'],
6394
                'username' => $user_data['username'],
6395
                'avatar' => $user_data['avatar_medium'],
6396
                'url' => $user_data['profile_url'],
6397
            ];
6398
6399
            if (!empty($user_data['official_code'])) {
6400
                $data['code'] = $user_data['official_code'];
6401
            }
6402
        }
6403
        // Description can be very long and is generally meant to explain
6404
        //   rules *before* the exam. Leaving here to make display easier if
6405
        //   necessary
6406
        /*
6407
        if (!empty($this->description)) {
6408
            $array[] = array('title' => get_lang("Description"), 'content' => $this->description);
6409
        }
6410
        */
6411
6412
        $data['start_date'] = $start_date;
6413
        $data['duration'] = $duration;
6414
        $data['ip'] = $ip;
6415
6416
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
6417
            $data['title'] = $this->get_formated_title().get_lang('Result');
6418
        } else {
6419
            $data['title'] = PHP_EOL.$this->exercise.' : '.get_lang('Result');
6420
        }
6421
6422
        $questionsCount = count(explode(',', $trackExerciseInfo['data_tracking']));
6423
        $savedAnswersCount = $this->countUserAnswersSavedInExercise($trackExerciseInfo['exe_id']);
6424
6425
        $data['number_of_answers'] = $questionsCount;
6426
        $data['number_of_answers_saved'] = $savedAnswersCount;
6427
        $exeId = $trackExerciseInfo['exe_id'];
6428
6429
        if ('true' === api_get_setting('exercise.quiz_confirm_saved_answers')) {
6430
            $em = Database::getManager();
6431
6432
            if ($saveUserResult) {
6433
                $trackConfirmation = new TrackEExerciseConfirmation();
6434
                $trackConfirmation
6435
                    ->setUser(api_get_user_entity($trackExerciseInfo['exe_user_id']))
6436
                    ->setQuizId($trackExerciseInfo['exe_exo_id'])
6437
                    ->setAttemptId($trackExerciseInfo['exe_id'])
6438
                    ->setQuestionsCount($questionsCount)
6439
                    ->setSavedAnswersCount($savedAnswersCount)
6440
                    ->setCourseId($trackExerciseInfo['c_id'])
6441
                    ->setSessionId($trackExerciseInfo['session_id'])
6442
                    ->setCreatedAt(api_get_utc_datetime(null, false, true));
6443
6444
                $em->persist($trackConfirmation);
6445
                $em->flush();
6446
            } else {
6447
                $trackConfirmation = $em
6448
                    ->getRepository(TrackEExerciseConfirmation::class)
6449
                    ->findOneBy(
6450
                        [
6451
                            'attemptId' => $trackExerciseInfo['exe_id'],
6452
                            'quizId' => $trackExerciseInfo['exe_exo_id'],
6453
                            'courseId' => $trackExerciseInfo['c_id'],
6454
                            'sessionId' => $trackExerciseInfo['session_id'],
6455
                        ]
6456
                    );
6457
            }
6458
6459
            $data['track_confirmation'] = $trackConfirmation;
6460
        }
6461
6462
        $signature = '';
6463
        if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($this)) {
6464
            $signature = ExerciseSignaturePlugin::getSignature($trackExerciseInfo['exe_user_id'], $trackExerciseInfo);
6465
        }
6466
        $tpl = new Template(null, false, false, false, false, false, false);
6467
        $tpl->assign('data', $data);
6468
        $tpl->assign('allow_signature', $allowSignature);
6469
        $tpl->assign('signature', $signature);
6470
        $tpl->assign('allow_export_pdf', $allowExportPdf);
6471
        $tpl->assign(
6472
            'export_url',
6473
            api_get_path(WEB_CODE_PATH).'exercise/result.php?action=export&id='.$exeId.'&'.api_get_cidreq()
6474
        );
6475
        $layoutTemplate = $tpl->get_template('exercise/partials/result_exercise.tpl');
6476
6477
        return $tpl->fetch($layoutTemplate);
6478
    }
6479
6480
    /**
6481
     * Returns the exercise result.
6482
     *
6483
     * @param int        attempt id
6484
     *
6485
     * @return array
6486
     */
6487
    public function get_exercise_result($exe_id)
6488
    {
6489
        $result = [];
6490
        $track_exercise_info = ExerciseLib::get_exercise_track_exercise_info($exe_id);
6491
6492
        if (!empty($track_exercise_info)) {
6493
            $totalScore = 0;
6494
            $objExercise = new self();
6495
            $objExercise->read($track_exercise_info['exe_exo_id']);
6496
            if (!empty($track_exercise_info['data_tracking'])) {
6497
                $question_list = explode(',', $track_exercise_info['data_tracking']);
6498
            }
6499
            foreach ($question_list as $questionId) {
6500
                $question_result = $objExercise->manage_answer(
6501
                    $exe_id,
6502
                    $questionId,
6503
                    '',
6504
                    'exercise_show',
6505
                    [],
6506
                    false,
6507
                    true,
6508
                    false,
6509
                    $objExercise->selectPropagateNeg()
6510
                );
6511
                $totalScore += $question_result['score'];
6512
            }
6513
6514
            if (0 == $objExercise->selectPropagateNeg() && $totalScore < 0) {
6515
                $totalScore = 0;
6516
            }
6517
            $result = [
6518
                'score' => $totalScore,
6519
                'weight' => $track_exercise_info['max_score'],
6520
            ];
6521
        }
6522
6523
        return $result;
6524
    }
6525
6526
    /**
6527
     * Checks if the exercise is visible due a lot of conditions
6528
     * visibility, time limits, student attempts
6529
     * Return associative array
6530
     * value : true if exercise visible
6531
     * message : HTML formatted message
6532
     * rawMessage : text message.
6533
     *
6534
     * @param int  $lpId
6535
     * @param int  $lpItemId
6536
     * @param int  $lpItemViewId
6537
     * @param bool $filterByAdmin
6538
     *
6539
     * @return array
6540
     */
6541
    public function is_visible(
6542
        $lpId = 0,
6543
        $lpItemId = 0,
6544
        $lpItemViewId = 0,
6545
        $filterByAdmin = true
6546
    ) {
6547
        // 1. By default the exercise is visible
6548
        $isVisible = true;
6549
        $message = null;
6550
6551
        // 1.1 Admins and teachers can access to the exercise
6552
        if ($filterByAdmin) {
6553
            if (api_is_platform_admin() || api_is_course_admin() || api_is_course_tutor()) {
6554
                return ['value' => true, 'message' => ''];
6555
            }
6556
        }
6557
6558
        // Deleted exercise.
6559
        if (-1 == $this->active) {
6560
            return [
6561
                'value' => false,
6562
                'message' => Display::return_message(
6563
                    get_lang('TestNotFound'),
6564
                    'warning',
6565
                    false
6566
                ),
6567
                'rawMessage' => get_lang('TestNotFound'),
6568
            ];
6569
        }
6570
6571
        $repo = Container::getQuizRepository();
6572
        $exercise = $repo->find($this->iId);
6573
6574
        if (null === $exercise) {
6575
            return [];
6576
        }
6577
6578
        $course = api_get_course_entity($this->course_id);
6579
        $link = $exercise->getFirstResourceLinkFromCourseSession($course);
6580
6581
        if ($link && $link->isDraft()) {
6582
            $this->active = 0;
6583
        }
6584
6585
        // 2. If the exercise is not active.
6586
        if (empty($lpId)) {
6587
            // 2.1 LP is OFF
6588
            if (0 == $this->active) {
6589
                return [
6590
                    'value' => false,
6591
                    'message' => Display::return_message(
6592
                        get_lang('TestNotFound'),
6593
                        'warning',
6594
                        false
6595
                    ),
6596
                    'rawMessage' => get_lang('TestNotFound'),
6597
                ];
6598
            }
6599
        } else {
6600
            $lp = Container::getLpRepository()->find($lpId);
6601
            // 2.1 LP is loaded
6602
            if ($lp && 0 == $this->active &&
6603
                !learnpath::is_lp_visible_for_student($lp, api_get_user_id(), $course)
6604
            ) {
6605
                return [
6606
                    'value' => false,
6607
                    'message' => Display::return_message(
6608
                        get_lang('TestNotFound'),
6609
                        'warning',
6610
                        false
6611
                    ),
6612
                    'rawMessage' => get_lang('TestNotFound'),
6613
                ];
6614
            }
6615
        }
6616
6617
        // 3. We check if the time limits are on
6618
        $limitTimeExists = false;
6619
        if (!empty($this->start_time) || !empty($this->end_time)) {
6620
            $limitTimeExists = true;
6621
        }
6622
6623
        if ($limitTimeExists) {
6624
            $timeNow = time();
6625
            $existsStartDate = false;
6626
            $nowIsAfterStartDate = true;
6627
            $existsEndDate = false;
6628
            $nowIsBeforeEndDate = true;
6629
6630
            if (!empty($this->start_time)) {
6631
                $existsStartDate = true;
6632
            }
6633
6634
            if (!empty($this->end_time)) {
6635
                $existsEndDate = true;
6636
            }
6637
6638
            // check if we are before-or-after end-or-start date
6639
            if ($existsStartDate && $timeNow < api_strtotime($this->start_time, 'UTC')) {
6640
                $nowIsAfterStartDate = false;
6641
            }
6642
6643
            if ($existsEndDate & $timeNow >= api_strtotime($this->end_time, 'UTC')) {
6644
                $nowIsBeforeEndDate = false;
6645
            }
6646
6647
            // lets check all cases
6648
            if ($existsStartDate && !$existsEndDate) {
6649
                // exists start date and dont exists end date
6650
                if ($nowIsAfterStartDate) {
6651
                    // after start date, no end date
6652
                    $isVisible = true;
6653
                    $message = sprintf(
6654
                        get_lang('TestAvailableSinceX'),
6655
                        api_convert_and_format_date($this->start_time)
6656
                    );
6657
                } else {
6658
                    // before start date, no end date
6659
                    $isVisible = false;
6660
                    $message = sprintf(
6661
                        get_lang('TestAvailableFromX'),
6662
                        api_convert_and_format_date($this->start_time)
6663
                    );
6664
                }
6665
            } elseif (!$existsStartDate && $existsEndDate) {
6666
                // doesnt exist start date, exists end date
6667
                if ($nowIsBeforeEndDate) {
6668
                    // before end date, no start date
6669
                    $isVisible = true;
6670
                    $message = sprintf(
6671
                        get_lang('TestAvailableUntilX'),
6672
                        api_convert_and_format_date($this->end_time)
6673
                    );
6674
                } else {
6675
                    // after end date, no start date
6676
                    $isVisible = false;
6677
                    $message = sprintf(
6678
                        get_lang('TestAvailableUntilX'),
6679
                        api_convert_and_format_date($this->end_time)
6680
                    );
6681
                }
6682
            } elseif ($existsStartDate && $existsEndDate) {
6683
                // exists start date and end date
6684
                if ($nowIsAfterStartDate) {
6685
                    if ($nowIsBeforeEndDate) {
6686
                        // after start date and before end date
6687
                        $isVisible = true;
6688
                        $message = sprintf(
6689
                            get_lang('TestIsActivatedFromXToY'),
6690
                            api_convert_and_format_date($this->start_time),
6691
                            api_convert_and_format_date($this->end_time)
6692
                        );
6693
                    } else {
6694
                        // after start date and after end date
6695
                        $isVisible = false;
6696
                        $message = sprintf(
6697
                            get_lang('TestWasActivatedFromXToY'),
6698
                            api_convert_and_format_date($this->start_time),
6699
                            api_convert_and_format_date($this->end_time)
6700
                        );
6701
                    }
6702
                } else {
6703
                    if ($nowIsBeforeEndDate) {
6704
                        // before start date and before end date
6705
                        $isVisible = false;
6706
                        $message = sprintf(
6707
                            get_lang('TestWillBeActivatedFromXToY'),
6708
                            api_convert_and_format_date($this->start_time),
6709
                            api_convert_and_format_date($this->end_time)
6710
                        );
6711
                    }
6712
                    // case before start date and after end date is impossible
6713
                }
6714
            } elseif (!$existsStartDate && !$existsEndDate) {
6715
                // doesnt exist start date nor end date
6716
                $isVisible = true;
6717
                $message = '';
6718
            }
6719
        }
6720
6721
        // 4. We check if the student have attempts
6722
        if ($isVisible) {
6723
            $exerciseAttempts = $this->selectAttempts();
6724
6725
            if ($exerciseAttempts > 0) {
6726
                $attemptCount = Event::get_attempt_count_not_finished(
6727
                    api_get_user_id(),
6728
                    $this->getId(),
6729
                    $lpId,
6730
                    $lpItemId,
6731
                    $lpItemViewId
6732
                );
6733
6734
                if ($attemptCount >= $exerciseAttempts) {
6735
                    $message = sprintf(
6736
                        get_lang('Reachedmax. 20 characters, e.g. <i>INNOV21</i>Attempts'),
6737
                        $this->name,
6738
                        $exerciseAttempts
6739
                    );
6740
                    $isVisible = false;
6741
                } else {
6742
                    // Check blocking exercise.
6743
                    $extraFieldValue = new ExtraFieldValue('exercise');
6744
                    $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
6745
                        $this->iId,
6746
                        'blocking_percentage'
6747
                    );
6748
                    if ($blockExercise && isset($blockExercise['value']) && !empty($blockExercise['value'])) {
6749
                        $blockPercentage = (int) $blockExercise['value'];
6750
                        $userAttempts = Event::getExerciseResultsByUser(
6751
                            api_get_user_id(),
6752
                            $this->iId,
6753
                            $this->course_id,
6754
                            $this->sessionId,
6755
                            $lpId,
6756
                            $lpItemId
6757
                        );
6758
6759
                        if (!empty($userAttempts)) {
6760
                            $currentAttempt = current($userAttempts);
6761
                            if ($currentAttempt['total_percentage'] <= $blockPercentage) {
6762
                                $message = sprintf(
6763
                                    get_lang('ExerciseBlockBecausePercentageX'),
6764
                                    $blockPercentage
6765
                                );
6766
                                $isVisible = false;
6767
                            }
6768
                        }
6769
                    }
6770
                }
6771
            }
6772
        }
6773
6774
        $rawMessage = '';
6775
        if (!empty($message)) {
6776
            $rawMessage = $message;
6777
            $message = Display::return_message($message, 'warning', false);
6778
        }
6779
6780
        return [
6781
            'value' => $isVisible,
6782
            'message' => $message,
6783
            'rawMessage' => $rawMessage,
6784
        ];
6785
    }
6786
6787
    /**
6788
     * @return bool
6789
     */
6790
    public function added_in_lp()
6791
    {
6792
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
6793
        $sql = "SELECT max_score FROM $TBL_LP_ITEM
6794
                WHERE
6795
                    item_type = '".TOOL_QUIZ."' AND
6796
                    path = '{$this->getId()}'";
6797
        $result = Database::query($sql);
6798
        if (Database::num_rows($result) > 0) {
6799
            return true;
6800
        }
6801
6802
        return false;
6803
    }
6804
6805
    /**
6806
     * Returns an array with this form.
6807
     *
6808
     * @return array
6809
     *
6810
     * @example
6811
     * <code>
6812
     * array (size=3)
6813
     * 999 =>
6814
     * array (size=3)
6815
     * 0 => int 3422
6816
     * 1 => int 3423
6817
     * 2 => int 3424
6818
     * 100 =>
6819
     * array (size=2)
6820
     * 0 => int 3469
6821
     * 1 => int 3470
6822
     * 101 =>
6823
     * array (size=1)
6824
     * 0 => int 3482
6825
     * </code>
6826
     * The array inside the key 999 means the question list that belongs to the media id = 999,
6827
     * this case is special because 999 means "no media".
6828
     */
6829
    public function getMediaList()
6830
    {
6831
        return $this->mediaList;
6832
    }
6833
6834
    /**
6835
     * Is media question activated?
6836
     *
6837
     * @return bool
6838
     */
6839
    public function mediaIsActivated()
6840
    {
6841
        $mediaQuestions = $this->getMediaList();
6842
        $active = false;
6843
        if (isset($mediaQuestions) && !empty($mediaQuestions)) {
6844
            $media_count = count($mediaQuestions);
6845
            if ($media_count > 1) {
6846
                return true;
6847
            } elseif (1 == $media_count) {
6848
                if (isset($mediaQuestions[999])) {
6849
                    return false;
6850
                } else {
6851
                    return true;
6852
                }
6853
            }
6854
        }
6855
6856
        return $active;
6857
    }
6858
6859
    /**
6860
     * Gets question list from the exercise.
6861
     *
6862
     * @return array
6863
     */
6864
    public function getQuestionList()
6865
    {
6866
        return $this->questionList;
6867
    }
6868
6869
    /**
6870
     * Question list with medias compressed like this.
6871
     *
6872
     * @return array
6873
     *
6874
     * @example
6875
     *      <code>
6876
     *      array(
6877
     *      question_id_1,
6878
     *      question_id_2,
6879
     *      media_id, <- this media id contains question ids
6880
     *      question_id_3,
6881
     *      )
6882
     *      </code>
6883
     */
6884
    public function getQuestionListWithMediasCompressed()
6885
    {
6886
        return $this->questionList;
6887
    }
6888
6889
    /**
6890
     * Question list with medias uncompressed like this.
6891
     *
6892
     * @return array
6893
     *
6894
     * @example
6895
     *      <code>
6896
     *      array(
6897
     *      question_id,
6898
     *      question_id,
6899
     *      question_id, <- belongs to a media id
6900
     *      question_id, <- belongs to a media id
6901
     *      question_id,
6902
     *      )
6903
     *      </code>
6904
     */
6905
    public function getQuestionListWithMediasUncompressed()
6906
    {
6907
        return $this->questionListUncompressed;
6908
    }
6909
6910
    /**
6911
     * Sets the question list when the exercise->read() is executed.
6912
     *
6913
     * @param bool $adminView Whether to view the set the list of *all* questions or just the normal student view
6914
     */
6915
    public function setQuestionList($adminView = false)
6916
    {
6917
        // Getting question list.
6918
        $questionList = $this->selectQuestionList(true, $adminView);
6919
        $this->setMediaList($questionList);
6920
        $this->questionList = $this->transformQuestionListWithMedias($questionList, false);
6921
        $this->questionListUncompressed = $this->transformQuestionListWithMedias(
6922
            $questionList,
6923
            true
6924
        );
6925
    }
6926
6927
    /**
6928
     * @params array question list
6929
     * @params bool expand or not question list (true show all questions,
6930
     * false show media question id instead of the question ids)
6931
     */
6932
    public function transformQuestionListWithMedias(
6933
        $question_list,
6934
        $expand_media_questions = false
6935
    ) {
6936
        $new_question_list = [];
6937
        if (!empty($question_list)) {
6938
            $media_questions = $this->getMediaList();
6939
            $media_active = $this->mediaIsActivated($media_questions);
6940
6941
            if ($media_active) {
6942
                $counter = 1;
6943
                foreach ($question_list as $question_id) {
6944
                    $add_question = true;
6945
                    foreach ($media_questions as $media_id => $question_list_in_media) {
6946
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
6947
                            $add_question = false;
6948
                            if (!in_array($media_id, $new_question_list)) {
6949
                                $new_question_list[$counter] = $media_id;
6950
                                $counter++;
6951
                            }
6952
6953
                            break;
6954
                        }
6955
                    }
6956
                    if ($add_question) {
6957
                        $new_question_list[$counter] = $question_id;
6958
                        $counter++;
6959
                    }
6960
                }
6961
                if ($expand_media_questions) {
6962
                    $media_key_list = array_keys($media_questions);
6963
                    foreach ($new_question_list as &$question_id) {
6964
                        if (in_array($question_id, $media_key_list)) {
6965
                            $question_id = $media_questions[$question_id];
6966
                        }
6967
                    }
6968
                    $new_question_list = array_flatten($new_question_list);
6969
                }
6970
            } else {
6971
                $new_question_list = $question_list;
6972
            }
6973
        }
6974
6975
        return $new_question_list;
6976
    }
6977
6978
    /**
6979
     * Get question list depend on the random settings.
6980
     *
6981
     * @return array
6982
     */
6983
    public function get_validated_question_list()
6984
    {
6985
        $isRandomByCategory = $this->isRandomByCat();
6986
        if (0 == $isRandomByCategory) {
6987
            if ($this->isRandom()) {
6988
                return $this->getRandomList();
6989
            }
6990
6991
            return $this->selectQuestionList();
6992
        }
6993
6994
        if ($this->isRandom()) {
6995
            // USE question categories
6996
            // get questions by category for this exercise
6997
            // we have to choice $objExercise->random question in each array values of $tabCategoryQuestions
6998
            // key of $tabCategoryQuestions are the categopy id (0 for not in a category)
6999
            // value is the array of question id of this category
7000
            $questionList = [];
7001
            $categoryQuestions = TestCategory::getQuestionsByCat($this->id);
7002
            $isRandomByCategory = $this->getRandomByCategory();
7003
            // We sort categories based on the term between [] in the head
7004
            // of the category's description
7005
            /* examples of categories :
7006
             * [biologie] Maitriser les mecanismes de base de la genetique
7007
             * [biologie] Relier les moyens de depenses et les agents infectieux
7008
             * [biologie] Savoir ou est produite l'enrgie dans les cellules et sous quelle forme
7009
             * [chimie] Classer les molles suivant leur pouvoir oxydant ou reacteur
7010
             * [chimie] Connaître la denition de la theoie acide/base selon Brönsted
7011
             * [chimie] Connaître les charges des particules
7012
             * We want that in the order of the groups defined by the term
7013
             * between brackets at the beginning of the category title
7014
            */
7015
            // If test option is Grouped By Categories
7016
            if (2 == $isRandomByCategory) {
7017
                $categoryQuestions = TestCategory::sortTabByBracketLabel($categoryQuestions);
7018
            }
7019
            foreach ($categoryQuestions as $question) {
7020
                $number_of_random_question = $this->random;
7021
                if (-1 == $this->random) {
7022
                    $number_of_random_question = count($this->questionList);
7023
                }
7024
                $questionList = array_merge(
7025
                    $questionList,
7026
                    TestCategory::getNElementsFromArray(
7027
                        $question,
7028
                        $number_of_random_question
7029
                    )
7030
                );
7031
            }
7032
            // shuffle the question list if test is not grouped by categories
7033
            if (1 == $isRandomByCategory) {
7034
                shuffle($questionList); // or not
7035
            }
7036
7037
            return $questionList;
7038
        }
7039
7040
        // Problem, random by category has been selected and
7041
        // we have no $this->isRandom number of question selected
7042
        // Should not happened
7043
7044
        return [];
7045
    }
7046
7047
    public function get_question_list($expand_media_questions = false)
7048
    {
7049
        $question_list = $this->get_validated_question_list();
7050
        $question_list = $this->transform_question_list_with_medias($question_list, $expand_media_questions);
7051
7052
        return $question_list;
7053
    }
7054
7055
    public function transform_question_list_with_medias($question_list, $expand_media_questions = false)
7056
    {
7057
        $new_question_list = [];
7058
        if (!empty($question_list)) {
7059
            $media_questions = $this->getMediaList();
7060
            $media_active = $this->mediaIsActivated($media_questions);
7061
7062
            if ($media_active) {
7063
                $counter = 1;
7064
                foreach ($question_list as $question_id) {
7065
                    $add_question = true;
7066
                    foreach ($media_questions as $media_id => $question_list_in_media) {
7067
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
7068
                            $add_question = false;
7069
                            if (!in_array($media_id, $new_question_list)) {
7070
                                $new_question_list[$counter] = $media_id;
7071
                                $counter++;
7072
                            }
7073
7074
                            break;
7075
                        }
7076
                    }
7077
                    if ($add_question) {
7078
                        $new_question_list[$counter] = $question_id;
7079
                        $counter++;
7080
                    }
7081
                }
7082
                if ($expand_media_questions) {
7083
                    $media_key_list = array_keys($media_questions);
7084
                    foreach ($new_question_list as &$question_id) {
7085
                        if (in_array($question_id, $media_key_list)) {
7086
                            $question_id = $media_questions[$question_id];
7087
                        }
7088
                    }
7089
                    $new_question_list = array_flatten($new_question_list);
7090
                }
7091
            } else {
7092
                $new_question_list = $question_list;
7093
            }
7094
        }
7095
7096
        return $new_question_list;
7097
    }
7098
7099
    /**
7100
     * @param int $exe_id
7101
     *
7102
     * @return array
7103
     */
7104
    public function get_stat_track_exercise_info_by_exe_id($exe_id)
7105
    {
7106
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7107
        $exe_id = (int) $exe_id;
7108
        $sql_track = "SELECT * FROM $table WHERE exe_id = $exe_id ";
7109
        $result = Database::query($sql_track);
7110
        $new_array = [];
7111
        if (Database::num_rows($result) > 0) {
7112
            $new_array = Database::fetch_array($result, 'ASSOC');
7113
            $start_date = api_get_utc_datetime($new_array['start_date'], true);
7114
            $end_date = api_get_utc_datetime($new_array['exe_date'], true);
7115
            $new_array['duration_formatted'] = '';
7116
            if (!empty($new_array['exe_duration']) && !empty($start_date) && !empty($end_date)) {
7117
                $time = api_format_time($new_array['exe_duration'], 'js');
7118
                $new_array['duration_formatted'] = $time;
7119
            }
7120
        }
7121
7122
        return $new_array;
7123
    }
7124
7125
    /**
7126
     * @param int $exeId
7127
     *
7128
     * @return bool
7129
     */
7130
    public function removeAllQuestionToRemind($exeId)
7131
    {
7132
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7133
        $exeId = (int) $exeId;
7134
        if (empty($exeId)) {
7135
            return false;
7136
        }
7137
        $sql = "UPDATE $table
7138
                SET questions_to_check = ''
7139
                WHERE exe_id = $exeId ";
7140
        Database::query($sql);
7141
7142
        return true;
7143
    }
7144
7145
    /**
7146
     * @param int   $exeId
7147
     * @param array $questionList
7148
     *
7149
     * @return bool
7150
     */
7151
    public function addAllQuestionToRemind($exeId, $questionList = [])
7152
    {
7153
        $exeId = (int) $exeId;
7154
        if (empty($questionList)) {
7155
            return false;
7156
        }
7157
7158
        $questionListToString = implode(',', $questionList);
7159
        $questionListToString = Database::escape_string($questionListToString);
7160
7161
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7162
        $sql = "UPDATE $table
7163
                SET questions_to_check = '$questionListToString'
7164
                WHERE exe_id = $exeId";
7165
        Database::query($sql);
7166
7167
        return true;
7168
    }
7169
7170
    /**
7171
     * @param int    $exeId
7172
     * @param int    $questionId
7173
     * @param string $action
7174
     */
7175
    public function editQuestionToRemind($exeId, $questionId, $action = 'add')
7176
    {
7177
        $exercise_info = self::get_stat_track_exercise_info_by_exe_id($exeId);
7178
        $questionId = (int) $questionId;
7179
        $exeId = (int) $exeId;
7180
7181
        if ($exercise_info) {
7182
            $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7183
            if (empty($exercise_info['questions_to_check'])) {
7184
                if ('add' == $action) {
7185
                    $sql = "UPDATE $track_exercises
7186
                            SET questions_to_check = '$questionId'
7187
                            WHERE exe_id = $exeId ";
7188
                    Database::query($sql);
7189
                }
7190
            } else {
7191
                $remind_list = explode(',', $exercise_info['questions_to_check']);
7192
                $remind_list_string = '';
7193
                if ('add' === $action) {
7194
                    if (!in_array($questionId, $remind_list)) {
7195
                        $newRemindList = [];
7196
                        $remind_list[] = $questionId;
7197
                        $questionListInSession = Session::read('questionList');
7198
                        if (!empty($questionListInSession)) {
7199
                            foreach ($questionListInSession as $originalQuestionId) {
7200
                                if (in_array($originalQuestionId, $remind_list)) {
7201
                                    $newRemindList[] = $originalQuestionId;
7202
                                }
7203
                            }
7204
                        }
7205
                        $remind_list_string = implode(',', $newRemindList);
7206
                    }
7207
                } elseif ('delete' == $action) {
7208
                    if (!empty($remind_list)) {
7209
                        if (in_array($questionId, $remind_list)) {
7210
                            $remind_list = array_flip($remind_list);
7211
                            unset($remind_list[$questionId]);
7212
                            $remind_list = array_flip($remind_list);
7213
7214
                            if (!empty($remind_list)) {
7215
                                sort($remind_list);
7216
                                array_filter($remind_list);
7217
                                $remind_list_string = implode(',', $remind_list);
7218
                            }
7219
                        }
7220
                    }
7221
                }
7222
                $value = Database::escape_string($remind_list_string);
7223
                $sql = "UPDATE $track_exercises
7224
                        SET questions_to_check = '$value'
7225
                        WHERE exe_id = $exeId ";
7226
                Database::query($sql);
7227
            }
7228
        }
7229
    }
7230
7231
    /**
7232
     * @param string $answer
7233
     */
7234
    public function fill_in_blank_answer_to_array($answer)
7235
    {
7236
        $list = null;
7237
        api_preg_match_all('/\[[^]]+\]/', $answer, $list);
7238
7239
        if (empty($list)) {
7240
            return '';
7241
        }
7242
7243
        return $list[0];
7244
    }
7245
7246
    /**
7247
     * @param string $answer
7248
     *
7249
     * @return string
7250
     */
7251
    public function fill_in_blank_answer_to_string($answer)
7252
    {
7253
        $teacher_answer_list = $this->fill_in_blank_answer_to_array($answer);
7254
        $result = '';
7255
        if (!empty($teacher_answer_list)) {
7256
            foreach ($teacher_answer_list as $teacher_item) {
7257
                //Cleaning student answer list
7258
                $value = strip_tags($teacher_item);
7259
                $value = api_substr($value, 1, api_strlen($value) - 2);
7260
                $value = explode('/', $value);
7261
                if (!empty($value[0])) {
7262
                    $value = trim($value[0]);
7263
                    $value = str_replace('&nbsp;', '', $value);
7264
                    $result .= $value;
7265
                }
7266
            }
7267
        }
7268
7269
        return $result;
7270
    }
7271
7272
    /**
7273
     * @return string
7274
     */
7275
    public function returnTimeLeftDiv()
7276
    {
7277
        $html = '<div id="clock_warning" style="display:none">';
7278
        $html .= Display::return_message(
7279
            get_lang('Time limit reached'),
7280
            'warning'
7281
        );
7282
        $html .= ' ';
7283
        $html .= sprintf(
7284
            get_lang('Just a moment, please. You will be redirected in %s seconds...'),
7285
            '<span id="counter_to_redirect" class="red_alert"></span>'
7286
        );
7287
        $html .= '</div>';
7288
        $icon = Display::getMdiIcon('clock-outline', 'ch-tool-icon');
7289
        $html .= '<div class="count_down">
7290
                    '.get_lang('RemainingTimeToFinishExercise').'
7291
                    '.$icon.'<span id="exercise_clock_warning"></span>
7292
                </div>';
7293
7294
        return $html;
7295
    }
7296
7297
    /**
7298
     * Get categories added in the exercise--category matrix.
7299
     *
7300
     * @return array
7301
     */
7302
    public function getCategoriesInExercise()
7303
    {
7304
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7305
        if (!empty($this->getId())) {
7306
            $sql = "SELECT * FROM $table
7307
                    WHERE exercise_id = {$this->getId()} ";
7308
            $result = Database::query($sql);
7309
            $list = [];
7310
            if (Database::num_rows($result)) {
7311
                while ($row = Database::fetch_array($result, 'ASSOC')) {
7312
                    $list[$row['category_id']] = $row;
7313
                }
7314
7315
                return $list;
7316
            }
7317
        }
7318
7319
        return [];
7320
    }
7321
7322
    /**
7323
     * Get total number of question that will be parsed when using the category/exercise.
7324
     *
7325
     * @return int
7326
     */
7327
    public function getNumberQuestionExerciseCategory()
7328
    {
7329
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7330
        if (!empty($this->getId())) {
7331
            $sql = "SELECT SUM(count_questions) count_questions
7332
                    FROM $table
7333
                    WHERE exercise_id = {$this->getId()}";
7334
            $result = Database::query($sql);
7335
            if (Database::num_rows($result)) {
7336
                $row = Database::fetch_array($result);
7337
7338
                return (int) $row['count_questions'];
7339
            }
7340
        }
7341
7342
        return 0;
7343
    }
7344
7345
    /**
7346
     * Save categories in the TABLE_QUIZ_REL_CATEGORY table
7347
     *
7348
     * @param array $categories
7349
     */
7350
    public function saveCategoriesInExercise($categories)
7351
    {
7352
        if (!empty($categories) && !empty($this->getId())) {
7353
            $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7354
            $sql = "DELETE FROM $table
7355
                    WHERE exercise_id = {$this->getId()}";
7356
            Database::query($sql);
7357
            foreach ($categories as $categoryId => $countQuestions) {
7358
                if ($categoryId !== 0) {
7359
                    $params = [
7360
                        'exercise_id' => $this->getId(),
7361
                        'category_id' => $categoryId,
7362
                        'count_questions' => $countQuestions,
7363
                    ];
7364
                    Database::insert($table, $params);
7365
                }
7366
            }
7367
        }
7368
    }
7369
7370
    /**
7371
     * @param array  $questionList
7372
     * @param int    $currentQuestion
7373
     * @param array  $conditions
7374
     * @param string $link
7375
     *
7376
     * @return string
7377
     */
7378
    public function progressExercisePaginationBar(
7379
        $questionList,
7380
        $currentQuestion,
7381
        $conditions,
7382
        $link
7383
    ) {
7384
        $mediaQuestions = $this->getMediaList();
7385
7386
        $html = '<div class="exercise_pagination pagination pagination-mini"><ul>';
7387
        $counter = 0;
7388
        $nextValue = 0;
7389
        $wasMedia = false;
7390
        $before = 0;
7391
        $counterNoMedias = 0;
7392
        foreach ($questionList as $questionId) {
7393
            $isCurrent = $currentQuestion == $counterNoMedias + 1 ? true : false;
7394
7395
            if (!empty($nextValue)) {
7396
                if ($wasMedia) {
7397
                    $nextValue = $nextValue - $before + 1;
7398
                }
7399
            }
7400
7401
            if (isset($mediaQuestions) && isset($mediaQuestions[$questionId])) {
7402
                $fixedValue = $counterNoMedias;
7403
7404
                $html .= Display::progressPaginationBar(
7405
                    $nextValue,
7406
                    $mediaQuestions[$questionId],
7407
                    $currentQuestion,
7408
                    $fixedValue,
7409
                    $conditions,
7410
                    $link,
7411
                    true,
7412
                    true
7413
                );
7414
7415
                $counter += count($mediaQuestions[$questionId]) - 1;
7416
                $before = count($questionList);
7417
                $wasMedia = true;
7418
                $nextValue += count($questionList);
7419
            } else {
7420
                $html .= Display::parsePaginationItem(
7421
                    $questionId,
7422
                    $isCurrent,
7423
                    $conditions,
7424
                    $link,
7425
                    $counter
7426
                );
7427
                $counter++;
7428
                $nextValue++;
7429
                $wasMedia = false;
7430
            }
7431
            $counterNoMedias++;
7432
        }
7433
        $html .= '</ul></div>';
7434
7435
        return $html;
7436
    }
7437
7438
    /**
7439
     *  Shows a list of numbers that represents the question to answer in a exercise.
7440
     *
7441
     * @param array  $categories
7442
     * @param int    $current
7443
     * @param array  $conditions
7444
     * @param string $link
7445
     *
7446
     * @return string
7447
     */
7448
    public function progressExercisePaginationBarWithCategories(
7449
        $categories,
7450
        $current,
7451
        $conditions = [],
7452
        $link = null
7453
    ) {
7454
        $html = null;
7455
        $counterNoMedias = 0;
7456
        $nextValue = 0;
7457
        $wasMedia = false;
7458
        $before = 0;
7459
7460
        if (!empty($categories)) {
7461
            $selectionType = $this->getQuestionSelectionType();
7462
            $useRootAsCategoryTitle = false;
7463
7464
            // Grouping questions per parent category see BT#6540
7465
            if (in_array(
7466
                $selectionType,
7467
                [
7468
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED,
7469
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM,
7470
                ]
7471
            )) {
7472
                $useRootAsCategoryTitle = true;
7473
            }
7474
7475
            // If the exercise is set to only show the titles of the categories
7476
            // at the root of the tree, then pre-order the categories tree by
7477
            // removing children and summing their questions into the parent
7478
            // categories
7479
            if ($useRootAsCategoryTitle) {
7480
                // The new categories list starts empty
7481
                $newCategoryList = [];
7482
                foreach ($categories as $category) {
7483
                    $rootElement = $category['root'];
7484
7485
                    if (isset($category['parent_info'])) {
7486
                        $rootElement = $category['parent_info']['id'];
7487
                    }
7488
7489
                    //$rootElement = $category['id'];
7490
                    // If the current category's ancestor was never seen
7491
                    // before, then declare it and assign the current
7492
                    // category to it.
7493
                    if (!isset($newCategoryList[$rootElement])) {
7494
                        $newCategoryList[$rootElement] = $category;
7495
                    } else {
7496
                        // If it was already seen, then merge the previous with
7497
                        // the current category
7498
                        $oldQuestionList = $newCategoryList[$rootElement]['question_list'];
7499
                        $category['question_list'] = array_merge($oldQuestionList, $category['question_list']);
7500
                        $newCategoryList[$rootElement] = $category;
7501
                    }
7502
                }
7503
                // Now use the newly built categories list, with only parents
7504
                $categories = $newCategoryList;
7505
            }
7506
7507
            foreach ($categories as $category) {
7508
                $questionList = $category['question_list'];
7509
                // Check if in this category there questions added in a media
7510
                $mediaQuestionId = $category['media_question'];
7511
                $isMedia = false;
7512
                $fixedValue = null;
7513
7514
                // Media exists!
7515
                if (999 != $mediaQuestionId) {
7516
                    $isMedia = true;
7517
                    $fixedValue = $counterNoMedias;
7518
                }
7519
7520
                //$categoryName = $category['path']; << show the path
7521
                $categoryName = $category['name'];
7522
7523
                if ($useRootAsCategoryTitle) {
7524
                    if (isset($category['parent_info'])) {
7525
                        $categoryName = $category['parent_info']['title'];
7526
                    }
7527
                }
7528
                $html .= '<div class="row">';
7529
                $html .= '<div class="span2">'.$categoryName.'</div>';
7530
                $html .= '<div class="span8">';
7531
7532
                if (!empty($nextValue)) {
7533
                    if ($wasMedia) {
7534
                        $nextValue = $nextValue - $before + 1;
7535
                    }
7536
                }
7537
                $html .= Display::progressPaginationBar(
7538
                    $nextValue,
7539
                    $questionList,
7540
                    $current,
7541
                    $fixedValue,
7542
                    $conditions,
7543
                    $link,
7544
                    $isMedia,
7545
                    true
7546
                );
7547
                $html .= '</div>';
7548
                $html .= '</div>';
7549
7550
                if (999 == $mediaQuestionId) {
7551
                    $counterNoMedias += count($questionList);
7552
                } else {
7553
                    $counterNoMedias++;
7554
                }
7555
7556
                $nextValue += count($questionList);
7557
                $before = count($questionList);
7558
7559
                if (999 != $mediaQuestionId) {
7560
                    $wasMedia = true;
7561
                } else {
7562
                    $wasMedia = false;
7563
                }
7564
            }
7565
        }
7566
7567
        return $html;
7568
    }
7569
7570
    /**
7571
     * Renders a question list.
7572
     *
7573
     * @param array $questionList    (with media questions compressed)
7574
     * @param int   $currentQuestion
7575
     * @param array $exerciseResult
7576
     * @param array $attemptList
7577
     * @param array $remindList
7578
     */
7579
    public function renderQuestionList(
7580
        $questionList,
7581
        $currentQuestion,
7582
        $exerciseResult,
7583
        $attemptList,
7584
        $remindList
7585
    ) {
7586
        $mediaQuestions = $this->getMediaList();
7587
        $i = 0;
7588
7589
        // Normal question list render (medias compressed)
7590
        foreach ($questionList as $questionId) {
7591
            $i++;
7592
            // For sequential exercises
7593
7594
            if (ONE_PER_PAGE == $this->type) {
7595
                // If it is not the right question, goes to the next loop iteration
7596
                if ($currentQuestion != $i) {
7597
                    continue;
7598
                } else {
7599
                    if (!in_array(
7600
                        $this->getFeedbackType(),
7601
                        [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
7602
                    )) {
7603
                        // if the user has already answered this question
7604
                        if (isset($exerciseResult[$questionId])) {
7605
                            echo Display::return_message(
7606
                                get_lang('You already answered the question'),
7607
                                'normal'
7608
                            );
7609
7610
                            break;
7611
                        }
7612
                    }
7613
                }
7614
            }
7615
7616
            // The $questionList contains the media id we check
7617
            // if this questionId is a media question type
7618
            if (isset($mediaQuestions[$questionId]) &&
7619
                999 != $mediaQuestions[$questionId]
7620
            ) {
7621
                // The question belongs to a media
7622
                $mediaQuestionList = $mediaQuestions[$questionId];
7623
                $objQuestionTmp = Question::read($questionId);
7624
7625
                $counter = 1;
7626
                if (MEDIA_QUESTION == $objQuestionTmp->type) {
7627
                    echo $objQuestionTmp->show_media_content();
7628
7629
                    $countQuestionsInsideMedia = count($mediaQuestionList);
7630
7631
                    // Show questions that belongs to a media
7632
                    if (!empty($mediaQuestionList)) {
7633
                        // In order to parse media questions we use letters a, b, c, etc.
7634
                        $letterCounter = 97;
7635
                        foreach ($mediaQuestionList as $questionIdInsideMedia) {
7636
                            $isLastQuestionInMedia = false;
7637
                            if ($counter == $countQuestionsInsideMedia) {
7638
                                $isLastQuestionInMedia = true;
7639
                            }
7640
                            $this->renderQuestion(
7641
                                $questionIdInsideMedia,
7642
                                $attemptList,
7643
                                $remindList,
7644
                                chr($letterCounter),
7645
                                $currentQuestion,
7646
                                $mediaQuestionList,
7647
                                $isLastQuestionInMedia,
7648
                                $questionList
7649
                            );
7650
                            $letterCounter++;
7651
                            $counter++;
7652
                        }
7653
                    }
7654
                } else {
7655
                    $this->renderQuestion(
7656
                        $questionId,
7657
                        $attemptList,
7658
                        $remindList,
7659
                        $i,
7660
                        $currentQuestion,
7661
                        null,
7662
                        null,
7663
                        $questionList
7664
                    );
7665
                    $i++;
7666
                }
7667
            } else {
7668
                // Normal question render.
7669
                $this->renderQuestion(
7670
                    $questionId,
7671
                    $attemptList,
7672
                    $remindList,
7673
                    $i,
7674
                    $currentQuestion,
7675
                    null,
7676
                    null,
7677
                    $questionList
7678
                );
7679
            }
7680
7681
            // For sequential exercises.
7682
            if (ONE_PER_PAGE == $this->type) {
7683
                // quits the loop
7684
                break;
7685
            }
7686
        }
7687
        // end foreach()
7688
7689
        if (ALL_ON_ONE_PAGE == $this->type) {
7690
            $exercise_actions = $this->show_button($questionId, $currentQuestion);
7691
            echo Display::div($exercise_actions, ['class' => 'exercise_actions']);
7692
        }
7693
    }
7694
7695
    /**
7696
     * Not implemented in 1.11.x.
7697
     *
7698
     * @param int   $questionId
7699
     * @param array $attemptList
7700
     * @param array $remindList
7701
     * @param int   $i
7702
     * @param int   $current_question
7703
     * @param array $questions_in_media
7704
     * @param bool  $last_question_in_media
7705
     * @param array $realQuestionList
7706
     * @param bool  $generateJS
7707
     */
7708
    public function renderQuestion(
7709
        $questionId,
7710
        $attemptList,
7711
        $remindList,
7712
        $i,
7713
        $current_question,
7714
        $questions_in_media = [],
7715
        $last_question_in_media = false,
7716
        $realQuestionList = [],
7717
        $generateJS = true
7718
    ) {
7719
        // With this option on the question is loaded via AJAX
7720
        //$generateJS = true;
7721
        //$this->loadQuestionAJAX = true;
7722
7723
        if ($generateJS && $this->loadQuestionAJAX) {
7724
            $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?a=get_question&id='.$questionId.'&'.api_get_cidreq();
7725
            $params = [
7726
                'questionId' => $questionId,
7727
                'attemptList' => $attemptList,
7728
                'remindList' => $remindList,
7729
                'i' => $i,
7730
                'current_question' => $current_question,
7731
                'questions_in_media' => $questions_in_media,
7732
                'last_question_in_media' => $last_question_in_media,
7733
            ];
7734
            $params = json_encode($params);
7735
7736
            $script = '<script>
7737
            $(function(){
7738
                var params = '.$params.';
7739
                $.ajax({
7740
                    type: "GET",
7741
                    data: params,
7742
                    url: "'.$url.'",
7743
                    success: function(return_value) {
7744
                        $("#ajaxquestiondiv'.$questionId.'").html(return_value);
7745
                    }
7746
                });
7747
            });
7748
            </script>
7749
            <div id="ajaxquestiondiv'.$questionId.'"></div>';
7750
            echo $script;
7751
        } else {
7752
            $origin = api_get_origin();
7753
            $question_obj = Question::read($questionId);
7754
            $user_choice = isset($attemptList[$questionId]) ? $attemptList[$questionId] : null;
7755
            $remind_highlight = null;
7756
7757
            // Hides questions when reviewing a ALL_ON_ONE_PAGE exercise
7758
            // see #4542 no_remind_highlight class hide with jquery
7759
            if (ALL_ON_ONE_PAGE == $this->type && isset($_GET['reminder']) && 2 == $_GET['reminder']) {
7760
                $remind_highlight = 'no_remind_highlight';
7761
                // @todo not implemented in 1.11.x
7762
                /*if (in_array($question_obj->type, Question::question_type_no_review())) {
7763
                    return null;
7764
                }*/
7765
            }
7766
7767
            $attributes = ['id' => 'remind_list['.$questionId.']'];
7768
7769
            // Showing the question
7770
            $exercise_actions = null;
7771
            echo '<a id="questionanchor'.$questionId.'"></a><br />';
7772
            echo '<div id="question_div_'.$questionId.'" class="main_question '.$remind_highlight.'" >';
7773
7774
            // Shows the question + possible answers
7775
            $showTitle = 1 == $this->getHideQuestionTitle() ? false : true;
7776
            // @todo not implemented in 1.11.x
7777
            /*echo $this->showQuestion(
7778
                $question_obj,
7779
                false,
7780
                $origin,
7781
                $i,
7782
                $showTitle,
7783
                false,
7784
                $user_choice,
7785
                false,
7786
                null,
7787
                false,
7788
                $this->getModelType(),
7789
                $this->categoryMinusOne
7790
            );*/
7791
7792
            // Button save and continue
7793
            switch ($this->type) {
7794
                case ONE_PER_PAGE:
7795
                    $exercise_actions .= $this->show_button(
7796
                        $questionId,
7797
                        $current_question,
7798
                        null,
7799
                        $remindList
7800
                    );
7801
7802
                    break;
7803
                case ALL_ON_ONE_PAGE:
7804
                    if (api_is_allowed_to_session_edit()) {
7805
                        $button = [
7806
                            Display::button(
7807
                                'save_now',
7808
                                get_lang('Save and continue'),
7809
                                ['type' => 'button', 'class' => 'btn btn--primary', 'data-question' => $questionId]
7810
                            ),
7811
                            '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>',
7812
                        ];
7813
                        $exercise_actions .= Display::div(
7814
                            implode(PHP_EOL, $button),
7815
                            ['class' => 'exercise_save_now_button']
7816
                        );
7817
                    }
7818
7819
                    break;
7820
            }
7821
7822
            if (!empty($questions_in_media)) {
7823
                $count_of_questions_inside_media = count($questions_in_media);
7824
                if ($count_of_questions_inside_media > 1 && api_is_allowed_to_session_edit()) {
7825
                    $button = [
7826
                        Display::button(
7827
                            'save_now',
7828
                            get_lang('Save and continue'),
7829
                            ['type' => 'button', 'class' => 'btn btn--primary', 'data-question' => $questionId]
7830
                        ),
7831
                        '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>&nbsp;',
7832
                    ];
7833
                    $exercise_actions = Display::div(
7834
                        implode(PHP_EOL, $button),
7835
                        ['class' => 'exercise_save_now_button']
7836
                    );
7837
                }
7838
7839
                if ($last_question_in_media && ONE_PER_PAGE == $this->type) {
7840
                    $exercise_actions = $this->show_button($questionId, $current_question, $questions_in_media);
7841
                }
7842
            }
7843
7844
            // Checkbox review answers. Not implemented.
7845
            /*if ($this->review_answers &&
7846
                !in_array($question_obj->type, Question::question_type_no_review())
7847
            ) {
7848
                $remind_question_div = Display::tag(
7849
                    'label',
7850
                    Display::input(
7851
                        'checkbox',
7852
                        'remind_list['.$questionId.']',
7853
                        '',
7854
                        $attributes
7855
                    ).get_lang('Revise question later'),
7856
                    [
7857
                        'class' => 'checkbox',
7858
                        'for' => 'remind_list['.$questionId.']',
7859
                    ]
7860
                );
7861
                $exercise_actions .= Display::div(
7862
                    $remind_question_div,
7863
                    ['class' => 'exercise_save_now_button']
7864
                );
7865
            }*/
7866
7867
            echo Display::div(' ', ['class' => 'clear']);
7868
7869
            $paginationCounter = null;
7870
            if (ONE_PER_PAGE == $this->type) {
7871
                if (empty($questions_in_media)) {
7872
                    $paginationCounter = Display::paginationIndicator(
7873
                        $current_question,
7874
                        count($realQuestionList)
7875
                    );
7876
                } else {
7877
                    if ($last_question_in_media) {
7878
                        $paginationCounter = Display::paginationIndicator(
7879
                            $current_question,
7880
                            count($realQuestionList)
7881
                        );
7882
                    }
7883
                }
7884
            }
7885
7886
            echo '<div class="row"><div class="pull-right">'.$paginationCounter.'</div></div>';
7887
            echo Display::div($exercise_actions, ['class' => 'form-actions']);
7888
            echo '</div>';
7889
        }
7890
    }
7891
7892
    /**
7893
     * Returns an array of categories details for the questions of the current
7894
     * exercise.
7895
     *
7896
     * @return array
7897
     */
7898
    public function getQuestionWithCategories()
7899
    {
7900
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
7901
        $categoryRelTable = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
7902
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
7903
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
7904
        $sql = "SELECT DISTINCT cat.*
7905
                FROM $TBL_EXERCICE_QUESTION e
7906
                INNER JOIN $TBL_QUESTIONS q
7907
                ON (e.question_id = q.iid)
7908
                INNER JOIN $categoryRelTable catRel
7909
                ON (catRel.question_id = e.question_id)
7910
                INNER JOIN $categoryTable cat
7911
                ON (cat.iid = catRel.category_id)
7912
                WHERE
7913
                  e.quiz_id	= ".(int) ($this->getId());
7914
7915
        $result = Database::query($sql);
7916
        $categoriesInExercise = [];
7917
        if (Database::num_rows($result)) {
7918
            $categoriesInExercise = Database::store_result($result, 'ASSOC');
7919
        }
7920
7921
        return $categoriesInExercise;
7922
    }
7923
7924
    /**
7925
     * Calculate the max_score of the quiz, depending of question inside, and quiz advanced option.
7926
     */
7927
    public function getMaxScore()
7928
    {
7929
        $outMaxScore = 0;
7930
        // list of question's id !!! the array key start at 1 !!!
7931
        $questionList = $this->selectQuestionList(true);
7932
7933
        if ($this->random > 0 && $this->randomByCat > 0) {
7934
            // test is random by category
7935
            // get the $numberRandomQuestions best score question of each category
7936
            $numberRandomQuestions = $this->random;
7937
            $tabCategoriesScores = [];
7938
            foreach ($questionList as $questionId) {
7939
                $questionCategoryId = TestCategory::getCategoryForQuestion($questionId);
7940
                if (!is_array($tabCategoriesScores[$questionCategoryId])) {
7941
                    $tabCategoriesScores[$questionCategoryId] = [];
7942
                }
7943
                $tmpObjQuestion = Question::read($questionId);
7944
                if (is_object($tmpObjQuestion)) {
7945
                    $tabCategoriesScores[$questionCategoryId][] = $tmpObjQuestion->weighting;
7946
                }
7947
            }
7948
7949
            // here we've got an array with first key, the category_id, second key, score of question for this cat
7950
            foreach ($tabCategoriesScores as $tabScores) {
7951
                rsort($tabScores);
7952
                $tabScoresCount = count($tabScores);
7953
                for ($i = 0; $i < min($numberRandomQuestions, $tabScoresCount); $i++) {
7954
                    $outMaxScore += $tabScores[$i];
7955
                }
7956
            }
7957
7958
            return $outMaxScore;
7959
        }
7960
7961
        // standard test, just add each question score
7962
        foreach ($questionList as $questionId) {
7963
            $question = Question::read($questionId, $this->course);
7964
            $outMaxScore += $question->weighting;
7965
        }
7966
7967
        return $outMaxScore;
7968
    }
7969
7970
    /**
7971
     * @return string
7972
     */
7973
    public function get_formated_title()
7974
    {
7975
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
7976
        }
7977
7978
        return api_html_entity_decode($this->selectTitle());
7979
    }
7980
7981
    /**
7982
     * @param string $title
7983
     *
7984
     * @return string
7985
     */
7986
    public static function get_formated_title_variable($title)
7987
    {
7988
        return api_html_entity_decode($title);
7989
    }
7990
7991
    /**
7992
     * @return string
7993
     */
7994
    public function format_title()
7995
    {
7996
        return api_htmlentities($this->title);
7997
    }
7998
7999
    /**
8000
     * @param string $title
8001
     *
8002
     * @return string
8003
     */
8004
    public static function format_title_variable($title)
8005
    {
8006
        return api_htmlentities($title);
8007
    }
8008
8009
    /**
8010
     * @param int $courseId
8011
     * @param int $sessionId
8012
     *
8013
     * @return array exercises
8014
     */
8015
    public function getExercisesByCourseSession($courseId, $sessionId)
8016
    {
8017
        $courseId = (int) $courseId;
8018
        $sessionId = (int) $sessionId;
8019
8020
        $tbl_quiz = Database::get_course_table(TABLE_QUIZ_TEST);
8021
        $sql = "SELECT * FROM $tbl_quiz cq
8022
                WHERE
8023
                    cq.c_id = %s AND
8024
                    (cq.session_id = %s OR cq.session_id = 0) AND
8025
                    cq.active = 0
8026
                ORDER BY cq.iid";
8027
        $sql = sprintf($sql, $courseId, $sessionId);
8028
8029
        $result = Database::query($sql);
8030
8031
        $rows = [];
8032
        while ($row = Database::fetch_array($result, 'ASSOC')) {
8033
            $rows[] = $row;
8034
        }
8035
8036
        return $rows;
8037
    }
8038
8039
    /**
8040
     * @param int   $courseId
8041
     * @param int   $sessionId
8042
     * @param array $quizId
8043
     *
8044
     * @return array exercises
8045
     */
8046
    public function getExerciseAndResult($courseId, $sessionId, $quizId = [])
8047
    {
8048
        if (empty($quizId)) {
8049
            return [];
8050
        }
8051
8052
        $sessionId = (int) $sessionId;
8053
        $courseId = (int) $courseId;
8054
8055
        $ids = is_array($quizId) ? $quizId : [$quizId];
8056
        $ids = array_map('intval', $ids);
8057
        $ids = implode(',', $ids);
8058
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8059
        if (0 != $sessionId) {
8060
            $sql = "SELECT * FROM $track_exercises te
8061
              INNER JOIN c_quiz cq
8062
              ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id
8063
              WHERE
8064
              te.id = %s AND
8065
              te.session_id = %s AND
8066
              cq.id IN (%s)
8067
              ORDER BY cq.id";
8068
8069
            $sql = sprintf($sql, $courseId, $sessionId, $ids);
8070
        } else {
8071
            $sql = "SELECT * FROM $track_exercises te
8072
              INNER JOIN c_quiz cq ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id
8073
              WHERE
8074
              te.id = %s AND
8075
              cq.id IN (%s)
8076
              ORDER BY cq.id";
8077
            $sql = sprintf($sql, $courseId, $ids);
8078
        }
8079
        $result = Database::query($sql);
8080
        $rows = [];
8081
        while ($row = Database::fetch_array($result, 'ASSOC')) {
8082
            $rows[] = $row;
8083
        }
8084
8085
        return $rows;
8086
    }
8087
8088
    /**
8089
     * @param $exeId
8090
     * @param $exercise_stat_info
8091
     * @param $remindList
8092
     * @param $currentQuestion
8093
     *
8094
     * @return int|null
8095
     */
8096
    public static function getNextQuestionId(
8097
        $exeId,
8098
        $exercise_stat_info,
8099
        $remindList,
8100
        $currentQuestion
8101
    ) {
8102
        $result = Event::get_exercise_results_by_attempt($exeId, 'incomplete');
8103
8104
        if (isset($result[$exeId])) {
8105
            $result = $result[$exeId];
8106
        } else {
8107
            return null;
8108
        }
8109
8110
        $data_tracking = $exercise_stat_info['data_tracking'];
8111
        $data_tracking = explode(',', $data_tracking);
8112
8113
        // if this is the final question do nothing.
8114
        if ($currentQuestion == count($data_tracking)) {
8115
            return null;
8116
        }
8117
8118
        $currentQuestion--;
8119
8120
        if (!empty($result['question_list'])) {
8121
            $answeredQuestions = [];
8122
            foreach ($result['question_list'] as $question) {
8123
                if (!empty($question['answer'])) {
8124
                    $answeredQuestions[] = $question['question_id'];
8125
                }
8126
            }
8127
8128
            // Checking answered questions
8129
            $counterAnsweredQuestions = 0;
8130
            foreach ($data_tracking as $questionId) {
8131
                if (!in_array($questionId, $answeredQuestions)) {
8132
                    if ($currentQuestion != $counterAnsweredQuestions) {
8133
                        break;
8134
                    }
8135
                }
8136
                $counterAnsweredQuestions++;
8137
            }
8138
8139
            $counterRemindListQuestions = 0;
8140
            // Checking questions saved in the reminder list
8141
            if (!empty($remindList)) {
8142
                foreach ($data_tracking as $questionId) {
8143
                    if (in_array($questionId, $remindList)) {
8144
                        // Skip the current question
8145
                        if ($currentQuestion != $counterRemindListQuestions) {
8146
                            break;
8147
                        }
8148
                    }
8149
                    $counterRemindListQuestions++;
8150
                }
8151
8152
                if ($counterRemindListQuestions < $currentQuestion) {
8153
                    return null;
8154
                }
8155
8156
                if (!empty($counterRemindListQuestions)) {
8157
                    if ($counterRemindListQuestions > $counterAnsweredQuestions) {
8158
                        return $counterAnsweredQuestions;
8159
                    } else {
8160
                        return $counterRemindListQuestions;
8161
                    }
8162
                }
8163
            }
8164
8165
            return $counterAnsweredQuestions;
8166
        }
8167
    }
8168
8169
    /**
8170
     * Gets the position of a questionId in the question list.
8171
     *
8172
     * @param $questionId
8173
     *
8174
     * @return int
8175
     */
8176
    public function getPositionInCompressedQuestionList($questionId)
8177
    {
8178
        $questionList = $this->getQuestionListWithMediasCompressed();
8179
        $mediaQuestions = $this->getMediaList();
8180
        $position = 1;
8181
        foreach ($questionList as $id) {
8182
            if (isset($mediaQuestions[$id]) && in_array($questionId, $mediaQuestions[$id])) {
8183
                $mediaQuestionList = $mediaQuestions[$id];
8184
                if (in_array($questionId, $mediaQuestionList)) {
8185
                    return $position;
8186
                } else {
8187
                    $position++;
8188
                }
8189
            } else {
8190
                if ($id == $questionId) {
8191
                    return $position;
8192
                } else {
8193
                    $position++;
8194
                }
8195
            }
8196
        }
8197
8198
        return 1;
8199
    }
8200
8201
    /**
8202
     * Get the correct answers in all attempts.
8203
     *
8204
     * @param int  $learnPathId
8205
     * @param int  $learnPathItemId
8206
     * @param bool $onlyCorrect
8207
     *
8208
     * @return array
8209
     */
8210
    public function getAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0, $onlyCorrect = true)
8211
    {
8212
        $attempts = Event::getExerciseResultsByUser(
8213
            api_get_user_id(),
8214
            $this->getId(),
8215
            api_get_course_int_id(),
8216
            api_get_session_id(),
8217
            $learnPathId,
8218
            $learnPathItemId,
8219
            'DESC'
8220
        );
8221
8222
        $list = [];
8223
        foreach ($attempts as $attempt) {
8224
            foreach ($attempt['question_list'] as $answers) {
8225
                foreach ($answers as $answer) {
8226
                    $objAnswer = new Answer($answer['question_id']);
8227
                    if ($onlyCorrect) {
8228
                        switch ($objAnswer->getQuestionType()) {
8229
                            case FILL_IN_BLANKS:
8230
                                $isCorrect = FillBlanks::isCorrect($answer['answer']);
8231
8232
                                break;
8233
                            case MATCHING:
8234
                            case DRAGGABLE:
8235
                            case MATCHING_DRAGGABLE:
8236
                                $isCorrect = Matching::isCorrect(
8237
                                    $answer['position'],
8238
                                    $answer['answer'],
8239
                                    $answer['question_id']
8240
                                );
8241
8242
                                break;
8243
                            case ORAL_EXPRESSION:
8244
                                $isCorrect = false;
8245
8246
                                break;
8247
                            default:
8248
                                $isCorrect = $objAnswer->isCorrectByAutoId($answer['answer']);
8249
                        }
8250
                        if ($isCorrect) {
8251
                            $list[$answer['question_id']][] = $answer;
8252
                        }
8253
                    } else {
8254
                        $list[$answer['question_id']][] = $answer;
8255
                    }
8256
                }
8257
            }
8258
8259
            if (false === $onlyCorrect) {
8260
                // Only take latest attempt
8261
                break;
8262
            }
8263
        }
8264
8265
        return $list;
8266
    }
8267
8268
    /**
8269
     * Get the correct answers in all attempts.
8270
     *
8271
     * @param int $learnPathId
8272
     * @param int $learnPathItemId
8273
     *
8274
     * @return array
8275
     */
8276
    public function getCorrectAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0)
8277
    {
8278
        return $this->getAnswersInAllAttempts($learnPathId, $learnPathItemId);
8279
    }
8280
8281
    /**
8282
     * @return bool
8283
     */
8284
    public function showPreviousButton()
8285
    {
8286
        $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
8287
        if (false === $allow) {
8288
            return true;
8289
        }
8290
8291
        return $this->showPreviousButton;
8292
    }
8293
8294
    public function getPreventBackwards()
8295
    {
8296
        return (int) $this->preventBackwards;
8297
    }
8298
8299
    /**
8300
     * @return int
8301
     */
8302
    public function getExerciseCategoryId()
8303
    {
8304
        if (empty($this->exerciseCategoryId)) {
8305
            return null;
8306
        }
8307
8308
        return (int) $this->exerciseCategoryId;
8309
    }
8310
8311
    /**
8312
     * @param int $value
8313
     */
8314
    public function setExerciseCategoryId($value)
8315
    {
8316
        if (!empty($value)) {
8317
            $this->exerciseCategoryId = (int) $value;
8318
        }
8319
    }
8320
8321
    /**
8322
     * Set the value to 1 to hide the question number.
8323
     *
8324
     * @param int $value
8325
     */
8326
    public function setHideQuestionNumber($value = 0)
8327
    {
8328
        $this->hideQuestionNumber = (int) $value;
8329
    }
8330
8331
    /**
8332
     * Gets the value to hide or show the question number. If it does not exist, it is set to 0.
8333
     *
8334
     * @return int 1 if the question number must be hidden
8335
     */
8336
    public function getHideQuestionNumber()
8337
    {
8338
        return (int) $this->hideQuestionNumber;
8339
    }
8340
8341
    public function setPageResultConfiguration(array $values)
8342
    {
8343
        $pageConfig = ('true' === api_get_setting('exercise.allow_quiz_results_page_config'));
8344
        if ($pageConfig) {
8345
            $params = [
8346
                'hide_expected_answer' => $values['hide_expected_answer'] ?? '',
8347
                'hide_question_score' => $values['hide_question_score'] ?? '',
8348
                'hide_total_score' => $values['hide_total_score'] ?? '',
8349
                'hide_category_table' => $values['hide_category_table'] ?? '',
8350
                'hide_correct_answered_questions' => $values['hide_correct_answered_questions'] ?? '',
8351
            ];
8352
            $this->pageResultConfiguration = $params;
8353
        }
8354
    }
8355
8356
    /**
8357
     * @param array $defaults
8358
     */
8359
    public function setPageResultConfigurationDefaults(&$defaults)
8360
    {
8361
        $configuration = $this->getPageResultConfiguration();
8362
        if (!empty($configuration) && !empty($defaults)) {
8363
            $defaults = array_merge($defaults, $configuration);
8364
        }
8365
    }
8366
8367
    /**
8368
     * @return array
8369
     */
8370
    public function getPageResultConfiguration()
8371
    {
8372
        $pageConfig = ('true' === api_get_setting('exercise.allow_quiz_results_page_config'));
8373
        if ($pageConfig) {
8374
            return $this->pageResultConfiguration;
8375
        }
8376
8377
        return [];
8378
    }
8379
8380
    /**
8381
     * @param string $attribute
8382
     *
8383
     * @return mixed|null
8384
     */
8385
    public function getPageConfigurationAttribute($attribute)
8386
    {
8387
        $result = $this->getPageResultConfiguration();
8388
8389
        if (!empty($result)) {
8390
            return $result[$attribute] ?? null;
8391
        }
8392
8393
        return null;
8394
    }
8395
8396
    /**
8397
     * @param bool $showPreviousButton
8398
     *
8399
     * @return Exercise
8400
     */
8401
    public function setShowPreviousButton($showPreviousButton)
8402
    {
8403
        $this->showPreviousButton = $showPreviousButton;
8404
8405
        return $this;
8406
    }
8407
8408
    /**
8409
     * @param array $notifications
8410
     */
8411
    public function setNotifications($notifications)
8412
    {
8413
        $this->notifications = $notifications;
8414
    }
8415
8416
    /**
8417
     * @return array
8418
     */
8419
    public function getNotifications()
8420
    {
8421
        return $this->notifications;
8422
    }
8423
8424
    /**
8425
     * @return bool
8426
     */
8427
    public function showExpectedChoice()
8428
    {
8429
        return ('true' === api_get_setting('exercise.show_exercise_expected_choice'));
8430
    }
8431
8432
    /**
8433
     * @return bool
8434
     */
8435
    public function showExpectedChoiceColumn()
8436
    {
8437
        if (true === $this->forceShowExpectedChoiceColumn) {
8438
            return true;
8439
        }
8440
        if ($this->hideExpectedAnswer) {
8441
            return false;
8442
        }
8443
        if (!in_array(
8444
            $this->results_disabled,
8445
            [
8446
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
8447
            ]
8448
        )
8449
        ) {
8450
            $hide = (int) $this->getPageConfigurationAttribute('hide_expected_answer');
8451
            if (1 === $hide) {
8452
                return false;
8453
            }
8454
8455
            return true;
8456
        }
8457
8458
        return false;
8459
    }
8460
8461
    public function getQuestionRibbon(string $class, string $scoreLabel, ?string $result, array $array): string
8462
    {
8463
        $hide = (int) $this->getPageConfigurationAttribute('hide_question_score');
8464
        if (1 === $hide) {
8465
            return '';
8466
        }
8467
8468
        $ribbon = '<div class="question-answer-result__header-ribbon-title question-answer-result__header-ribbon-title--'.$class.'">'.$scoreLabel.'</div>';
8469
        if (!empty($result)) {
8470
            $ribbon .= '<div class="question-answer-result__header-ribbon-detail">'
8471
                .get_lang('Score').': '.$result
8472
                .'</div>';
8473
        }
8474
8475
        $ribbonClassModifier = '';
8476
8477
        if ($this->showExpectedChoice()) {
8478
            $hideLabel = ('true' === api_get_setting('exercise.exercise_hide_label'));
8479
            if (true === $hideLabel) {
8480
                $ribbonClassModifier = 'question-answer-result__header-ribbon--no-ribbon';
8481
                $html = '';
8482
                $answerUsed = (int) $array['used'];
8483
                $answerMissing = (int) $array['missing'] - $answerUsed;
8484
                for ($i = 1; $i <= $answerUsed; $i++) {
8485
                    $html .= Display::getMdiIcon(StateIcon::COMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL);
8486
                }
8487
                for ($i = 1; $i <= $answerMissing; $i++) {
8488
                    $html .= Display::getMdiIcon(StateIcon::INCOMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL);
8489
                }
8490
                $ribbon = '<div class="question-answer-result__header-ribbon-title hide-label-title">'
8491
                    .get_lang('Correct answers').': '.$result.'</div>'
8492
                    .'<div class="question-answer-result__header-ribbon-detail">'.$html.'</div>';
8493
            }
8494
        }
8495
8496
        return Display::div(
8497
            $ribbon,
8498
            ['class' => "question-answer-result__header-ribbon $ribbonClassModifier"]
8499
        );
8500
    }
8501
8502
    /**
8503
     * @return int
8504
     */
8505
    public function getAutoLaunch()
8506
    {
8507
        return $this->autolaunch;
8508
    }
8509
8510
    /**
8511
     * Clean auto launch settings for all exercise in course/course-session.
8512
     */
8513
    public function enableAutoLaunch()
8514
    {
8515
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8516
        $sql = "UPDATE $table SET autolaunch = 1
8517
                WHERE iid = ".$this->iId;
8518
        Database::query($sql);
8519
    }
8520
8521
    /**
8522
     * Clean auto launch settings for all exercise in course/course-session.
8523
     */
8524
    public function cleanCourseLaunchSettings()
8525
    {
8526
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8527
        $sql = "UPDATE $table SET autolaunch = 0
8528
                WHERE c_id = ".$this->course_id.' AND session_id = '.$this->sessionId;
8529
        Database::query($sql);
8530
    }
8531
8532
    /**
8533
     * Get the title without HTML tags.
8534
     *
8535
     * @return string
8536
     */
8537
    public function getUnformattedTitle()
8538
    {
8539
        return strip_tags(api_html_entity_decode($this->title));
8540
    }
8541
8542
    /**
8543
     * Get the question IDs from quiz_rel_question for the current quiz,
8544
     * using the parameters as the arguments to the SQL's LIMIT clause.
8545
     * Because the exercise_id is known, it also comes with a filter on
8546
     * the session, so sessions are not specified here.
8547
     *
8548
     * @param int $start  At which question do we want to start the list
8549
     * @param int $length Up to how many results we want
8550
     *
8551
     * @return array A list of question IDs
8552
     */
8553
    public function getQuestionForTeacher($start = 0, $length = 10)
8554
    {
8555
        $start = (int) $start;
8556
        if ($start < 0) {
8557
            $start = 0;
8558
        }
8559
8560
        $length = (int) $length;
8561
8562
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8563
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
8564
        $sql = "SELECT DISTINCT e.question_id
8565
                FROM $quizRelQuestion e
8566
                INNER JOIN $question q
8567
                ON (e.question_id = q.iid)
8568
                WHERE
8569
8570
                    e.quiz_id = '".$this->getId()."'
8571
                ORDER BY question_order
8572
                LIMIT $start, $length
8573
            ";
8574
        $result = Database::query($sql);
8575
        $questionList = [];
8576
        while ($object = Database::fetch_object($result)) {
8577
            $questionList[] = $object->question_id;
8578
        }
8579
8580
        return $questionList;
8581
    }
8582
8583
    /**
8584
     * @param int   $exerciseId
8585
     * @param array $courseInfo
8586
     * @param int   $sessionId
8587
     *
8588
     * @return bool
8589
     */
8590
    public function generateStats($exerciseId, $courseInfo, $sessionId)
8591
    {
8592
        $allowStats = ('true' === api_get_setting('gradebook.allow_gradebook_stats'));
8593
        if (!$allowStats) {
8594
            return false;
8595
        }
8596
8597
        if (empty($courseInfo)) {
8598
            return false;
8599
        }
8600
8601
        $courseId = $courseInfo['real_id'];
8602
8603
        $sessionId = (int) $sessionId;
8604
        $exerciseId = (int) $exerciseId;
8605
8606
        $result = $this->read($exerciseId);
8607
8608
        if (empty($result)) {
8609
            api_not_allowed(true);
8610
        }
8611
8612
        $statusToFilter = empty($sessionId) ? STUDENT : 0;
8613
8614
        $studentList = CourseManager::get_user_list_from_course_code(
8615
            $courseInfo['code'],
8616
            $sessionId,
8617
            null,
8618
            null,
8619
            $statusToFilter
8620
        );
8621
8622
        if (empty($studentList)) {
8623
            Display::addFlash(Display::return_message(get_lang('No users in course')));
8624
            header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
8625
            exit;
8626
        }
8627
8628
        $tblStats = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8629
8630
        $studentIdList = [];
8631
        if (!empty($studentList)) {
8632
            $studentIdList = array_column($studentList, 'user_id');
8633
        }
8634
8635
        $sessionCondition = api_get_session_condition($sessionId);
8636
        if (false == $this->exercise_was_added_in_lp) {
8637
            $sql = "SELECT * FROM $tblStats
8638
                        WHERE
8639
                            exe_exo_id = $exerciseId AND
8640
                            orig_lp_id = 0 AND
8641
                            orig_lp_item_id = 0 AND
8642
                            status <> 'incomplete' AND
8643
                            c_id = $courseId
8644
                            $sessionCondition
8645
                        ";
8646
        } else {
8647
            $lpId = null;
8648
            if (!empty($this->lpList)) {
8649
                // Taking only the first LP
8650
                $lpId = $this->getLpBySession($sessionId);
8651
                $lpId = $lpId['lp_id'];
8652
            }
8653
8654
            $sql = "SELECT *
8655
                        FROM $tblStats
8656
                        WHERE
8657
                            exe_exo_id = $exerciseId AND
8658
                            orig_lp_id = $lpId AND
8659
                            status <> 'incomplete' AND
8660
                            session_id = $sessionId AND
8661
                            c_id = $courseId ";
8662
        }
8663
8664
        $sql .= ' ORDER BY exe_id DESC';
8665
8666
        $studentCount = 0;
8667
        $sum = 0;
8668
        $bestResult = 0;
8669
        $sumResult = 0;
8670
        $result = Database::query($sql);
8671
        while ($data = Database::fetch_array($result, 'ASSOC')) {
8672
            // Only take into account users in the current student list.
8673
            if (!empty($studentIdList)) {
8674
                if (!in_array($data['exe_user_id'], $studentIdList)) {
8675
                    continue;
8676
                }
8677
            }
8678
8679
            if (!isset($students[$data['exe_user_id']])) {
8680
                if (0 != $data['max_score']) {
8681
                    $students[$data['exe_user_id']] = $data['score'];
8682
                    if ($data['score'] > $bestResult) {
8683
                        $bestResult = $data['score'];
8684
                    }
8685
                    $sumResult += $data['score'];
8686
                }
8687
            }
8688
        }
8689
8690
        $count = count($studentList);
8691
        $average = $sumResult / $count;
8692
        $em = Database::getManager();
8693
8694
        $links = AbstractLink::getGradebookLinksFromItem(
8695
            $this->getId(),
8696
            LINK_EXERCISE,
8697
            $courseInfo['code'],
8698
            $sessionId
8699
        );
8700
8701
        if (empty($links)) {
8702
            $links = AbstractLink::getGradebookLinksFromItem(
8703
                $this->iId,
8704
                LINK_EXERCISE,
8705
                $courseInfo['code'],
8706
                $sessionId
8707
            );
8708
        }
8709
8710
        if (!empty($links)) {
8711
            $repo = $em->getRepository(GradebookLink::class);
8712
8713
            foreach ($links as $link) {
8714
                $linkId = $link['id'];
8715
                /** @var GradebookLink $exerciseLink */
8716
                $exerciseLink = $repo->find($linkId);
8717
                if ($exerciseLink) {
8718
                    $exerciseLink
8719
                        ->setUserScoreList($students)
8720
                        ->setBestScore($bestResult)
8721
                        ->setAverageScore($average)
8722
                        ->setScoreWeight($this->getMaxScore());
8723
                    $em->persist($exerciseLink);
8724
                    $em->flush();
8725
                }
8726
            }
8727
        }
8728
    }
8729
8730
    /**
8731
     * Return an HTML table of exercises for on-screen printing, including
8732
     * action icons. If no exercise is present and the user can edit the
8733
     * course, show a "create test" button.
8734
     *
8735
     * @param int    $categoryId
8736
     * @param string $keyword
8737
     * @param int    $userId
8738
     * @param int    $courseId
8739
     * @param int    $sessionId
8740
     * @param bool   $returnData
8741
     * @param int    $minCategoriesInExercise
8742
     * @param int    $filterByResultDisabled
8743
     * @param int    $filterByAttempt
8744
     *
8745
     * @return string|SortableTableFromArrayConfig
8746
     */
8747
    public static function exerciseGridResource(
8748
        $categoryId,
8749
        $keyword = '',
8750
        $userId = 0,
8751
        $courseId = 0,
8752
        $sessionId = 0,
8753
        $returnData = false,
8754
        $minCategoriesInExercise = 0,
8755
        $filterByResultDisabled = 0,
8756
        $filterByAttempt = 0,
8757
        $myActions = null,
8758
        $returnTable = false
8759
    ) {
8760
        $is_allowedToEdit = api_is_allowed_to_edit(null, true);
8761
        $courseId = $courseId ? (int) $courseId : api_get_course_int_id();
8762
        $sessionId = $sessionId ? (int) $sessionId : api_get_session_id();
8763
8764
        $course = api_get_course_entity($courseId);
8765
        $session = api_get_session_entity($sessionId);
8766
8767
        $userId = $userId ? (int) $userId : api_get_user_id();
8768
        $user = api_get_user_entity($userId);
8769
8770
        $repo = Container::getQuizRepository();
8771
8772
        // 2. Get query builder from repo.
8773
        $qb = $repo->getResourcesByCourse($course, $session);
8774
8775
        if (!empty($categoryId)) {
8776
            $qb->andWhere($qb->expr()->eq('resource.exerciseCategory', $categoryId));
8777
        } else {
8778
            $qb->andWhere($qb->expr()->isNull('resource.exerciseCategory'));
8779
        }
8780
8781
        $allowDelete = self::allowAction('delete');
8782
        $allowClean = self::allowAction('clean_results');
8783
8784
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8785
8786
        $categoryId = (int) $categoryId;
8787
        $keyword = Database::escape_string($keyword);
8788
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
8789
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
8790
        $autoLaunchAvailable = false;
8791
        if (1 == api_get_course_setting('enable_exercise_auto_launch') &&
8792
            ('true' === api_get_setting('exercise.allow_exercise_auto_launch'))
8793
        ) {
8794
            $autoLaunchAvailable = true;
8795
        }
8796
8797
        $courseId = $course->getId();
8798
        $tableRows = [];
8799
        $origin = api_get_origin();
8800
        $charset = 'utf-8';
8801
        $token = Security::get_token();
8802
        $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh($userId, ['real_id' => $courseId]);
8803
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
8804
        $content = '';
8805
        $column = 0;
8806
        if ($is_allowedToEdit) {
8807
            $column = 1;
8808
        }
8809
8810
        $table = new SortableTableFromArrayConfig(
8811
            [],
8812
            $column,
8813
            self::PAGINATION_ITEMS_PER_PAGE,
8814
            'exercises_cat_'.$categoryId.'_'.api_get_course_int_id().'_'.api_get_session_id()
8815
        );
8816
8817
        $limit = $table->per_page;
8818
        $page = $table->page_nr;
8819
        $from = $limit * ($page - 1);
8820
8821
        $categoryCondition = '';
8822
        if ('true' === api_get_setting('exercise.allow_exercise_categories')) {
8823
            if (!empty($categoryId)) {
8824
                $categoryCondition = " AND exercise_category_id = $categoryId ";
8825
            } else {
8826
                $categoryCondition = ' AND exercise_category_id IS NULL ';
8827
            }
8828
        }
8829
8830
        if (!empty($keyword)) {
8831
            $qb->andWhere($qb->expr()->eq('resource.title', ':keyword'));
8832
            $qb->setParameter('keyword', $keyword);
8833
        }
8834
8835
        // Only for administrators
8836
        if ($is_allowedToEdit) {
8837
            $qb->andWhere($qb->expr()->neq('resource.active', -1));
8838
        } else {
8839
            $qb->andWhere($qb->expr()->eq('resource.active', 1));
8840
        }
8841
8842
        $qb->setFirstResult($from);
8843
        $qb->setMaxResults($limit);
8844
8845
        $filterByResultDisabledCondition = '';
8846
        $filterByResultDisabled = (int) $filterByResultDisabled;
8847
        if (!empty($filterByResultDisabled)) {
8848
            $filterByResultDisabledCondition = ' AND e.results_disabled = '.$filterByResultDisabled;
8849
        }
8850
        $filterByAttemptCondition = '';
8851
        $filterByAttempt = (int) $filterByAttempt;
8852
        if (!empty($filterByAttempt)) {
8853
            $filterByAttemptCondition = ' AND e.max_attempt = '.$filterByAttempt;
8854
        }
8855
8856
        $exerciseList = $qb->getQuery()->getResult();
8857
8858
        $total = $repo->getCount($qb);
8859
8860
        $webPath = api_get_path(WEB_CODE_PATH);
8861
        if (!empty($exerciseList)) {
8862
            $visibilitySetting = ('true' === api_get_setting('lp.show_hidden_exercise_added_to_lp'));
8863
            //avoid sending empty parameters
8864
            $mylpid = empty($learnpath_id) ? '' : '&learnpath_id='.$learnpath_id;
8865
            $mylpitemid = empty($learnpath_item_id) ? '' : '&learnpath_item_id='.$learnpath_item_id;
8866
8867
            /** @var CQuiz $exerciseEntity */
8868
            foreach ($exerciseList as $exerciseEntity) {
8869
                $currentRow = [];
8870
                $exerciseId = $exerciseEntity->getIid();
8871
                $actions = '';
8872
                $attempt_text = '';
8873
                $exercise = new Exercise($courseId);
8874
                $exercise->read($exerciseId, false);
8875
8876
                if (empty($exercise->iId)) {
8877
                    continue;
8878
                }
8879
8880
                $allowToEditBaseCourse = true;
8881
                $visibility = $visibilityInCourse = $exerciseEntity->isVisible($course);
8882
                $visibilityInSession = false;
8883
                if (!empty($sessionId)) {
8884
                    // If we are in a session, the test is invisible
8885
                    // in the base course, it is included in a LP
8886
                    // *and* the setting to show it is *not*
8887
                    // specifically set to true, then hide it.
8888
                    if (false === $visibility) {
8889
                        if (!$visibilitySetting) {
8890
                            if ($exercise->exercise_was_added_in_lp) {
8891
                                continue;
8892
                            }
8893
                        }
8894
                    }
8895
8896
                    $visibility = $visibilityInSession = $exerciseEntity->isVisible($course, $session);
8897
                }
8898
8899
                // Validation when belongs to a session
8900
                $isBaseCourseExercise = true;
8901
                if (!($visibilityInCourse && $visibilityInSession)) {
8902
                    $isBaseCourseExercise = false;
8903
                }
8904
8905
                if (!empty($sessionId) && $isBaseCourseExercise) {
8906
                    $allowToEditBaseCourse = false;
8907
                }
8908
8909
                $sessionStar = null;
8910
                if (!$isBaseCourseExercise) {
8911
                    $sessionStar = api_get_session_image($sessionId, $user);
8912
                }
8913
8914
                $locked = $exercise->is_gradebook_locked;
8915
8916
                $startTime = $exerciseEntity->getStartTime();
8917
                $endTime = $exerciseEntity->getEndTime();
8918
                $time_limits = false;
8919
                if (!empty($startTime) || !empty($endTime)) {
8920
                    $time_limits = true;
8921
                }
8922
8923
                $is_actived_time = false;
8924
                if ($time_limits) {
8925
                    // check if start time
8926
                    $start_time = false;
8927
                    if (!empty($startTime)) {
8928
                        $start_time = api_strtotime($startTime->format('Y-m-d H:i:s'), 'UTC');
8929
                    }
8930
                    $end_time = false;
8931
                    if (!empty($endTime)) {
8932
                        $end_time = api_strtotime($endTime->format('Y-m-d H:i:s'), 'UTC');
8933
                    }
8934
                    $now = time();
8935
                    //If both "clocks" are enable
8936
                    if ($start_time && $end_time) {
8937
                        if ($now > $start_time && $end_time > $now) {
8938
                            $is_actived_time = true;
8939
                        }
8940
                    } else {
8941
                        //we check the start and end
8942
                        if ($start_time) {
8943
                            if ($now > $start_time) {
8944
                                $is_actived_time = true;
8945
                            }
8946
                        }
8947
                        if ($end_time) {
8948
                            if ($end_time > $now) {
8949
                                $is_actived_time = true;
8950
                            }
8951
                        }
8952
                    }
8953
                }
8954
8955
                // Blocking empty start times see BT#2800
8956
                // @todo replace global
8957
                /*global $_custom;
8958
                if (isset($_custom['exercises_hidden_when_no_start_date']) &&
8959
                    $_custom['exercises_hidden_when_no_start_date']
8960
                ) {
8961
                    if (empty($startTime)) {
8962
                        $time_limits = true;
8963
                        $is_actived_time = false;
8964
                    }
8965
                }*/
8966
8967
                $cut_title = $exercise->getCutTitle();
8968
                $alt_title = '';
8969
                if ($cut_title != $exerciseEntity->getTitle()) {
8970
                    $alt_title = ' title = "'.$exercise->getUnformattedTitle().'" ';
8971
                }
8972
8973
                // Teacher only.
8974
                if ($is_allowedToEdit) {
8975
                    $lp_blocked = null;
8976
                    if (true == $exercise->exercise_was_added_in_lp) {
8977
                        $lp_blocked = Display::div(
8978
                            get_lang(
8979
                                '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.'
8980
                            ),
8981
                            ['class' => 'lp_content_type_label']
8982
                        );
8983
                    }
8984
8985
                    $style = '';
8986
                    if (0 === $exerciseEntity->getActive() || false === $visibility) {
8987
                        $style = 'color:grey';
8988
                        //$title = Display::tag('font', $cut_title, ['style' => 'color:grey']);
8989
                    }
8990
8991
                    $title = $cut_title;
8992
8993
                    $url = '<a
8994
                        '.$alt_title.'
8995
                        id="tooltip_'.$exerciseId.'"
8996
                        href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$exerciseId.'"
8997
                        style = "'.$style.'"
8998
                        >
8999
                         '.Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, ICON_SIZE_SMALL, $title).$title.
9000
                        '</a>';
9001
9002
                    if (ExerciseLib::isQuizEmbeddable($exerciseEntity)) {
9003
                        $embeddableIcon = Display::getMdiIcon('book-music-outline', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('ThisQuizCanBeEmbeddable'));
9004
                        $url .= Display::div($embeddableIcon, ['class' => 'pull-right']);
9005
                    }
9006
9007
                    $currentRow['title'] = $url.' '.$sessionStar.$lp_blocked;
9008
                    $rowi = $exerciseEntity->getQuestions()->count();
9009
9010
                    if ($repo->isGranted('EDIT', $exerciseEntity) && $allowToEditBaseCourse) {
9011
                        // Questions list
9012
                        $actions = Display::url(
9013
                            Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit')),
9014
                            'admin.php?'.api_get_cidreq().'&exerciseId='.$exerciseId
9015
                        );
9016
9017
                        // Test settings
9018
                        $settings = Display::url(
9019
                            Display::getMdiIcon(ToolIcon::SETTINGS, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Configure')),
9020
                            'exercise_admin.php?'.api_get_cidreq().'&exerciseId='.$exerciseId
9021
                        );
9022
9023
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9024
                            $settings = '';
9025
                        }
9026
                        $actions .= $settings;
9027
9028
                        // Exercise results
9029
                        $resultsLink = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9030
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).'</a>';
9031
9032
                        if ($limitTeacherAccess) {
9033
                            if (api_is_platform_admin()) {
9034
                                $actions .= $resultsLink;
9035
                            }
9036
                        } else {
9037
                            // Exercise results
9038
                            $actions .= $resultsLink;
9039
                        }
9040
9041
                        // Auto launch
9042
                        if ($autoLaunchAvailable) {
9043
                            $autoLaunch = $exercise->getAutoLaunch();
9044
                            if (empty($autoLaunch)) {
9045
                                $actions .= Display::url(
9046
                                    Display::getMdiIcon('rocket-launch', 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang('Enable')),
9047
                                    'exercise.php?'.api_get_cidreq(
9048
                                    ).'&action=enable_launch&sec_token='.$token.'&exerciseId='.$exerciseId
9049
                                );
9050
                            } else {
9051
                                $actions .= Display::url(
9052
                                    Display::getMdiIcon('rocket-launch', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Disable')),
9053
                                    'exercise.php?'.api_get_cidreq(
9054
                                    ).'&action=disable_launch&sec_token='.$token.'&exerciseId='.$exerciseId
9055
                                );
9056
                            }
9057
                        }
9058
9059
                        // Export
9060
                        $actions .= Display::url(
9061
                            Display::getMdiIcon('disc', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Copy this exercise as a new one')),
9062
                            '',
9063
                            [
9064
                                'onclick' => "javascript:if(!confirm('".addslashes(
9065
                                        api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES)
9066
                                    )." ".addslashes($title)."?"."')) return false;",
9067
                                'href' => 'exercise.php?'.api_get_cidreq(
9068
                                    ).'&action=copy_exercise&sec_token='.$token.'&exerciseId='.$exerciseId,
9069
                            ]
9070
                        );
9071
9072
                        // Clean exercise
9073
                        $clean = '';
9074
                        if (true === $allowClean) {
9075
                            if (!$locked) {
9076
                                $clean = Display::url(
9077
                                    Display::getMdiIcon(ActionIcon::RESET, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('CleanStudentResults')
9078
                                    ),
9079
                                    '',
9080
                                    [
9081
                                        'onclick' => "javascript:if(!confirm('".
9082
                                            addslashes(
9083
                                                api_htmlentities(
9084
                                                    get_lang('Are you sure to delete results'),
9085
                                                    ENT_QUOTES
9086
                                                )
9087
                                            )." ".addslashes($title)."?"."')) return false;",
9088
                                        'href' => 'exercise.php?'.api_get_cidreq(
9089
                                            ).'&action=clean_results&sec_token='.$token.'&exerciseId='.$exerciseId,
9090
                                    ]
9091
                                );
9092
                            } else {
9093
                                $clean = Display::getMdiIcon(ActionIcon::RESET, 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang('ResourceLockedByGradebook')
9094
                                );
9095
                            }
9096
                        }
9097
9098
                        $actions .= $clean;
9099
                        // Visible / invisible
9100
                        // Check if this exercise was added in a LP
9101
                        if ($exercise->exercise_was_added_in_lp) {
9102
                            $visibility = Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('AddedToLPCannotBeAccessed')
9103
                            );
9104
                        } else {
9105
                            if (0 === $exerciseEntity->getActive()) {
9106
                                $visibility = Display::url(
9107
                                    Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Activate')
9108
                                    ),
9109
                                    'exercise.php?'.api_get_cidreq(
9110
                                    ).'&choice=enable&sec_token='.$token.'&exerciseId='.$exerciseId
9111
                                );
9112
                            } else {
9113
                                // else if not active
9114
                                $visibility = Display::url(
9115
                                    Display::getMdiIcon(StateIcon::PUBLIC_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Deactivate')
9116
                                    ),
9117
                                    'exercise.php?'.api_get_cidreq(
9118
                                    ).'&choice=disable&sec_token='.$token.'&exerciseId='.$exerciseId
9119
                                );
9120
                            }
9121
                        }
9122
9123
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9124
                            $visibility = '';
9125
                        }
9126
9127
                        $actions .= $visibility;
9128
9129
                        // Export qti ...
9130
                        $export = Display::url(
9131
                            Display::getMdiIcon(
9132
                                'database',
9133
                                'ch-tool-icon',
9134
                                null,
9135
                                ICON_SIZE_SMALL,
9136
                                'IMS/QTI'
9137
                            ),
9138
                            'exercise.php?action=exportqti2&exerciseId='.$exerciseId.'&'.api_get_cidreq()
9139
                        );
9140
9141
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9142
                            $export = '';
9143
                        }
9144
9145
                        $actions .= $export;
9146
                    } else {
9147
                        // not session
9148
                        $actions = Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang('ExerciseEditionNotAvailableInSession')
9149
                        );
9150
9151
                        // Check if this exercise was added in a LP
9152
                        if ($exercise->exercise_was_added_in_lp) {
9153
                            $visibility = Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('AddedToLPCannotBeAccessed')
9154
                            );
9155
                        } else {
9156
                            if (0 === $exerciseEntity->getActive() || 0 == $visibility) {
9157
                                $visibility = Display::url(
9158
                                    Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Activate')
9159
                                    ),
9160
                                    'exercise.php?'.api_get_cidreq(
9161
                                    ).'&choice=enable&sec_token='.$token.'&exerciseId='.$exerciseId
9162
                                );
9163
                            } else {
9164
                                // else if not active
9165
                                $visibility = Display::url(
9166
                                    Display::getMdiIcon(StateIcon::PUBLIC_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Deactivate')
9167
                                    ),
9168
                                    'exercise.php?'.api_get_cidreq(
9169
                                    ).'&choice=disable&sec_token='.$token.'&exerciseId='.$exerciseId
9170
                                );
9171
                            }
9172
                        }
9173
9174
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9175
                            $visibility = '';
9176
                        }
9177
9178
                        $actions .= $visibility;
9179
                        $actions .= '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9180
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).'</a>';
9181
                        $actions .= Display::url(
9182
                            Display::getMdiIcon('disc', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Copy this exercise as a new one')),
9183
                            '',
9184
                            [
9185
                                'onclick' => "javascript:if(!confirm('".addslashes(
9186
                                        api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES)
9187
                                    )." ".addslashes($title)."?"."')) return false;",
9188
                                'href' => 'exercise.php?'.api_get_cidreq(
9189
                                    ).'&choice=copy_exercise&sec_token='.$token.'&exerciseId='.$exerciseId,
9190
                            ]
9191
                        );
9192
                    }
9193
9194
                    // Delete
9195
                    $delete = '';
9196
                    if ($repo->isGranted('DELETE', $exerciseEntity) && $allowToEditBaseCourse) {
9197
                        if (!$locked) {
9198
                            $deleteUrl = 'exercise.php?'.api_get_cidreq().'&action=delete&sec_token='.$token.'&exerciseId='.$exerciseId;
9199
                            $delete = Display::url(
9200
                                Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete')),
9201
                                '',
9202
                                [
9203
                                    'onclick' => "javascript:if(!confirm('".
9204
                                        addslashes(api_htmlentities(get_lang('Are you sure to delete?')))." ".
9205
                                        addslashes($exercise->getUnformattedTitle())."?"."')) return false;",
9206
                                    'href' => $deleteUrl,
9207
                                ]
9208
                            );
9209
                        } else {
9210
                            $delete = Display::getMdiIcon(
9211
                                ActionIcon::DELETE,
9212
                                'ch-tool-icon-disabled',
9213
                                null,
9214
                                ICON_SIZE_SMALL,
9215
                                get_lang(
9216
                                    '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.'
9217
                                )
9218
                            );
9219
                        }
9220
                    }
9221
9222
                    if ($limitTeacherAccess && !api_is_platform_admin()) {
9223
                        $delete = '';
9224
                    }
9225
9226
                    if (!empty($minCategoriesInExercise)) {
9227
                        $cats = TestCategory::getListOfCategoriesForTest($exercise);
9228
                        if (!(count($cats) >= $minCategoriesInExercise)) {
9229
                            continue;
9230
                        }
9231
                    }
9232
                    $actions .= $delete;
9233
9234
                    // Number of questions.
9235
                    $random = $exerciseEntity->getRandom();
9236
                    if ($random > 0 || -1 == $random) {
9237
                        // if random == -1 means use random questions with all questions
9238
                        $random_number_of_question = $random;
9239
                        if (-1 == $random_number_of_question) {
9240
                            $random_number_of_question = $rowi;
9241
                        }
9242
                        if ($exerciseEntity->getRandomByCategory() > 0) {
9243
                            $nbQuestionsTotal = TestCategory::getNumberOfQuestionRandomByCategory(
9244
                                $exerciseId,
9245
                                $random_number_of_question
9246
                            );
9247
                            $number_of_questions = $nbQuestionsTotal.' ';
9248
                            $number_of_questions .= ($nbQuestionsTotal > 1) ? get_lang('QuestionsLowerCase') : get_lang(
9249
                                'QuestionLowerCase'
9250
                            );
9251
                            $number_of_questions .= ' - ';
9252
                            $number_of_questions .= min(
9253
                                    TestCategory::getNumberMaxQuestionByCat($exerciseId),
9254
                                    $random_number_of_question
9255
                                ).' '.get_lang('QuestionByCategory');
9256
                        } else {
9257
                            $random_label = ' ('.get_lang('Random').') ';
9258
                            $number_of_questions = $random_number_of_question.' '.$random_label.' / '.$rowi;
9259
                            // Bug if we set a random value bigger than the real number of questions
9260
                            if ($random_number_of_question > $rowi) {
9261
                                $number_of_questions = $rowi.' '.$random_label;
9262
                            }
9263
                        }
9264
                    } else {
9265
                        $number_of_questions = $rowi;
9266
                    }
9267
9268
                    $currentRow['count_questions'] = $number_of_questions;
9269
                } else {
9270
                    // Student only.
9271
                    $visibility = $exerciseEntity->isVisible($course, null);
9272
                    if (false === $visibility && !empty($sessionId)) {
9273
                        $visibility = $exerciseEntity->isVisible($course, $session);
9274
                    }
9275
9276
                    if (false === $visibility) {
9277
                        continue;
9278
                    }
9279
9280
                    $url = '<a '.$alt_title.'
9281
                        href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$exerciseId.'">'.
9282
                        $cut_title.'</a>';
9283
9284
                    // Link of the exercise.
9285
                    $currentRow['title'] = $url.' '.$sessionStar;
9286
                    // This query might be improved later on by ordering by the new "tms" field rather than by exe_id
9287
                    if ($returnData) {
9288
                        $currentRow['title'] = $exercise->getUnformattedTitle();
9289
                    }
9290
9291
                    $sessionCondition = api_get_session_condition(api_get_session_id());
9292
                    // Don't remove this marker: note-query-exe-results
9293
                    $sql = "SELECT * FROM $TBL_TRACK_EXERCISES
9294
                            WHERE
9295
                                exe_exo_id = ".$exerciseId." AND
9296
                                exe_user_id = $userId AND
9297
                                c_id = ".api_get_course_int_id()." AND
9298
                                status <> 'incomplete' AND
9299
                                orig_lp_id = 0 AND
9300
                                orig_lp_item_id = 0
9301
                                $sessionCondition
9302
                            ORDER BY exe_id DESC";
9303
9304
                    $qryres = Database::query($sql);
9305
                    $num = Database:: num_rows($qryres);
9306
9307
                    // Hide the results.
9308
                    $my_result_disabled = $exerciseEntity->getResultsDisabled();
9309
                    $attempt_text = '-';
9310
                    // Time limits are on
9311
                    if ($time_limits) {
9312
                        // Exam is ready to be taken
9313
                        if ($is_actived_time) {
9314
                            // Show results
9315
                            if (
9316
                            in_array(
9317
                                $my_result_disabled,
9318
                                [
9319
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9320
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9321
                                    RESULT_DISABLE_SHOW_SCORE_ONLY,
9322
                                    RESULT_DISABLE_RANKING,
9323
                                ]
9324
                            )
9325
                            ) {
9326
                                // More than one attempt
9327
                                if ($num > 0) {
9328
                                    $row_track = Database:: fetch_array($qryres);
9329
                                    $attempt_text = get_lang('Latest attempt').' : ';
9330
                                    $attempt_text .= ExerciseLib::show_score(
9331
                                        $row_track['score'],
9332
                                        $row_track['max_score']
9333
                                    );
9334
                                } else {
9335
                                    //No attempts
9336
                                    $attempt_text = get_lang('Not attempted');
9337
                                }
9338
                            } else {
9339
                                $attempt_text = '-';
9340
                            }
9341
                        } else {
9342
                            // Quiz not ready due to time limits
9343
                            //@todo use the is_visible function
9344
                            if (!empty($startTime) && !empty($endTime)) {
9345
                                $today = time();
9346
                                if ($today < $start_time) {
9347
                                    $attempt_text = sprintf(
9348
                                        get_lang('ExerciseWillBeActivatedFromXToY'),
9349
                                        api_convert_and_format_date($start_time),
9350
                                        api_convert_and_format_date($end_time)
9351
                                    );
9352
                                } else {
9353
                                    if ($today > $end_time) {
9354
                                        $attempt_text = sprintf(
9355
                                            get_lang('ExerciseWasActivatedFromXToY'),
9356
                                            api_convert_and_format_date($start_time),
9357
                                            api_convert_and_format_date($end_time)
9358
                                        );
9359
                                    }
9360
                                }
9361
                            } else {
9362
                                if (!empty($startTime)) {
9363
                                    $attempt_text = sprintf(
9364
                                        get_lang('ExerciseAvailableFromX'),
9365
                                        api_convert_and_format_date($start_time)
9366
                                    );
9367
                                }
9368
                                if (!empty($endTime)) {
9369
                                    $attempt_text = sprintf(
9370
                                        get_lang('ExerciseAvailableUntilX'),
9371
                                        api_convert_and_format_date($end_time)
9372
                                    );
9373
                                }
9374
                            }
9375
                        }
9376
                    } else {
9377
                        // Normal behaviour.
9378
                        // Show results.
9379
                        if (
9380
                        in_array(
9381
                            $my_result_disabled,
9382
                            [
9383
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9384
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9385
                                RESULT_DISABLE_SHOW_SCORE_ONLY,
9386
                                RESULT_DISABLE_RANKING,
9387
                                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
9388
                            ]
9389
                        )
9390
                        ) {
9391
                            if ($num > 0) {
9392
                                $row_track = Database::fetch_array($qryres);
9393
                                $attempt_text = get_lang('Latest attempt').' : ';
9394
                                $attempt_text .= ExerciseLib::show_score(
9395
                                    $row_track['score'],
9396
                                    $row_track['max_score']
9397
                                );
9398
                            } else {
9399
                                $attempt_text = get_lang('Not attempted');
9400
                            }
9401
                        }
9402
                    }
9403
                    if ($returnData) {
9404
                        $attempt_text = $num;
9405
                    }
9406
                }
9407
9408
                $currentRow['attempt'] = $attempt_text;
9409
9410
                if ($is_allowedToEdit) {
9411
                    $additionalActions = ExerciseLib::getAdditionalTeacherActions($exerciseId);
9412
9413
                    if (!empty($additionalActions)) {
9414
                        $actions .= $additionalActions.PHP_EOL;
9415
                    }
9416
9417
                    if (!empty($myActions) && is_callable($myActions)) {
9418
                        $actions = $myActions($row);
9419
                    }
9420
                    $currentRow = [
9421
                        $exerciseId,
9422
                        $currentRow['title'],
9423
                        $currentRow['count_questions'],
9424
                        $actions,
9425
                    ];
9426
                } else {
9427
                    $currentRow = [
9428
                        $currentRow['title'],
9429
                        $currentRow['attempt'],
9430
                    ];
9431
9432
                    if ($isDrhOfCourse) {
9433
                        $currentRow[] = '<a
9434
                            href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9435
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).
9436
                            '</a>';
9437
                    }
9438
                    if ($returnData) {
9439
                        $currentRow['id'] = $exercise->id;
9440
                        $currentRow['url'] = $webPath.'exercise/overview.php?'
9441
                            .api_get_cidreq_params($courseId, $sessionId).'&'
9442
                            ."$mylpid$mylpitemid&exerciseId={$exercise->id}";
9443
                        $currentRow['name'] = $currentRow[0];
9444
                    }
9445
                }
9446
                $tableRows[] = $currentRow;
9447
            }
9448
        }
9449
9450
        if (empty($tableRows) && empty($categoryId)) {
9451
            if ($is_allowedToEdit && 'learnpath' !== $origin) {
9452
                $content .= Display::noDataView(
9453
                    get_lang('Quiz'),
9454
                    Display::getMdiIcon(ToolIcon::QUIZ, 'ch-tool-icon', null, ICON_SIZE_BIG),
9455
                    get_lang('Create a new test'),
9456
                    'exercise_admin.php?'.api_get_cidreq()
9457
                );
9458
            }
9459
        } else {
9460
            if (empty($tableRows)) {
9461
                return '';
9462
            }
9463
            $table->setTableData($tableRows);
9464
            $table->setTotalNumberOfItems($total);
9465
            $table->set_additional_parameters(
9466
                [
9467
                    'cid' => api_get_course_int_id(),
9468
                    'sid' => api_get_session_id(),
9469
                    'category_id' => $categoryId,
9470
                ]
9471
            );
9472
9473
            if ($is_allowedToEdit) {
9474
                $formActions = [];
9475
                $formActions['visible'] = get_lang('Activate');
9476
                $formActions['invisible'] = get_lang('Deactivate');
9477
                $formActions['delete'] = get_lang('Delete');
9478
                $table->set_form_actions($formActions);
9479
            }
9480
9481
            $i = 0;
9482
            if ($is_allowedToEdit) {
9483
                $table->set_header($i++, '', false, 'width="18px"');
9484
            }
9485
            $table->set_header($i++, get_lang('Test name'), false);
9486
9487
            if ($is_allowedToEdit) {
9488
                $table->set_header($i++, get_lang('Questions'), false);
9489
                $table->set_header($i++, get_lang('Actions'), false, ['class' => 'text-right']);
9490
            } else {
9491
                $table->set_header($i++, get_lang('Status'), false);
9492
                if ($isDrhOfCourse) {
9493
                    $table->set_header($i++, get_lang('Actions'), false, ['class' => 'text-right']);
9494
                }
9495
            }
9496
9497
            if ($returnTable) {
9498
                return $table;
9499
            }
9500
            $content .= $table->return_table();
9501
        }
9502
9503
        return $content;
9504
    }
9505
9506
    /**
9507
     * @return int value in minutes
9508
     */
9509
    public function getResultAccess()
9510
    {
9511
        $extraFieldValue = new ExtraFieldValue('exercise');
9512
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9513
            $this->iId,
9514
            'results_available_for_x_minutes'
9515
        );
9516
9517
        if (!empty($value) && isset($value['value'])) {
9518
            return (int) $value['value'];
9519
        }
9520
9521
        return 0;
9522
    }
9523
9524
    /**
9525
     * @param array $exerciseResultInfo
9526
     *
9527
     * @return bool
9528
     */
9529
    public function getResultAccessTimeDiff($exerciseResultInfo)
9530
    {
9531
        $value = $this->getResultAccess();
9532
        if (!empty($value)) {
9533
            $endDate = new DateTime($exerciseResultInfo['exe_date'], new DateTimeZone('UTC'));
9534
            $endDate->add(new DateInterval('PT'.$value.'M'));
9535
            $now = time();
9536
            if ($endDate->getTimestamp() > $now) {
9537
                return (int) $endDate->getTimestamp() - $now;
9538
            }
9539
        }
9540
9541
        return 0;
9542
    }
9543
9544
    /**
9545
     * @param array $exerciseResultInfo
9546
     *
9547
     * @return bool
9548
     */
9549
    public function hasResultsAccess($exerciseResultInfo)
9550
    {
9551
        $diff = $this->getResultAccessTimeDiff($exerciseResultInfo);
9552
        if (0 === $diff) {
9553
            return false;
9554
        }
9555
9556
        return true;
9557
    }
9558
9559
    /**
9560
     * @return int
9561
     */
9562
    public function getResultsAccess()
9563
    {
9564
        $extraFieldValue = new ExtraFieldValue('exercise');
9565
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9566
            $this->iId,
9567
            'results_available_for_x_minutes'
9568
        );
9569
        if (!empty($value)) {
9570
            return (int) $value;
9571
        }
9572
9573
        return 0;
9574
    }
9575
9576
    /**
9577
     * @param int   $questionId
9578
     * @param bool  $show_results
9579
     * @param array $question_result
9580
     */
9581
    public function getDelineationResult(Question $objQuestionTmp, $questionId, $show_results, $question_result)
9582
    {
9583
        $id = (int) $objQuestionTmp->id;
9584
        $questionId = (int) $questionId;
9585
9586
        $final_overlap = $question_result['extra']['final_overlap'];
9587
        $final_missing = $question_result['extra']['final_missing'];
9588
        $final_excess = $question_result['extra']['final_excess'];
9589
9590
        $overlap_color = $question_result['extra']['overlap_color'];
9591
        $missing_color = $question_result['extra']['missing_color'];
9592
        $excess_color = $question_result['extra']['excess_color'];
9593
9594
        $threadhold1 = $question_result['extra']['threadhold1'];
9595
        $threadhold2 = $question_result['extra']['threadhold2'];
9596
        $threadhold3 = $question_result['extra']['threadhold3'];
9597
9598
        if ($show_results) {
9599
            if ($overlap_color) {
9600
                $overlap_color = 'green';
9601
            } else {
9602
                $overlap_color = 'red';
9603
            }
9604
9605
            if ($missing_color) {
9606
                $missing_color = 'green';
9607
            } else {
9608
                $missing_color = 'red';
9609
            }
9610
            if ($excess_color) {
9611
                $excess_color = 'green';
9612
            } else {
9613
                $excess_color = 'red';
9614
            }
9615
9616
            if (!is_numeric($final_overlap)) {
9617
                $final_overlap = 0;
9618
            }
9619
9620
            if (!is_numeric($final_missing)) {
9621
                $final_missing = 0;
9622
            }
9623
            if (!is_numeric($final_excess)) {
9624
                $final_excess = 0;
9625
            }
9626
9627
            if ($final_excess > 100) {
9628
                $final_excess = 100;
9629
            }
9630
9631
            $table_resume = '
9632
                    <table class="table table-hover table-striped data_table">
9633
                        <tr class="row_odd" >
9634
                            <td>&nbsp;</td>
9635
                            <td><b>'.get_lang('Requirements').'</b></td>
9636
                            <td><b>'.get_lang('YourAnswer').'</b></td>
9637
                        </tr>
9638
                        <tr class="row_even">
9639
                            <td><b>'.get_lang('Overlap').'</b></td>
9640
                            <td>'.get_lang('Min').' '.$threadhold1.'</td>
9641
                            <td>
9642
                                <div style="color:'.$overlap_color.'">
9643
                                    '.(($final_overlap < 0) ? 0 : intval($final_overlap)).'
9644
                                </div>
9645
                            </td>
9646
                        </tr>
9647
                        <tr>
9648
                            <td><b>'.get_lang('Excess').'</b></td>
9649
                            <td>'.get_lang('Max').' '.$threadhold2.'</td>
9650
                            <td>
9651
                                <div style="color:'.$excess_color.'">
9652
                                    '.(($final_excess < 0) ? 0 : intval($final_excess)).'
9653
                                </div>
9654
                            </td>
9655
                        </tr>
9656
                        <tr class="row_even">
9657
                            <td><b>'.get_lang('Missing').'</b></td>
9658
                            <td>'.get_lang('Max').' '.$threadhold3.'</td>
9659
                            <td>
9660
                                <div style="color:'.$missing_color.'">
9661
                                    '.(($final_missing < 0) ? 0 : intval($final_missing)).'
9662
                                </div>
9663
                            </td>
9664
                        </tr>
9665
                    </table>
9666
                ';
9667
9668
            $answerType = $objQuestionTmp->selectType();
9669
            /*if ($next == 0) {
9670
                $try = $try_hotspot;
9671
                $lp = $lp_hotspot;
9672
                $destinationid = $select_question_hotspot;
9673
                $url = $url_hotspot;
9674
            } else {
9675
                //show if no error
9676
                $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
9677
                $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
9678
            }
9679
            echo '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>';
9680
            if ($organs_at_risk_hit > 0) {
9681
                $message = '<br />'.get_lang('ResultIs').' <b>'.$result_comment.'</b><br />';
9682
                $message .= '<p style="color:#DC0A0A;"><b>'.get_lang('OARHit').'</b></p>';
9683
            } else {
9684
                $message = '<p>'.get_lang('YourDelineation').'</p>';
9685
                $message .= $table_resume;
9686
                $message .= '<br />'.get_lang('ResultIs').' <b>'.$result_comment.'</b><br />';
9687
            }
9688
            $message .= '<p>'.$comment.'</p>';
9689
            echo $message;*/
9690
9691
            // Showing the score
9692
            /*$queryfree = "SELECT marks FROM $TBL_TRACK_ATTEMPT
9693
                          WHERE exe_id = $id AND question_id =  $questionId";
9694
            $resfree = Database::query($queryfree);
9695
            $questionScore = Database::result($resfree, 0, 'marks');
9696
            $totalScore += $questionScore;*/
9697
            $relPath = api_get_path(REL_CODE_PATH);
9698
            echo '</table></td></tr>';
9699
            echo "
9700
                        <tr>
9701
                            <td colspan=\"2\">
9702
                                <div id=\"hotspot-solution\"></div>
9703
                                <script>
9704
                                    $(function() {
9705
                                        new HotspotQuestion({
9706
                                            questionId: $questionId,
9707
                                            exerciseId: {$this->id},
9708
                                            exeId: $id,
9709
                                            selector: '#hotspot-solution',
9710
                                            for: 'solution',
9711
                                            relPath: '$relPath'
9712
                                        });
9713
                                    });
9714
                                </script>
9715
                            </td>
9716
                        </tr>
9717
                    </table>
9718
                ";
9719
        }
9720
    }
9721
9722
    /**
9723
     * Clean exercise session variables.
9724
     */
9725
    public static function cleanSessionVariables()
9726
    {
9727
        Session::erase('objExercise');
9728
        Session::erase('exe_id');
9729
        Session::erase('calculatedAnswerId');
9730
        Session::erase('duration_time_previous');
9731
        Session::erase('duration_time');
9732
        Session::erase('objQuestion');
9733
        Session::erase('objAnswer');
9734
        Session::erase('questionList');
9735
        Session::erase('categoryList');
9736
        Session::erase('exerciseResult');
9737
        Session::erase('firstTime');
9738
9739
        Session::erase('time_per_question');
9740
        Session::erase('question_start');
9741
        Session::erase('exerciseResultCoordinates');
9742
        Session::erase('hotspot_coord');
9743
        Session::erase('hotspot_dest');
9744
        Session::erase('hotspot_delineation_result');
9745
    }
9746
9747
    /**
9748
     * Get the first LP found matching the session ID.
9749
     *
9750
     * @param int $sessionId
9751
     *
9752
     * @return array
9753
     */
9754
    public function getLpBySession($sessionId)
9755
    {
9756
        if (!empty($this->lpList)) {
9757
            $sessionId = (int) $sessionId;
9758
9759
            foreach ($this->lpList as $lp) {
9760
                if (isset($lp['session_id']) && (int) $lp['session_id'] == $sessionId) {
9761
                    return $lp;
9762
                }
9763
            }
9764
9765
            return current($this->lpList);
9766
        }
9767
9768
        return [
9769
            'lp_id' => 0,
9770
            'max_score' => 0,
9771
            'session_id' => 0,
9772
        ];
9773
    }
9774
9775
    public static function saveExerciseInLp($safe_item_id, $safe_exe_id)
9776
    {
9777
        $lp = Session::read('oLP');
9778
9779
        $safe_exe_id = (int) $safe_exe_id;
9780
        $safe_item_id = (int) $safe_item_id;
9781
9782
        if (empty($lp) || empty($safe_exe_id) || empty($safe_item_id)) {
9783
            return false;
9784
        }
9785
9786
        $viewId = $lp->get_view_id();
9787
        $course_id = api_get_course_int_id();
9788
        $userId = (int) api_get_user_id();
9789
        $viewId = (int) $viewId;
9790
9791
        $TBL_TRACK_EXERCICES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
9792
        $TBL_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
9793
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
9794
9795
        $sql = "SELECT start_date, exe_date, score, max_score, exe_exo_id, exe_duration
9796
                FROM $TBL_TRACK_EXERCICES
9797
                WHERE exe_id = $safe_exe_id AND exe_user_id = $userId";
9798
        $res = Database::query($sql);
9799
        $row_dates = Database::fetch_array($res);
9800
9801
        if (empty($row_dates)) {
9802
            return false;
9803
        }
9804
9805
        $duration = (int) $row_dates['exe_duration'];
9806
        $score = (float) $row_dates['score'];
9807
        $max_score = (float) $row_dates['max_score'];
9808
9809
        $sql = "UPDATE $TBL_LP_ITEM SET
9810
                    max_score = '$max_score'
9811
                WHERE iid = $safe_item_id";
9812
        Database::query($sql);
9813
9814
        $sql = "SELECT iid FROM $TBL_LP_ITEM_VIEW
9815
                WHERE
9816
                    lp_item_id = $safe_item_id AND
9817
                    lp_view_id = $viewId
9818
                ORDER BY iid DESC
9819
                LIMIT 1";
9820
        $res_last_attempt = Database::query($sql);
9821
9822
        if (Database::num_rows($res_last_attempt) && !api_is_invitee()) {
9823
            $row_last_attempt = Database::fetch_row($res_last_attempt);
9824
            $lp_item_view_id = $row_last_attempt[0];
9825
9826
            $exercise = new Exercise($course_id);
9827
            $exercise->read($row_dates['exe_exo_id']);
9828
            $status = 'completed';
9829
9830
            if (!empty($exercise->pass_percentage)) {
9831
                $status = 'failed';
9832
                $success = ExerciseLib::isSuccessExerciseResult(
9833
                    $score,
9834
                    $max_score,
9835
                    $exercise->pass_percentage
9836
                );
9837
                if ($success) {
9838
                    $status = 'passed';
9839
                }
9840
            }
9841
9842
            $sql = "UPDATE $TBL_LP_ITEM_VIEW SET
9843
                        status = '$status',
9844
                        score = '$score',
9845
                        total_time = '$duration'
9846
                    WHERE iid = $lp_item_view_id";
9847
            Database::query($sql);
9848
9849
            $sql = "UPDATE $TBL_TRACK_EXERCICES SET
9850
                        orig_lp_item_view_id = '$lp_item_view_id'
9851
                    WHERE exe_id = ".$safe_exe_id;
9852
            Database::query($sql);
9853
        }
9854
    }
9855
9856
    /**
9857
     * Get the user answers saved in exercise.
9858
     *
9859
     * @param int $attemptId
9860
     *
9861
     * @return array
9862
     */
9863
    public function getUserAnswersSavedInExercise($attemptId)
9864
    {
9865
        $exerciseResult = [];
9866
9867
        $attemptList = Event::getAllExerciseEventByExeId($attemptId);
9868
9869
        foreach ($attemptList as $questionId => $options) {
9870
            foreach ($options as $option) {
9871
                $question = Question::read($option['question_id']);
9872
9873
                if ($question) {
9874
                    switch ($question->type) {
9875
                        case FILL_IN_BLANKS:
9876
                            $option['answer'] = $this->fill_in_blank_answer_to_string($option['answer']);
9877
                            break;
9878
                    }
9879
                }
9880
9881
                if (!empty($option['answer'])) {
9882
                    $exerciseResult[] = $questionId;
9883
9884
                    break;
9885
                }
9886
            }
9887
        }
9888
9889
        return $exerciseResult;
9890
    }
9891
9892
    /**
9893
     * Get the number of user answers saved in exercise.
9894
     *
9895
     * @param int $attemptId
9896
     *
9897
     * @return int
9898
     */
9899
    public function countUserAnswersSavedInExercise($attemptId)
9900
    {
9901
        $answers = $this->getUserAnswersSavedInExercise($attemptId);
9902
9903
        return count($answers);
9904
    }
9905
9906
    public static function allowAction($action)
9907
    {
9908
        if (api_is_platform_admin()) {
9909
            return true;
9910
        }
9911
9912
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
9913
        $disableClean = ('true' === api_get_setting('exercise.disable_clean_exercise_results_for_teachers'));
9914
9915
        switch ($action) {
9916
            case 'delete':
9917
                if (api_is_allowed_to_edit(null, true)) {
9918
                    if ($limitTeacherAccess) {
9919
                        return false;
9920
                    }
9921
9922
                    return true;
9923
                }
9924
                break;
9925
            case 'clean_results':
9926
                if (api_is_allowed_to_edit(null, true)) {
9927
                    if ($limitTeacherAccess) {
9928
                        return false;
9929
                    }
9930
9931
                    if ($disableClean) {
9932
                        return false;
9933
                    }
9934
9935
                    return true;
9936
                }
9937
9938
                break;
9939
        }
9940
9941
        return false;
9942
    }
9943
9944
    public static function getLpListFromExercise($exerciseId, $courseId)
9945
    {
9946
        $tableLpItem = Database::get_course_table(TABLE_LP_ITEM);
9947
        $tblLp = Database::get_course_table(TABLE_LP_MAIN);
9948
9949
        $exerciseId = (int) $exerciseId;
9950
        $courseId = (int) $courseId;
9951
9952
        $sql = "SELECT
9953
                    lp.title,
9954
                    lpi.lp_id,
9955
                    lpi.max_score
9956
                FROM $tableLpItem lpi
9957
                INNER JOIN $tblLp lp
9958
                ON (lpi.lp_id = lp.iid)
9959
                WHERE
9960
                    lpi.item_type = '".TOOL_QUIZ."' AND
9961
                    lpi.path = '$exerciseId'";
9962
        $result = Database::query($sql);
9963
        $lpList = [];
9964
        if (Database::num_rows($result) > 0) {
9965
            $lpList = Database::store_result($result, 'ASSOC');
9966
        }
9967
9968
        return $lpList;
9969
    }
9970
9971
    public function getReminderTable($questionList, $exercise_stat_info, $disableCheckBoxes = false)
9972
    {
9973
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
9974
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
9975
        $learnpath_item_view_id = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
9976
        $categoryId = isset($_REQUEST['category_id']) ? (int) $_REQUEST['category_id'] : 0;
9977
9978
        if (empty($exercise_stat_info)) {
9979
            return '';
9980
        }
9981
9982
        $remindList = $exercise_stat_info['questions_to_check'];
9983
        $remindList = explode(',', $remindList);
9984
9985
        $exeId = $exercise_stat_info['exe_id'];
9986
        $exerciseId = $exercise_stat_info['exe_exo_id'];
9987
        $exercise_result = $this->getUserAnswersSavedInExercise($exeId);
9988
9989
        $content = Display::label(get_lang('QuestionWithNoAnswer'), 'danger');
9990
        $content .= '<div class="clear"></div><br />';
9991
        $table = '';
9992
        $counter = 0;
9993
        // Loop over all question to show results for each of them, one by one
9994
        foreach ($questionList as $questionId) {
9995
            $objQuestionTmp = Question::read($questionId);
9996
            $check_id = 'remind_list['.$questionId.']';
9997
            $attributes = [
9998
                'id' => $check_id,
9999
                'onclick' => "save_remind_item(this, '$questionId');",
10000
                'data-question-id' => $questionId,
10001
            ];
10002
            if (in_array($questionId, $remindList)) {
10003
                $attributes['checked'] = 1;
10004
            }
10005
10006
            $checkbox = Display::input('checkbox', 'remind_list['.$questionId.']', '', $attributes);
10007
            $checkbox = '<div class="pretty p-svg p-curve">
10008
                        '.$checkbox.'
10009
                        <div class="state p-primary ">
10010
                         <svg class="svg svg-icon" viewBox="0 0 20 20">
10011
                            <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>
10012
                         </svg>
10013
                         <label>&nbsp;</label>
10014
                        </div>
10015
                    </div>';
10016
            $counter++;
10017
            $questionTitle = $counter.'. '.strip_tags($objQuestionTmp->selectTitle());
10018
            // Check if the question doesn't have an answer.
10019
            if (!in_array($questionId, $exercise_result)) {
10020
                $questionTitle = Display::label($questionTitle, 'danger');
10021
            }
10022
10023
            $label_attributes = [];
10024
            $label_attributes['for'] = $check_id;
10025
            if (false === $disableCheckBoxes) {
10026
                $questionTitle = Display::tag('label', $checkbox.$questionTitle, $label_attributes);
10027
            }
10028
            $table .= Display::div($questionTitle, ['class' => 'exercise_reminder_item ']);
10029
        }
10030
10031
        $content .= Display::div('', ['id' => 'message']).
10032
            Display::div($table, ['class' => 'question-check-test']);
10033
10034
        $content .= '<script>
10035
        var lp_data = $.param({
10036
            "learnpath_id": '.$learnpath_id.',
10037
            "learnpath_item_id" : '.$learnpath_item_id.',
10038
            "learnpath_item_view_id": '.$learnpath_item_view_id.'
10039
        });
10040
10041
        function final_submit() {
10042
            // Normal inputs.
10043
            window.location = "'.api_get_path(WEB_CODE_PATH).'exercise/exercise_result.php?'.api_get_cidreq().'&exe_id='.$exeId.'&" + lp_data;
10044
        }
10045
10046
        function selectAll() {
10047
            $("input[type=checkbox]").each(function () {
10048
                $(this).prop("checked", 1);
10049
                var question_id = $(this).data("question-id");
10050
                var action = "add";
10051
                $.ajax({
10052
                    url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10053
                    data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
10054
                    success: function(returnValue) {
10055
                    }
10056
                });
10057
            });
10058
        }
10059
10060
        function changeOptionStatus(status)
10061
        {
10062
            $("input[type=checkbox]").each(function () {
10063
                $(this).prop("checked", status);
10064
            });
10065
10066
            var action = "";
10067
            var option = "remove_all";
10068
            if (status == 1) {
10069
                option = "add_all";
10070
            }
10071
            $.ajax({
10072
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10073
                data: "option="+option+"&exe_id='.$exeId.'&action="+action,
10074
                success: function(returnValue) {
10075
                }
10076
            });
10077
        }
10078
10079
        function reviewQuestions() {
10080
            var isChecked = 1;
10081
            $("input[type=checkbox]").each(function () {
10082
                if ($(this).prop("checked")) {
10083
                    isChecked = 2;
10084
                    return false;
10085
                }
10086
            });
10087
10088
            if (isChecked == 1) {
10089
                $("#message").addClass("warning-message");
10090
                $("#message").html("'.addslashes(get_lang('SelectAQuestionToReview')).'");
10091
            } else {
10092
                window.location = "exercise_submit.php?'.api_get_cidreq().'&category_id='.$categoryId.'&exerciseId='.$exerciseId.'&reminder=2&" + lp_data;
10093
            }
10094
        }
10095
10096
        function save_remind_item(obj, question_id) {
10097
            var action = "";
10098
            if ($(obj).prop("checked")) {
10099
                action = "add";
10100
            } else {
10101
                action = "delete";
10102
            }
10103
            $.ajax({
10104
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10105
                data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
10106
                success: function(returnValue) {
10107
                }
10108
            });
10109
        }
10110
        </script>';
10111
10112
        return $content;
10113
    }
10114
10115
    public function getRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10116
    {
10117
        $dataSet = [];
10118
        $labels = [];
10119
        $labelsWithId = [];
10120
        /** @var Exercise $exercise */
10121
        foreach ($exercises as $exercise) {
10122
            if (empty($labels)) {
10123
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10124
                if (!empty($categoryNameList)) {
10125
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10126
                    asort($labelsWithId);
10127
                    $labels = array_values($labelsWithId);
10128
                }
10129
            }
10130
10131
            foreach ($userList as $userId) {
10132
                $results = Event::getExerciseResultsByUser(
10133
                    $userId,
10134
                    $exercise->iId,
10135
                    $courseId,
10136
                    $sessionId
10137
                );
10138
10139
                if ($results) {
10140
                    $firstAttempt = end($results);
10141
                    $exeId = $firstAttempt['exe_id'];
10142
10143
                    ob_start();
10144
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10145
                        $exercise,
10146
                        $exeId,
10147
                        false
10148
                    );
10149
                    ob_end_clean();
10150
10151
                    $categoryList = $stats['category_list'];
10152
                    $tempResult = [];
10153
                    foreach ($labelsWithId as $category_id => $title) {
10154
                        if (isset($categoryList[$category_id])) {
10155
                            $category_item = $categoryList[$category_id];
10156
                            $tempResult[] = round($category_item['score'] / $category_item['total'] * 10);
10157
                        } else {
10158
                            $tempResult[] = 0;
10159
                        }
10160
                    }
10161
                    $dataSet[] = $tempResult;
10162
                }
10163
            }
10164
        }
10165
10166
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10167
    }
10168
10169
    public function getAverageRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10170
    {
10171
        $dataSet = [];
10172
        $labels = [];
10173
        $labelsWithId = [];
10174
10175
        $tempResult = [];
10176
        /** @var Exercise $exercise */
10177
        foreach ($exercises as $exercise) {
10178
            $exerciseId = $exercise->iId;
10179
            if (empty($labels)) {
10180
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10181
                if (!empty($categoryNameList)) {
10182
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10183
                    asort($labelsWithId);
10184
                    $labels = array_values($labelsWithId);
10185
                }
10186
            }
10187
10188
            foreach ($userList as $userId) {
10189
                $results = Event::getExerciseResultsByUser(
10190
                    $userId,
10191
                    $exerciseId,
10192
                    $courseId,
10193
                    $sessionId
10194
                );
10195
10196
                if ($results) {
10197
                    $firstAttempt = end($results);
10198
                    $exeId = $firstAttempt['exe_id'];
10199
10200
                    ob_start();
10201
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10202
                        $exercise,
10203
                        $exeId,
10204
                        false
10205
                    );
10206
                    ob_end_clean();
10207
10208
                    $categoryList = $stats['category_list'];
10209
                    foreach ($labelsWithId as $category_id => $title) {
10210
                        if (isset($categoryList[$category_id])) {
10211
                            $category_item = $categoryList[$category_id];
10212
                            if (!isset($tempResult[$exerciseId][$category_id])) {
10213
                                $tempResult[$exerciseId][$category_id] = 0;
10214
                            }
10215
                            $tempResult[$exerciseId][$category_id] += $category_item['score'] / $category_item['total'] * 10;
10216
                        }
10217
                    }
10218
                }
10219
            }
10220
        }
10221
10222
        $totalUsers = count($userList);
10223
10224
        foreach ($exercises as $exercise) {
10225
            $exerciseId = $exercise->iId;
10226
            $data = [];
10227
            foreach ($labelsWithId as $category_id => $title) {
10228
                if (isset($tempResult[$exerciseId]) && isset($tempResult[$exerciseId][$category_id])) {
10229
                    $data[] = round($tempResult[$exerciseId][$category_id] / $totalUsers);
10230
                } else {
10231
                    $data[] = 0;
10232
                }
10233
            }
10234
            $dataSet[] = $data;
10235
        }
10236
10237
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10238
    }
10239
10240
    public function getRadar($labels, $dataSet, $dataSetLabels = [])
10241
    {
10242
        if (empty($labels) || empty($dataSet)) {
10243
            return '';
10244
        }
10245
10246
        $displayLegend = 0;
10247
        if (!empty($dataSetLabels)) {
10248
            $displayLegend = 1;
10249
        }
10250
10251
        $labels = json_encode($labels);
10252
10253
        $colorList = ChamiloApi::getColorPalette(true, true);
10254
10255
        $dataSetToJson = [];
10256
        $counter = 0;
10257
        foreach ($dataSet as $index => $resultsArray) {
10258
            $color = isset($colorList[$counter]) ? $colorList[$counter] : 'rgb('.rand(0, 255).', '.rand(0, 255).', '.rand(0, 255).', 1.0)';
10259
10260
            $label = isset($dataSetLabels[$index]) ? $dataSetLabels[$index] : '';
10261
            $background = str_replace('1.0', '0.2', $color);
10262
            $dataSetToJson[] = [
10263
                'fill' => false,
10264
                'label' => $label,
10265
                'backgroundColor' => $background,
10266
                'borderColor' => $color,
10267
                'pointBackgroundColor' => $color,
10268
                'pointBorderColor' => '#fff',
10269
                'pointHoverBackgroundColor' => '#fff',
10270
                'pointHoverBorderColor' => $color,
10271
                'pointRadius' => 6,
10272
                'pointBorderWidth' => 3,
10273
                'pointHoverRadius' => 10,
10274
                'data' => $resultsArray,
10275
            ];
10276
            $counter++;
10277
        }
10278
        $resultsToJson = json_encode($dataSetToJson);
10279
10280
        return "
10281
                <canvas id='categoryRadar' height='200'></canvas>
10282
                <script>
10283
                    var data = {
10284
                        labels: $labels,
10285
                        datasets: $resultsToJson
10286
                    }
10287
                    var options = {
10288
                        responsive: true,
10289
                        scale: {
10290
                            angleLines: {
10291
                                display: false
10292
                            },
10293
                            ticks: {
10294
                                beginAtZero: true,
10295
                                  min: 0,
10296
                                  max: 10,
10297
                                stepSize: 1,
10298
                            },
10299
                            pointLabels: {
10300
                              fontSize: 14,
10301
                              //fontStyle: 'bold'
10302
                            },
10303
                        },
10304
                        elements: {
10305
                            line: {
10306
                                tension: 0,
10307
                                borderWidth: 3
10308
                            }
10309
                        },
10310
                        legend: {
10311
                            //position: 'bottom'
10312
                            display: $displayLegend
10313
                        },
10314
                        animation: {
10315
                            animateScale: true,
10316
                            animateRotate: true
10317
                        },
10318
                    };
10319
                    var ctx = document.getElementById('categoryRadar').getContext('2d');
10320
                    var myRadarChart = new Chart(ctx, {
10321
                        type: 'radar',
10322
                        data: data,
10323
                        options: options
10324
                    });
10325
                </script>
10326
                ";
10327
    }
10328
10329
    /**
10330
     * Returns true if the exercise is locked by percentage. an exercise attempt must be passed.
10331
     */
10332
    public function isBlockedByPercentage(array $attempt = []): bool
10333
    {
10334
        if (empty($attempt)) {
10335
            return false;
10336
        }
10337
        $extraFieldValue = new ExtraFieldValue('exercise');
10338
        $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
10339
            $this->iId,
10340
            'blocking_percentage'
10341
        );
10342
10343
        if (empty($blockExercise['value'])) {
10344
            return false;
10345
        }
10346
10347
        $blockPercentage = (int) $blockExercise['value'];
10348
10349
        if (0 === $blockPercentage) {
10350
            return false;
10351
        }
10352
10353
        $resultPercentage = 0;
10354
10355
        if (isset($attempt['score']) && isset($attempt['max_score'])) {
10356
            $weight = (int) $attempt['max_score'];
10357
            $weight = (0 == $weight) ? 1 : $weight;
10358
            $resultPercentage = float_format(
10359
                ($attempt['score'] / $weight) * 100,
10360
                1
10361
            );
10362
        }
10363
        if ($resultPercentage <= $blockPercentage) {
10364
            return true;
10365
        }
10366
10367
        return false;
10368
    }
10369
10370
    /**
10371
     * Gets the question list ordered by the question_order setting (drag and drop).
10372
     *
10373
     * @param bool $adminView Optional.
10374
     *
10375
     * @return array
10376
     */
10377
    public function getQuestionOrderedList($adminView = false)
10378
    {
10379
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
10380
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
10381
10382
        // Getting question_order to verify that the question
10383
        // list is correct and all question_order's were set
10384
        $sql = "SELECT DISTINCT count(e.question_order) as count
10385
                FROM $TBL_EXERCICE_QUESTION e
10386
                INNER JOIN $TBL_QUESTIONS q
10387
                ON (e.question_id = q.iid)
10388
                WHERE
10389
                  e.quiz_id	= ".$this->getId();
10390
10391
        $result = Database::query($sql);
10392
        $row = Database::fetch_array($result);
10393
        $count_question_orders = $row['count'];
10394
10395
        // Getting question list from the order (question list drag n drop interface).
10396
        $sql = "SELECT DISTINCT e.question_id, e.question_order
10397
                FROM $TBL_EXERCICE_QUESTION e
10398
                INNER JOIN $TBL_QUESTIONS q
10399
                ON (e.question_id = q.iid)
10400
                WHERE
10401
10402
                    e.quiz_id = '".$this->getId()."'
10403
                ORDER BY question_order";
10404
        $result = Database::query($sql);
10405
10406
        // Fills the array with the question ID for this exercise
10407
        // the key of the array is the question position
10408
        $temp_question_list = [];
10409
        $counter = 1;
10410
        $questionList = [];
10411
        while ($new_object = Database::fetch_object($result)) {
10412
            if (!$adminView) {
10413
                // Correct order.
10414
                $questionList[$new_object->question_order] = $new_object->question_id;
10415
            } else {
10416
                $questionList[$counter] = $new_object->question_id;
10417
            }
10418
10419
            // Just in case we save the order in other array
10420
            $temp_question_list[$counter] = $new_object->question_id;
10421
            $counter++;
10422
        }
10423
10424
        if (!empty($temp_question_list)) {
10425
            /* If both array don't match it means that question_order was not correctly set
10426
               for all questions using the default mysql order */
10427
            if (count($temp_question_list) != $count_question_orders) {
10428
                $questionList = $temp_question_list;
10429
            }
10430
        }
10431
10432
        return $questionList;
10433
    }
10434
10435
    /**
10436
     * Get number of questions in exercise by user attempt.
10437
     *
10438
     * @return int
10439
     */
10440
    private function countQuestionsInExercise()
10441
    {
10442
        $lpId = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10443
        $lpItemId = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10444
        $lpItemViewId = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10445
10446
        $trackInfo = $this->get_stat_track_exercise_info($lpId, $lpItemId, $lpItemViewId);
10447
10448
        if (!empty($trackInfo)) {
10449
            $questionIds = explode(',', $trackInfo['data_tracking']);
10450
10451
            return count($questionIds);
10452
        }
10453
10454
        return $this->getQuestionCount();
10455
    }
10456
10457
    /**
10458
     * Select N values from the questions per category array.
10459
     *
10460
     * @param array $categoriesAddedInExercise
10461
     * @param array $question_list
10462
     * @param array $questions_by_category
10463
     * @param bool  $flatResult
10464
     * @param bool  $randomizeQuestions
10465
     * @param array $questionsByCategoryMandatory
10466
     *
10467
     * @return array
10468
     */
10469
    private function pickQuestionsPerCategory(
10470
        $categoriesAddedInExercise,
10471
        $question_list,
10472
        &$questions_by_category,
10473
        $flatResult = true,
10474
        $randomizeQuestions = false,
10475
        $questionsByCategoryMandatory = []
10476
    ) {
10477
        $addAll = true;
10478
        $categoryCountArray = [];
10479
10480
        // Getting how many questions will be selected per category.
10481
        if (!empty($categoriesAddedInExercise)) {
10482
            $addAll = false;
10483
            // Parsing question according the category rel exercise settings
10484
            foreach ($categoriesAddedInExercise as $category_info) {
10485
                $category_id = $category_info['category_id'];
10486
                if (isset($questions_by_category[$category_id])) {
10487
                    // How many question will be picked from this category.
10488
                    $count = $category_info['count_questions'];
10489
                    // -1 means all questions
10490
                    $categoryCountArray[$category_id] = $count;
10491
                    if (-1 == $count) {
10492
                        $categoryCountArray[$category_id] = 999;
10493
                    }
10494
                }
10495
            }
10496
        }
10497
10498
        if (!empty($questions_by_category)) {
10499
            $temp_question_list = [];
10500
            foreach ($questions_by_category as $category_id => &$categoryQuestionList) {
10501
                if (isset($categoryCountArray) && !empty($categoryCountArray)) {
10502
                    $numberOfQuestions = 0;
10503
                    if (isset($categoryCountArray[$category_id])) {
10504
                        $numberOfQuestions = $categoryCountArray[$category_id];
10505
                    }
10506
                }
10507
10508
                if ($addAll) {
10509
                    $numberOfQuestions = 999;
10510
                }
10511
                if (!empty($numberOfQuestions)) {
10512
                    $mandatoryQuestions = [];
10513
                    if (isset($questionsByCategoryMandatory[$category_id])) {
10514
                        $mandatoryQuestions = $questionsByCategoryMandatory[$category_id];
10515
                    }
10516
10517
                    $elements = TestCategory::getNElementsFromArray(
10518
                        $categoryQuestionList,
10519
                        $numberOfQuestions,
10520
                        $randomizeQuestions,
10521
                        $mandatoryQuestions
10522
                    );
10523
10524
                    if (!empty($elements)) {
10525
                        $temp_question_list[$category_id] = $elements;
10526
                        $categoryQuestionList = $elements;
10527
                    }
10528
                }
10529
            }
10530
10531
            if (!empty($temp_question_list)) {
10532
                if ($flatResult) {
10533
                    $temp_question_list = array_flatten($temp_question_list);
10534
                }
10535
                $question_list = $temp_question_list;
10536
            }
10537
        }
10538
10539
        return $question_list;
10540
    }
10541
10542
    /**
10543
     * Sends a notification when a user ends an examn.
10544
     *
10545
     * @param array  $question_list_answers
10546
     * @param string $origin
10547
     * @param array  $user_info
10548
     * @param string $url_email
10549
     * @param array  $teachers
10550
     */
10551
    private function sendNotificationForOpenQuestions(
10552
        $question_list_answers,
10553
        $origin,
10554
        $user_info,
10555
        $url_email,
10556
        $teachers
10557
    ) {
10558
        // Email configuration settings
10559
        $courseCode = api_get_course_id();
10560
        $courseInfo = api_get_course_info($courseCode);
10561
        $sessionId = api_get_session_id();
10562
        $sessionData = '';
10563
        if (!empty($sessionId)) {
10564
            $sessionInfo = api_get_session_info($sessionId);
10565
            if (!empty($sessionInfo)) {
10566
                $sessionData = '<tr>'
10567
                    .'<td><em>'.get_lang('Session name').'</em></td>'
10568
                    .'<td>&nbsp;<b>'.$sessionInfo['name'].'</b></td>'
10569
                    .'</tr>';
10570
            }
10571
        }
10572
10573
        $msg = get_lang('A learner has answered an open question').'<br /><br />'
10574
            .get_lang('Attempt details').' : <br /><br />'
10575
            .'<table>'
10576
            .'<tr>'
10577
            .'<td><em>'.get_lang('Course name').'</em></td>'
10578
            .'<td>&nbsp;<b>#course#</b></td>'
10579
            .'</tr>'
10580
            .$sessionData
10581
            .'<tr>'
10582
            .'<td>'.get_lang('Test attempted').'</td>'
10583
            .'<td>&nbsp;#exercise#</td>'
10584
            .'</tr>'
10585
            .'<tr>'
10586
            .'<td>'.get_lang('Learner name').'</td>'
10587
            .'<td>&nbsp;#firstName# #lastName#</td>'
10588
            .'</tr>'
10589
            .'<tr>'
10590
            .'<td>'.get_lang('Learner e-mail').'</td>'
10591
            .'<td>&nbsp;#mail#</td>'
10592
            .'</tr>'
10593
            .'</table>';
10594
10595
        $open_question_list = null;
10596
        foreach ($question_list_answers as $item) {
10597
            $question = $item['question'];
10598
            $answer = $item['answer'];
10599
            $answer_type = $item['answer_type'];
10600
10601
            if (!empty($question) && !empty($answer) && FREE_ANSWER == $answer_type) {
10602
                $open_question_list .=
10603
                    '<tr>
10604
                    <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
10605
                    <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
10606
                    </tr>
10607
                    <tr>
10608
                    <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
10609
                    <td valign="top" bgcolor="#F3F3F3">'.$answer.'</td>
10610
                    </tr>';
10611
            }
10612
        }
10613
10614
        if (!empty($open_question_list)) {
10615
            $msg .= '<p><br />'.get_lang('A learner has answered an open questionAre').' :</p>'.
10616
                '<table width="730" height="136" border="0" cellpadding="3" cellspacing="3">';
10617
            $msg .= $open_question_list;
10618
            $msg .= '</table><br />';
10619
10620
            $msg = str_replace('#exercise#', $this->exercise, $msg);
10621
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg);
10622
            $msg = str_replace('#lastName#', $user_info['lastname'], $msg);
10623
            $msg = str_replace('#mail#', $user_info['email'], $msg);
10624
            $msg = str_replace(
10625
                '#course#',
10626
                Display::url($courseInfo['title'], $courseInfo['course_public_url'].'?sid='.$sessionId),
10627
                $msg
10628
            );
10629
10630
            if ('learnpath' !== $origin) {
10631
                $msg .= '<br /><a href="#url#">'.get_lang(
10632
                        'Click this link to check the answer and/or give feedback'
10633
                    ).'</a>';
10634
            }
10635
            $msg = str_replace('#url#', $url_email, $msg);
10636
            $subject = get_lang('A learner has answered an open question');
10637
10638
            if (!empty($teachers)) {
10639
                foreach ($teachers as $user_id => $teacher_data) {
10640
                    MessageManager::send_message_simple(
10641
                        $user_id,
10642
                        $subject,
10643
                        $msg
10644
                    );
10645
                }
10646
            }
10647
        }
10648
    }
10649
10650
    /**
10651
     * Send notification for oral questions.
10652
     *
10653
     * @param array  $question_list_answers
10654
     * @param string $origin
10655
     * @param int    $exe_id
10656
     * @param array  $user_info
10657
     * @param string $url_email
10658
     * @param array  $teachers
10659
     */
10660
    private function sendNotificationForOralQuestions(
10661
        $question_list_answers,
10662
        $origin,
10663
        $exe_id,
10664
        $user_info,
10665
        $url_email,
10666
        $teachers
10667
    ) {
10668
        // Email configuration settings
10669
        $courseCode = api_get_course_id();
10670
        $courseInfo = api_get_course_info($courseCode);
10671
        $oral_question_list = null;
10672
        foreach ($question_list_answers as $item) {
10673
            $question = $item['question'];
10674
            $file = $item['generated_oral_file'];
10675
            $answer = $item['answer'];
10676
            if (0 == $answer) {
10677
                $answer = '';
10678
            }
10679
            $answer_type = $item['answer_type'];
10680
            if (!empty($question) && (!empty($answer) || !empty($file)) && ORAL_EXPRESSION == $answer_type) {
10681
                if (!empty($file)) {
10682
                    $file = Display::url($file, $file);
10683
                }
10684
                $oral_question_list .= '<br />
10685
                    <table width="730" height="136" border="0" cellpadding="3" cellspacing="3">
10686
                    <tr>
10687
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
10688
                        <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
10689
                    </tr>
10690
                    <tr>
10691
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
10692
                        <td valign="top" bgcolor="#F3F3F3">'.$answer.$file.'</td>
10693
                    </tr></table>';
10694
            }
10695
        }
10696
10697
        if (!empty($oral_question_list)) {
10698
            $msg = get_lang('A learner has attempted one or more oral question').'<br /><br />
10699
                    '.get_lang('Attempt details').' : <br /><br />
10700
                    <table>
10701
                        <tr>
10702
                            <td><em>'.get_lang('Course name').'</em></td>
10703
                            <td>&nbsp;<b>#course#</b></td>
10704
                        </tr>
10705
                        <tr>
10706
                            <td>'.get_lang('Test attempted').'</td>
10707
                            <td>&nbsp;#exercise#</td>
10708
                        </tr>
10709
                        <tr>
10710
                            <td>'.get_lang('Learner name').'</td>
10711
                            <td>&nbsp;#firstName# #lastName#</td>
10712
                        </tr>
10713
                        <tr>
10714
                            <td>'.get_lang('Learner e-mail').'</td>
10715
                            <td>&nbsp;#mail#</td>
10716
                        </tr>
10717
                    </table>';
10718
            $msg .= '<br />'.sprintf(
10719
                    get_lang('A learner has attempted one or more oral questionAreX'),
10720
                    $oral_question_list
10721
                ).'<br />';
10722
            $msg1 = str_replace('#exercise#', $this->exercise, $msg);
10723
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg1);
10724
            $msg1 = str_replace('#lastName#', $user_info['lastname'], $msg);
10725
            $msg = str_replace('#mail#', $user_info['email'], $msg1);
10726
            $msg = str_replace('#course#', $courseInfo['name'], $msg1);
10727
10728
            if (!in_array($origin, ['learnpath', 'embeddable'])) {
10729
                $msg .= '<br /><a href="#url#">'.get_lang(
10730
                        'Click this link to check the answer and/or give feedback'
10731
                    ).'</a>';
10732
            }
10733
            $msg1 = str_replace('#url#', $url_email, $msg);
10734
            $mail_content = $msg1;
10735
            $subject = get_lang('A learner has attempted one or more oral question');
10736
10737
            if (!empty($teachers)) {
10738
                foreach ($teachers as $user_id => $teacher_data) {
10739
                    MessageManager::send_message_simple(
10740
                        $user_id,
10741
                        $subject,
10742
                        $mail_content
10743
                    );
10744
                }
10745
            }
10746
        }
10747
    }
10748
10749
    /**
10750
     * Returns an array with the media list.
10751
     *
10752
     * @param array $questionList question list
10753
     *
10754
     * @example there's 1 question with iid 5 that belongs to the media question with iid = 100
10755
     * <code>
10756
     * array (size=2)
10757
     *  999 =>
10758
     *    array (size=3)
10759
     *      0 => int 7
10760
     *      1 => int 6
10761
     *      2 => int 3254
10762
     *  100 =>
10763
     *   array (size=1)
10764
     *      0 => int 5
10765
     *  </code>
10766
     */
10767
    private function setMediaList($questionList)
10768
    {
10769
        $mediaList = [];
10770
        /*
10771
         * Media feature is not activated in 1.11.x
10772
        if (!empty($questionList)) {
10773
            foreach ($questionList as $questionId) {
10774
                $objQuestionTmp = Question::read($questionId, $this->course_id);
10775
                // If a media question exists
10776
                if (isset($objQuestionTmp->parent_id) && $objQuestionTmp->parent_id != 0) {
10777
                    $mediaList[$objQuestionTmp->parent_id][] = $objQuestionTmp->id;
10778
                } else {
10779
                    // Always the last item
10780
                    $mediaList[999][] = $objQuestionTmp->id;
10781
                }
10782
            }
10783
        }*/
10784
10785
        $this->mediaList = $mediaList;
10786
    }
10787
10788
    /**
10789
     * @return HTML_QuickForm_group
10790
     */
10791
    private function setResultDisabledGroup(FormValidator $form)
10792
    {
10793
        $resultDisabledGroup = [];
10794
10795
        $resultDisabledGroup[] = $form->createElement(
10796
            'radio',
10797
            'results_disabled',
10798
            null,
10799
            get_lang('Auto-evaluation mode: show score and expected answers'),
10800
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
10801
            ['id' => 'result_disabled_0']
10802
        );
10803
10804
        $warning = sprintf(
10805
            get_lang('TheSettingXWillChangeToX'),
10806
            get_lang('FeedbackType'),
10807
            get_lang('NoFeedback')
10808
        );
10809
        $resultDisabledGroup[] = $form->createElement(
10810
            'radio',
10811
            'results_disabled',
10812
            null,
10813
            get_lang('Exam mode: Do not show score nor answers'),
10814
            RESULT_DISABLE_NO_SCORE_AND_EXPECTED_ANSWERS,
10815
            [
10816
                'id' => 'result_disabled_1',
10817
                //'onclick' => 'check_results_disabled()'
10818
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
10819
            ]
10820
        );
10821
10822
        $resultDisabledGroup[] = $form->createElement(
10823
            'radio',
10824
            'results_disabled',
10825
            null,
10826
            get_lang('Practice mode: Show score only, by category if at least one is used'),
10827
            RESULT_DISABLE_SHOW_SCORE_ONLY,
10828
            [
10829
                'id' => 'result_disabled_2',
10830
                //'onclick' => 'check_results_disabled()'
10831
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
10832
            ]
10833
        );
10834
10835
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
10836
            return $form->addGroup(
10837
                $resultDisabledGroup,
10838
                null,
10839
                get_lang(
10840
                    'Show score to learner'
10841
                )
10842
            );
10843
        }
10844
10845
        $resultDisabledGroup[] = $form->createElement(
10846
            'radio',
10847
            'results_disabled',
10848
            null,
10849
            get_lang(
10850
                'Show score on every attempt, show correct answers only on last attempt (only works with an attempts limit)'
10851
            ),
10852
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
10853
            ['id' => 'result_disabled_4']
10854
        );
10855
10856
        $resultDisabledGroup[] = $form->createElement(
10857
            'radio',
10858
            'results_disabled',
10859
            null,
10860
            get_lang(
10861
                'Do not show the score (only when user finishes all attempts) but show feedback for each attempt.'
10862
            ),
10863
            RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
10864
            [
10865
                'id' => 'result_disabled_5',
10866
                //'onclick' => 'check_results_disabled()'
10867
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
10868
            ]
10869
        );
10870
10871
        $resultDisabledGroup[] = $form->createElement(
10872
            'radio',
10873
            'results_disabled',
10874
            null,
10875
            get_lang(
10876
                'Ranking mode: Do not show results details question by question and show a table with the ranking of all other users.'
10877
            ),
10878
            RESULT_DISABLE_RANKING,
10879
            ['id' => 'result_disabled_6']
10880
        );
10881
10882
        $resultDisabledGroup[] = $form->createElement(
10883
            'radio',
10884
            'results_disabled',
10885
            null,
10886
            get_lang(
10887
                'Show only global score (not question score) and show only the correct answers, do not show incorrect answers at all'
10888
            ),
10889
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
10890
            ['id' => 'result_disabled_7']
10891
        );
10892
10893
        $resultDisabledGroup[] = $form->createElement(
10894
            'radio',
10895
            'results_disabled',
10896
            null,
10897
            get_lang('Auto-evaluation mode and ranking'),
10898
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
10899
            ['id' => 'result_disabled_8']
10900
        );
10901
10902
        $resultDisabledGroup[] = $form->createElement(
10903
            'radio',
10904
            'results_disabled',
10905
            null,
10906
            get_lang('Show score by category on a radar/spiderweb chart'),
10907
            RESULT_DISABLE_RADAR,
10908
            ['id' => 'result_disabled_9']
10909
        );
10910
10911
        $resultDisabledGroup[] = $form->createElement(
10912
            'radio',
10913
            'results_disabled',
10914
            null,
10915
            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.'),
10916
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
10917
            ['id' => 'result_disabled_10']
10918
        );
10919
10920
        return $form->addGroup(
10921
            $resultDisabledGroup,
10922
            null,
10923
            get_lang('Show score to learner')
10924
        );
10925
    }
10926
10927
    /**
10928
     * Return the text to display, based on the score and the max score.
10929
     * @param int|float $score
10930
     * @param int|float $maxScore
10931
     * @return string
10932
     */
10933
    public function getFinishText(int|float $score, int|float $maxScore): string
10934
    {
10935
        $passPercentage = $this->selectPassPercentage();
10936
        if (!empty($passPercentage)) {
10937
            $percentage = float_format(
10938
                ($score / (0 != $maxScore ? $maxScore : 1)) * 100,
10939
                1
10940
            );
10941
            if ($percentage >= $passPercentage) {
10942
                return $this->getTextWhenFinished();
10943
            } else {
10944
                return $this->getTextWhenFinishedFailure();
10945
            }
10946
        } else {
10947
            return $this->getTextWhenFinished();
10948
        }
10949
10950
        return '';
0 ignored issues
show
Unused Code introduced by
return '' is not reachable.

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

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

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

    return false;
}

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

Loading history...
10951
    }
10952
}
10953