Passed
Push — master ( 1c618d...c1c6b0 )
by Yannick
10:36 queued 02:52
created

Exercise::showSimpleTimeControl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 42
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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