Passed
Pull Request — 1.11.x (#6306)
by Yannick
18:44
created

Exercise::setShowPreviousButton()   A

Complexity

Conditions 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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