Issues (2037)

main/exercise/exercise.class.php (1 issue)

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