Issues (2160)

main/exercise/exercise.class.php (9 issues)

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
6
use Chamilo\CoreBundle\Entity\GradebookLink;
7
use Chamilo\CoreBundle\Entity\TrackEExerciseConfirmation;
8
use Chamilo\CoreBundle\Entity\TrackEHotspot;
9
use Chamilo\CourseBundle\Entity\CExerciseCategory;
10
use ChamiloSession as Session;
11
use Doctrine\DBAL\Types\Type;
12
13
/**
14
 * Class Exercise.
15
 *
16
 * Allows to instantiate an object of type Exercise
17
 *
18
 * @todo use getters and setters correctly
19
 *
20
 * @author Olivier Brouckaert
21
 * @author Julio Montoya Cleaning exercises
22
 * Modified by Hubert Borderiou #294
23
 */
24
class Exercise
25
{
26
    public const PAGINATION_ITEMS_PER_PAGE = 20;
27
    public $iid;
28
    public $name;
29
    public $title;
30
    public $exercise;
31
    public $description;
32
    public $sound;
33
    public $type; //ALL_ON_ONE_PAGE or ONE_PER_PAGE
34
    public $random;
35
    public $random_answers;
36
    public $active;
37
    public $timeLimit;
38
    public $attempts;
39
    public $feedback_type;
40
    public $end_time;
41
    public $start_time;
42
    public $questionList; // array with the list of this exercise's questions
43
    /* including question list of the media */
44
    public $questionListUncompressed;
45
    public $results_disabled;
46
    public $expired_time;
47
    public $course;
48
    public $course_id;
49
    public $propagate_neg;
50
    public $saveCorrectAnswers;
51
    public $review_answers;
52
    public $randomByCat;
53
    public $text_when_finished;
54
    public $text_when_finished_failure;
55
    public $display_category_name;
56
    public $pass_percentage;
57
    public $edit_exercise_in_lp = false;
58
    public $is_gradebook_locked = false;
59
    public $exercise_was_added_in_lp = false;
60
    public $lpList = [];
61
    public $force_edit_exercise_in_lp = false;
62
    public $categories;
63
    public $categories_grouping = true;
64
    public $endButton = 0;
65
    public $categoryWithQuestionList;
66
    public $mediaList;
67
    public $loadQuestionAJAX = false;
68
    // Notification send to the teacher.
69
    public $emailNotificationTemplate = null;
70
    // Notification send to the student.
71
    public $emailNotificationTemplateToUser = null;
72
    public $countQuestions = 0;
73
    public $fastEdition = false;
74
    public $modelType = 1;
75
    public $questionSelectionType = EX_Q_SELECTION_ORDERED;
76
    public $hideQuestionTitle = 0;
77
    public $scoreTypeModel = 0;
78
    public $categoryMinusOne = true; // Shows the category -1: See BT#6540
79
    public $globalCategoryId = null;
80
    public $onSuccessMessage = null;
81
    public $onFailedMessage = null;
82
    public $emailAlert;
83
    public $notifyUserByEmail = '';
84
    public $sessionId = 0;
85
    public $questionFeedbackEnabled = false;
86
    public $questionTypeWithFeedback;
87
    public $showPreviousButton;
88
    public $notifications;
89
    public $export = false;
90
    public $autolaunch;
91
    public $exerciseCategoryId;
92
    public $pageResultConfiguration;
93
    public $hideQuestionNumber;
94
    public $preventBackwards;
95
    public $currentQuestion;
96
    public $hideComment;
97
    public $hideNoAnswer;
98
    public $hideExpectedAnswer;
99
    public $forceShowExpectedChoiceColumn;
100
    public $disableHideCorrectAnsweredQuestions;
101
    public $hideAttemptsTableOnStartPage;
102
103
    /**
104
     * Constructor of the class.
105
     *
106
     * @param int $courseId
107
     *
108
     * @author Olivier Brouckaert
109
     */
110
    public function __construct($courseId = 0)
111
    {
112
        $this->iid = 0;
113
        $this->exercise = '';
114
        $this->description = '';
115
        $this->sound = '';
116
        $this->type = ALL_ON_ONE_PAGE;
117
        $this->random = 0;
118
        $this->random_answers = 0;
119
        $this->active = 1;
120
        $this->questionList = [];
121
        $this->timeLimit = 0;
122
        $this->end_time = '';
123
        $this->start_time = '';
124
        $this->results_disabled = 1;
125
        $this->expired_time = 0;
126
        $this->propagate_neg = 0;
127
        $this->saveCorrectAnswers = 0;
128
        $this->review_answers = false;
129
        $this->randomByCat = 0;
130
        $this->text_when_finished = '';
131
        $this->text_when_finished_failure = '';
132
        $this->display_category_name = 0;
133
        $this->pass_percentage = 0;
134
        $this->modelType = 1;
135
        $this->questionSelectionType = EX_Q_SELECTION_ORDERED;
136
        $this->endButton = 0;
137
        $this->scoreTypeModel = 0;
138
        $this->globalCategoryId = null;
139
        $this->notifications = [];
140
        $this->exerciseCategoryId = null;
141
        $this->pageResultConfiguration;
142
        $this->hideQuestionNumber = 0;
143
        $this->preventBackwards = 0;
144
        $this->hideComment = false;
145
        $this->hideNoAnswer = false;
146
        $this->hideExpectedAnswer = false;
147
        $this->disableHideCorrectAnsweredQuestions = false;
148
        $this->hideAttemptsTableOnStartPage = 0;
149
150
        if (!empty($courseId)) {
151
            $courseInfo = api_get_course_info_by_id($courseId);
152
        } else {
153
            $courseInfo = api_get_course_info();
154
        }
155
        $this->course_id = $courseInfo['real_id'];
156
        $this->course = $courseInfo;
157
        $this->sessionId = api_get_session_id();
158
159
        // ALTER TABLE c_quiz_question ADD COLUMN feedback text;
160
        $this->questionFeedbackEnabled = api_get_configuration_value('allow_quiz_question_feedback');
161
        $this->showPreviousButton = true;
162
    }
163
164
    /**
165
     * Reads exercise information from the data base.
166
     *
167
     * @author Olivier Brouckaert
168
     *
169
     * @param int  $id                - exercise Id
170
     * @param bool $parseQuestionList
171
     *
172
     * @return bool - true if exercise exists, otherwise false
173
     */
174
    public function read($id, $parseQuestionList = true)
175
    {
176
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
177
178
        $id = (int) $id;
179
        if (empty($this->course_id)) {
180
            return false;
181
        }
182
183
        $sql = "SELECT * FROM $table WHERE iid = ".$id;
184
        $result = Database::query($sql);
185
186
        // if the exercise has been found
187
        if ($object = Database::fetch_object($result)) {
188
            $this->iid = $object->iid;
189
            $this->exercise = $object->title;
190
            $this->name = $object->title;
191
            $this->title = $object->title;
192
            $this->description = $object->description;
193
            $this->sound = $object->sound;
194
            $this->type = $object->type;
195
            if (empty($this->type)) {
196
                $this->type = ONE_PER_PAGE;
197
            }
198
            $this->random = $object->random;
199
            $this->random_answers = $object->random_answers;
200
            $this->active = $object->active;
201
            $this->results_disabled = $object->results_disabled;
202
            $this->attempts = $object->max_attempt;
203
            $this->feedback_type = $object->feedback_type;
204
            $this->sessionId = $object->session_id;
205
            $this->propagate_neg = $object->propagate_neg;
206
            $this->saveCorrectAnswers = $object->save_correct_answers;
207
            $this->randomByCat = $object->random_by_category;
208
            $this->text_when_finished = $object->text_when_finished;
209
            $this->text_when_finished_failure = isset($object->text_when_finished_failure) ? $object->text_when_finished_failure : null;
210
            $this->display_category_name = $object->display_category_name;
211
            $this->pass_percentage = $object->pass_percentage;
212
            $this->is_gradebook_locked = api_resource_is_locked_by_gradebook($id, LINK_EXERCISE);
213
            $this->review_answers = (isset($object->review_answers) && $object->review_answers == 1) ? true : false;
214
            $this->globalCategoryId = isset($object->global_category_id) ? $object->global_category_id : null;
215
            $this->questionSelectionType = isset($object->question_selection_type) ? (int) $object->question_selection_type : null;
216
            $this->hideQuestionTitle = isset($object->hide_question_title) ? (int) $object->hide_question_title : 0;
217
            $this->autolaunch = isset($object->autolaunch) ? (int) $object->autolaunch : 0;
218
            $this->exerciseCategoryId = isset($object->exercise_category_id) ? (int) $object->exercise_category_id : null;
219
            $this->preventBackwards = isset($object->prevent_backwards) ? (int) $object->prevent_backwards : 0;
220
            $this->exercise_was_added_in_lp = false;
221
            $this->lpList = [];
222
            $this->notifications = [];
223
            if (!empty($object->notifications)) {
224
                $this->notifications = explode(',', $object->notifications);
225
            }
226
227
            if (!empty($object->page_result_configuration)) {
228
                $this->pageResultConfiguration = $object->page_result_configuration;
229
            }
230
231
            if (isset($object->hide_question_number)) {
232
                $this->hideQuestionNumber = $object->hide_question_number == 1;
233
            }
234
235
            if (isset($object->hide_attempts_table)) {
236
                $this->hideAttemptsTableOnStartPage = $object->hide_attempts_table == 1;
237
            }
238
239
            if (isset($object->show_previous_button)) {
240
                $this->showPreviousButton = $object->show_previous_button == 1;
241
            }
242
243
            $list = self::getLpListFromExercise($id, $this->course_id);
244
            if (!empty($list)) {
245
                $this->exercise_was_added_in_lp = true;
246
                $this->lpList = $list;
247
            }
248
249
            $this->force_edit_exercise_in_lp = api_get_configuration_value('force_edit_exercise_in_lp');
250
            $this->edit_exercise_in_lp = true;
251
            if ($this->exercise_was_added_in_lp) {
252
                $this->edit_exercise_in_lp = $this->force_edit_exercise_in_lp == true;
253
            }
254
255
            if (!empty($object->end_time)) {
256
                $this->end_time = $object->end_time;
257
            }
258
            if (!empty($object->start_time)) {
259
                $this->start_time = $object->start_time;
260
            }
261
262
            // Control time
263
            $this->expired_time = $object->expired_time;
264
265
            // Checking if question_order is correctly set
266
            if ($parseQuestionList) {
267
                $this->setQuestionList(true);
268
            }
269
270
            //overload questions list with recorded questions list
271
            //load questions only for exercises of type 'one question per page'
272
            //this is needed only is there is no questions
273
274
            // @todo not sure were in the code this is used somebody mess with the exercise tool
275
            // @todo don't know who add that config and why $_configuration['live_exercise_tracking']
276
            /*global $_configuration, $questionList;
277
            if ($this->type == ONE_PER_PAGE && $_SERVER['REQUEST_METHOD'] != 'POST'
278
                && defined('QUESTION_LIST_ALREADY_LOGGED') &&
279
                isset($_configuration['live_exercise_tracking']) && $_configuration['live_exercise_tracking']
280
            ) {
281
                $this->questionList = $questionList;
282
            }*/
283
            return true;
284
        }
285
286
        return false;
287
    }
288
289
    /**
290
     * @return string
291
     */
292
    public function getCutTitle()
293
    {
294
        $title = $this->getUnformattedTitle();
295
296
        return cut($title, EXERCISE_MAX_NAME_SIZE);
297
    }
298
299
    /**
300
     * returns the exercise ID.
301
     *
302
     * @author Olivier Brouckaert
303
     *
304
     * @return int - exercise ID
305
     */
306
    public function selectId()
307
    {
308
        return $this->iid;
309
    }
310
311
    /**
312
     * returns the exercise title.
313
     *
314
     * @author Olivier Brouckaert
315
     *
316
     * @param bool $unformattedText Optional. Get the title without HTML tags
317
     *
318
     * @return string - exercise title
319
     */
320
    public function selectTitle($unformattedText = false)
321
    {
322
        if ($unformattedText) {
323
            return $this->getUnformattedTitle();
324
        }
325
326
        return $this->exercise;
327
    }
328
329
    /**
330
     * Returns the maximum number of attempts set in the exercise configuration.
331
     *
332
     * @return int Maximum attempts allowed (0 if no limit)
333
     */
334
    public function selectAttempts()
335
    {
336
        return $this->attempts;
337
    }
338
339
    /**
340
     * Returns the number of FeedbackType
341
     *  0: Feedback , 1: DirectFeedback, 2: NoFeedback.
342
     *
343
     * @return int - exercise attempts
344
     */
345
    public function getFeedbackType()
346
    {
347
        return (int) $this->feedback_type;
348
    }
349
350
    /**
351
     * returns the time limit.
352
     *
353
     * @return int
354
     */
355
    public function selectTimeLimit()
356
    {
357
        return $this->timeLimit;
358
    }
359
360
    /**
361
     * returns the exercise description.
362
     *
363
     * @author Olivier Brouckaert
364
     *
365
     * @return string - exercise description
366
     */
367
    public function selectDescription()
368
    {
369
        return $this->description;
370
    }
371
372
    /**
373
     * returns the exercise sound file.
374
     *
375
     * @author Olivier Brouckaert
376
     *
377
     * @return string - exercise description
378
     */
379
    public function selectSound()
380
    {
381
        return $this->sound;
382
    }
383
384
    /**
385
     * returns the exercise type.
386
     *
387
     * @author Olivier Brouckaert
388
     *
389
     * @return int - exercise type
390
     */
391
    public function selectType()
392
    {
393
        return $this->type;
394
    }
395
396
    /**
397
     * @return int
398
     */
399
    public function getModelType()
400
    {
401
        return $this->modelType;
402
    }
403
404
    /**
405
     * @return int
406
     */
407
    public function selectEndButton()
408
    {
409
        return $this->endButton;
410
    }
411
412
    /**
413
     * @author hubert borderiou 30-11-11
414
     *
415
     * @return int : do we display the question category name for students
416
     */
417
    public function selectDisplayCategoryName()
418
    {
419
        return $this->display_category_name;
420
    }
421
422
    /**
423
     * @return int
424
     */
425
    public function selectPassPercentage()
426
    {
427
        return $this->pass_percentage;
428
    }
429
430
    /**
431
     * Modify object to update the switch display_category_name.
432
     *
433
     * @author hubert borderiou 30-11-11
434
     *
435
     * @param int $value is an integer 0 or 1
436
     */
437
    public function updateDisplayCategoryName($value)
438
    {
439
        $this->display_category_name = $value;
440
    }
441
442
    /**
443
     * @author hubert borderiou 28-11-11
444
     *
445
     * @return string html text : the text to display ay the end of the test
446
     */
447
    public function getTextWhenFinished()
448
    {
449
        return $this->text_when_finished;
450
    }
451
452
    /**
453
     * @param string $text
454
     *
455
     * @author hubert borderiou 28-11-11
456
     */
457
    public function updateTextWhenFinished($text)
458
    {
459
        $this->text_when_finished = $text;
460
    }
461
462
    /**
463
     * Get the text to display when the user has failed the test.
464
     *
465
     * @return string html text : the text to display ay the end of the test
466
     */
467
    public function getTextWhenFinishedFailure(): string
468
    {
469
        if (empty($this->text_when_finished_failure)) {
470
            return '';
471
        }
472
473
        return $this->text_when_finished_failure;
474
    }
475
476
    /**
477
     * Set the text to display when the user has succeeded in the test.
478
     */
479
    public function setTextWhenFinishedFailure(string $text): void
480
    {
481
        $this->text_when_finished_failure = $text;
482
    }
483
484
    /**
485
     * return 1 or 2 if randomByCat.
486
     *
487
     * @author hubert borderiou
488
     *
489
     * @return int - quiz random by category
490
     */
491
    public function getRandomByCategory()
492
    {
493
        return $this->randomByCat;
494
    }
495
496
    /**
497
     * return 0 if no random by cat
498
     * return 1 if random by cat, categories shuffled
499
     * return 2 if random by cat, categories sorted by alphabetic order.
500
     *
501
     * @author hubert borderiou
502
     *
503
     * @return int - quiz random by category
504
     */
505
    public function isRandomByCat()
506
    {
507
        $res = EXERCISE_CATEGORY_RANDOM_DISABLED;
508
        if ($this->randomByCat == EXERCISE_CATEGORY_RANDOM_SHUFFLED) {
509
            $res = EXERCISE_CATEGORY_RANDOM_SHUFFLED;
510
        } elseif ($this->randomByCat == EXERCISE_CATEGORY_RANDOM_ORDERED) {
511
            $res = EXERCISE_CATEGORY_RANDOM_ORDERED;
512
        }
513
514
        return $res;
515
    }
516
517
    /**
518
     * return nothing
519
     * update randomByCat value for object.
520
     *
521
     * @param int $random
522
     *
523
     * @author hubert borderiou
524
     */
525
    public function updateRandomByCat($random)
526
    {
527
        $this->randomByCat = EXERCISE_CATEGORY_RANDOM_DISABLED;
528
        if (in_array(
529
            $random,
530
            [
531
                EXERCISE_CATEGORY_RANDOM_SHUFFLED,
532
                EXERCISE_CATEGORY_RANDOM_ORDERED,
533
                EXERCISE_CATEGORY_RANDOM_DISABLED,
534
            ]
535
        )) {
536
            $this->randomByCat = $random;
537
        }
538
    }
539
540
    /**
541
     * Tells if questions are selected randomly, and if so returns the draws.
542
     *
543
     * @author Carlos Vargas
544
     *
545
     * @return int - results disabled exercise
546
     */
547
    public function selectResultsDisabled()
548
    {
549
        return $this->results_disabled;
550
    }
551
552
    /**
553
     * tells if questions are selected randomly, and if so returns the draws.
554
     *
555
     * @author Olivier Brouckaert
556
     *
557
     * @return bool
558
     */
559
    public function isRandom()
560
    {
561
        $isRandom = false;
562
        // "-1" means all questions will be random
563
        if ($this->random > 0 || $this->random == -1) {
564
            $isRandom = true;
565
        }
566
567
        return $isRandom;
568
    }
569
570
    /**
571
     * returns random answers status.
572
     *
573
     * @author Juan Carlos Rana
574
     */
575
    public function getRandomAnswers()
576
    {
577
        return $this->random_answers;
578
    }
579
580
    /**
581
     * Same as isRandom() but has a name applied to values different than 0 or 1.
582
     *
583
     * @return int
584
     */
585
    public function getShuffle()
586
    {
587
        return $this->random;
588
    }
589
590
    /**
591
     * returns the exercise status (1 = enabled ; 0 = disabled).
592
     *
593
     * @author Olivier Brouckaert
594
     *
595
     * @return int - 1 if enabled, otherwise 0
596
     */
597
    public function selectStatus()
598
    {
599
        return $this->active;
600
    }
601
602
    /**
603
     * If false the question list will be managed as always if true
604
     * the question will be filtered
605
     * depending of the exercise settings (table c_quiz_rel_category).
606
     *
607
     * @param bool $status active or inactive grouping
608
     */
609
    public function setCategoriesGrouping($status)
610
    {
611
        $this->categories_grouping = (bool) $status;
612
    }
613
614
    /**
615
     * @return int
616
     */
617
    public function getHideQuestionTitle()
618
    {
619
        return $this->hideQuestionTitle;
620
    }
621
622
    /**
623
     * @param $value
624
     */
625
    public function setHideQuestionTitle($value)
626
    {
627
        $this->hideQuestionTitle = (int) $value;
628
    }
629
630
    /**
631
     * @return int
632
     */
633
    public function getScoreTypeModel()
634
    {
635
        return $this->scoreTypeModel;
636
    }
637
638
    /**
639
     * @param int $value
640
     */
641
    public function setScoreTypeModel($value)
642
    {
643
        $this->scoreTypeModel = (int) $value;
644
    }
645
646
    /**
647
     * @return int
648
     */
649
    public function getGlobalCategoryId()
650
    {
651
        return $this->globalCategoryId;
652
    }
653
654
    /**
655
     * @param int $value
656
     */
657
    public function setGlobalCategoryId($value)
658
    {
659
        if (is_array($value) && isset($value[0])) {
660
            $value = $value[0];
661
        }
662
        $this->globalCategoryId = (int) $value;
663
    }
664
665
    /**
666
     * @param int    $start
667
     * @param int    $limit
668
     * @param int    $sidx
669
     * @param string $sord
670
     * @param array  $whereCondition
671
     * @param array  $extraFields
672
     *
673
     * @return array
674
     */
675
    public function getQuestionListPagination(
676
        $start,
677
        $limit,
678
        $sidx,
679
        $sord,
680
        $whereCondition = [],
681
        $extraFields = []
682
    ) {
683
        if (!empty($this->iid)) {
684
            $category_list = TestCategory::getListOfCategoriesNameForTest(
685
                $this->iid,
686
                false
687
            );
688
            $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
689
            $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
690
691
            $sql = "SELECT q.iid
692
                    FROM $TBL_EXERCICE_QUESTION e
693
                    INNER JOIN $TBL_QUESTIONS  q ON e.question_id = q.iid
694
					WHERE e.exercice_id	= ".$this->iid." AND e.c_id = {$this->course_id}";
695
696
            $orderCondition = ' ORDER BY question_order ';
697
698
            if (!empty($sidx) && !empty($sord)) {
699
                if ('question' === $sidx) {
700
                    if (in_array(strtolower($sord), ['desc', 'asc'])) {
701
                        $orderCondition = " ORDER BY q.question $sord";
702
                    }
703
                }
704
            }
705
706
            $sql .= $orderCondition;
707
            $limitCondition = null;
708
            if (isset($start) && isset($limit)) {
709
                $start = (int) $start;
710
                $limit = (int) $limit;
711
                $limitCondition = " LIMIT $start, $limit";
712
            }
713
            $sql .= $limitCondition;
714
            $result = Database::query($sql);
715
            $questions = [];
716
            if (Database::num_rows($result)) {
717
                if (!empty($extraFields)) {
718
                    $extraFieldValue = new ExtraFieldValue('question');
719
                }
720
                while ($question = Database::fetch_array($result, 'ASSOC')) {
721
                    /** @var Question $objQuestionTmp */
722
                    $objQuestionTmp = Question::read($question['iid']);
723
                    $category_labels = TestCategory::return_category_labels(
724
                        $objQuestionTmp->category_list,
725
                        $category_list
726
                    );
727
728
                    if (empty($category_labels)) {
729
                        $category_labels = '-';
730
                    }
731
732
                    // Question type
733
                    $typeImg = $objQuestionTmp->getTypePicture();
734
                    $typeExpl = $objQuestionTmp->getExplanation();
735
736
                    $question_media = null;
737
                    if (!empty($objQuestionTmp->parent_id)) {
738
                        $objQuestionMedia = Question::read($objQuestionTmp->parent_id);
739
                        $question_media = Question::getMediaLabel($objQuestionMedia->question);
740
                    }
741
742
                    $questionType = Display::tag(
743
                        'div',
744
                        Display::return_icon($typeImg, $typeExpl, [], ICON_SIZE_MEDIUM).$question_media
745
                    );
746
747
                    $question = [
748
                        'iid' => $question['iid'],
749
                        'question' => $objQuestionTmp->selectTitle(),
750
                        'type' => $questionType,
751
                        'category' => Display::tag(
752
                            'div',
753
                            '<a href="#" style="padding:0px; margin:0px;">'.$category_labels.'</a>'
754
                        ),
755
                        'score' => $objQuestionTmp->selectWeighting(),
756
                        'level' => $objQuestionTmp->level,
757
                    ];
758
759
                    if (!empty($extraFields)) {
760
                        foreach ($extraFields as $extraField) {
761
                            $value = $extraFieldValue->get_values_by_handler_and_field_id(
762
                                $question['iid'],
763
                                $extraField['id']
764
                            );
765
                            $stringValue = null;
766
                            if ($value) {
767
                                $stringValue = $value['field_value'];
768
                            }
769
                            $question[$extraField['field_variable']] = $stringValue;
770
                        }
771
                    }
772
                    $questions[] = $question;
773
                }
774
            }
775
776
            return $questions;
777
        }
778
    }
779
780
    /**
781
     * Get question count per exercise from DB (any special treatment).
782
     *
783
     * @return int
784
     */
785
    public function getQuestionCount()
786
    {
787
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
788
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
789
        $sql = "SELECT count(q.iid) as count
790
                FROM $TBL_EXERCICE_QUESTION e
791
                INNER JOIN $TBL_QUESTIONS q
792
                ON e.question_id = q.iid
793
                WHERE
794
                    e.c_id = {$this->course_id} AND
795
                    e.exercice_id = {$this->iid}";
796
        $result = Database::query($sql);
797
798
        $count = 0;
799
        if (Database::num_rows($result)) {
800
            $row = Database::fetch_array($result);
801
            $count = (int) $row['count'];
802
        }
803
804
        return $count;
805
    }
806
807
    /**
808
     * @return array
809
     */
810
    public function getQuestionOrderedListByName()
811
    {
812
        if (empty($this->course_id) || empty($this->iid)) {
813
            return [];
814
        }
815
816
        $exerciseQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
817
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
818
819
        // Getting question list from the order (question list drag n drop interface ).
820
        $sql = "SELECT e.question_id
821
                FROM $exerciseQuestionTable e
822
                INNER JOIN $questionTable q
823
                ON e.question_id= q.iid
824
                WHERE
825
                    e.c_id = {$this->course_id} AND
826
                    e.exercice_id = {$this->iid}
827
                ORDER BY q.question";
828
        $result = Database::query($sql);
829
        $list = [];
830
        if (Database::num_rows($result)) {
831
            $list = Database::store_result($result, 'ASSOC');
832
        }
833
834
        return $list;
835
    }
836
837
    /**
838
     * Selecting question list depending in the exercise-category
839
     * relationship (category table in exercise settings).
840
     *
841
     * @param array $question_list
842
     * @param int   $questionSelectionType
843
     *
844
     * @return array
845
     */
846
    public function getQuestionListWithCategoryListFilteredByCategorySettings(
847
        $question_list,
848
        $questionSelectionType
849
    ) {
850
        $result = [
851
            'question_list' => [],
852
            'category_with_questions_list' => [],
853
        ];
854
855
        // Order/random categories
856
        $cat = new TestCategory();
857
        // Setting category order.
858
        switch ($questionSelectionType) {
859
            case EX_Q_SELECTION_ORDERED: // 1
860
            case EX_Q_SELECTION_RANDOM:  // 2
861
                // This options are not allowed here.
862
                break;
863
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED: // 3
864
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
865
                    $this,
866
                    $this->course['real_id'],
867
                    'title ASC',
868
                    false,
869
                    true
870
                );
871
872
                $questions_by_category = TestCategory::getQuestionsByCat(
873
                    $this->iid,
874
                    $question_list,
875
                    $categoriesAddedInExercise
876
                );
877
878
                $question_list = $this->pickQuestionsPerCategory(
879
                    $categoriesAddedInExercise,
880
                    $question_list,
881
                    $questions_by_category,
882
                    true,
883
                    false
884
                );
885
                break;
886
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED: // 4
887
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7
888
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
889
                    $this,
890
                    $this->course['real_id'],
891
                    null,
892
                    true,
893
                    true
894
                );
895
                $questions_by_category = TestCategory::getQuestionsByCat(
896
                    $this->iid,
897
                    $question_list,
898
                    $categoriesAddedInExercise
899
                );
900
                $question_list = $this->pickQuestionsPerCategory(
901
                    $categoriesAddedInExercise,
902
                    $question_list,
903
                    $questions_by_category,
904
                    true,
905
                    false
906
                );
907
                break;
908
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM: // 5
909
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
910
                    $this,
911
                    $this->course['real_id'],
912
                    'title ASC',
913
                    false,
914
                    true
915
                );
916
                $questions_by_category = TestCategory::getQuestionsByCat(
917
                    $this->iid,
918
                    $question_list,
919
                    $categoriesAddedInExercise
920
                );
921
922
                $questionsByCategoryMandatory = [];
923
                if (EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM == $this->getQuestionSelectionType() &&
924
                    api_get_configuration_value('allow_mandatory_question_in_category')
925
                ) {
926
                    $questionsByCategoryMandatory = TestCategory::getQuestionsByCat(
927
                        $this->iid,
928
                        $question_list,
929
                        $categoriesAddedInExercise,
930
                        true
931
                    );
932
                }
933
934
                $question_list = $this->pickQuestionsPerCategory(
935
                    $categoriesAddedInExercise,
936
                    $question_list,
937
                    $questions_by_category,
938
                    true,
939
                    true,
940
                    $questionsByCategoryMandatory
941
                );
942
                break;
943
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM: // 6
944
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED:
945
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
946
                    $this,
947
                    $this->course['real_id'],
948
                    null,
949
                    true,
950
                    true
951
                );
952
953
                $questions_by_category = TestCategory::getQuestionsByCat(
954
                    $this->iid,
955
                    $question_list,
956
                    $categoriesAddedInExercise
957
                );
958
959
                $question_list = $this->pickQuestionsPerCategory(
960
                    $categoriesAddedInExercise,
961
                    $question_list,
962
                    $questions_by_category,
963
                    true,
964
                    true
965
                );
966
                break;
967
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7
968
                break;
969
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED: // 8
970
                break;
971
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED: // 9
972
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
973
                    $this,
974
                    $this->course['real_id'],
975
                    'root ASC, lft ASC',
976
                    false,
977
                    true
978
                );
979
                $questions_by_category = TestCategory::getQuestionsByCat(
980
                    $this->iid,
981
                    $question_list,
982
                    $categoriesAddedInExercise
983
                );
984
                $question_list = $this->pickQuestionsPerCategory(
985
                    $categoriesAddedInExercise,
986
                    $question_list,
987
                    $questions_by_category,
988
                    true,
989
                    false
990
                );
991
                break;
992
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM: // 10
993
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
994
                    $this,
995
                    $this->course['real_id'],
996
                    'root, lft ASC',
997
                    false,
998
                    true
999
                );
1000
                $questions_by_category = TestCategory::getQuestionsByCat(
1001
                    $this->iid,
1002
                    $question_list,
1003
                    $categoriesAddedInExercise
1004
                );
1005
                $question_list = $this->pickQuestionsPerCategory(
1006
                    $categoriesAddedInExercise,
1007
                    $question_list,
1008
                    $questions_by_category,
1009
                    true,
1010
                    true
1011
                );
1012
                break;
1013
        }
1014
1015
        $result['question_list'] = isset($question_list) ? $question_list : [];
1016
        $result['category_with_questions_list'] = isset($questions_by_category) ? $questions_by_category : [];
1017
        $parentsLoaded = [];
1018
        // Adding category info in the category list with question list:
1019
        if (!empty($questions_by_category)) {
1020
            $newCategoryList = [];
1021
            $em = Database::getManager();
1022
            $repo = $em->getRepository('ChamiloCourseBundle:CQuizCategory');
1023
1024
            foreach ($questions_by_category as $categoryId => $questionList) {
1025
                $cat = new TestCategory();
1026
                $cat = $cat->getCategory($categoryId);
1027
                if ($cat) {
1028
                    $cat = (array) $cat;
1029
                }
1030
1031
                $categoryParentInfo = null;
1032
                // Parent is not set no loop here
1033
                if (isset($cat['parent_id']) && !empty($cat['parent_id'])) {
1034
                    /** @var \Chamilo\CourseBundle\Entity\CQuizCategory $categoryEntity */
1035
                    if (!isset($parentsLoaded[$cat['parent_id']])) {
1036
                        $categoryEntity = $em->find('ChamiloCourseBundle:CQuizCategory', $cat['parent_id']);
1037
                        $parentsLoaded[$cat['parent_id']] = $categoryEntity;
1038
                    } else {
1039
                        $categoryEntity = $parentsLoaded[$cat['parent_id']];
1040
                    }
1041
                    $path = $repo->getPath($categoryEntity);
1042
1043
                    $index = 0;
1044
                    if ($this->categoryMinusOne) {
1045
                        //$index = 1;
1046
                    }
1047
1048
                    /** @var \Chamilo\CourseBundle\Entity\CQuizCategory $categoryParent */
1049
                    foreach ($path as $categoryParent) {
1050
                        $visibility = $categoryParent->getVisibility();
1051
                        if (0 == $visibility) {
1052
                            $categoryParentId = $categoryId;
1053
                            $categoryTitle = $cat['title'];
1054
                            if (count($path) > 1) {
1055
                                continue;
1056
                            }
1057
                        } else {
1058
                            $categoryParentId = $categoryParent->getIid();
1059
                            $categoryTitle = $categoryParent->getTitle();
1060
                        }
1061
1062
                        $categoryParentInfo['id'] = $categoryParentId;
1063
                        $categoryParentInfo['iid'] = $categoryParentId;
1064
                        $categoryParentInfo['parent_path'] = null;
1065
                        $categoryParentInfo['title'] = $categoryTitle;
1066
                        $categoryParentInfo['name'] = $categoryTitle;
1067
                        $categoryParentInfo['parent_id'] = null;
1068
                        break;
1069
                    }
1070
                }
1071
                $cat['parent_info'] = $categoryParentInfo;
1072
                $newCategoryList[$categoryId] = [
1073
                    'category' => $cat,
1074
                    'question_list' => $questionList,
1075
                ];
1076
            }
1077
1078
            $result['category_with_questions_list'] = $newCategoryList;
1079
        }
1080
1081
        return $result;
1082
    }
1083
1084
    /**
1085
     * returns the array with the question ID list.
1086
     *
1087
     * @param bool $fromDatabase Whether the results should be fetched in the database or just from memory
1088
     * @param bool $adminView    Whether we should return all questions (admin view) or
1089
     *                           just a list limited by the max number of random questions
1090
     *
1091
     * @author Olivier Brouckaert
1092
     *
1093
     * @return array - question ID list
1094
     */
1095
    public function selectQuestionList($fromDatabase = false, $adminView = false)
1096
    {
1097
        if ($fromDatabase && !empty($this->iid)) {
1098
            $nbQuestions = $this->getQuestionCount();
1099
            $questionSelectionType = $this->getQuestionSelectionType();
1100
1101
            switch ($questionSelectionType) {
1102
                case EX_Q_SELECTION_ORDERED:
1103
                    $questionList = $this->getQuestionOrderedList($adminView);
1104
                    break;
1105
                case EX_Q_SELECTION_RANDOM:
1106
                    // Not a random exercise, or if there are not at least 2 questions
1107
                    if ($this->random == 0 || $nbQuestions < 2) {
1108
                        $questionList = $this->getQuestionOrderedList($adminView);
1109
                    } else {
1110
                        $questionList = $this->getRandomList($adminView);
1111
                    }
1112
                    break;
1113
                default:
1114
                    $questionList = $this->getQuestionOrderedList($adminView);
1115
                    $result = $this->getQuestionListWithCategoryListFilteredByCategorySettings(
1116
                        $questionList,
1117
                        $questionSelectionType
1118
                    );
1119
                    $this->categoryWithQuestionList = $result['category_with_questions_list'];
1120
                    $questionList = $result['question_list'];
1121
                    break;
1122
            }
1123
1124
            return $questionList;
1125
        }
1126
1127
        return $this->questionList;
1128
    }
1129
1130
    /**
1131
     * returns the number of questions in this exercise.
1132
     *
1133
     * @author Olivier Brouckaert
1134
     *
1135
     * @return int - number of questions
1136
     */
1137
    public function selectNbrQuestions()
1138
    {
1139
        return count($this->questionList);
1140
    }
1141
1142
    /**
1143
     * @return int
1144
     */
1145
    public function selectPropagateNeg()
1146
    {
1147
        return $this->propagate_neg;
1148
    }
1149
1150
    /**
1151
     * @return int
1152
     */
1153
    public function getSaveCorrectAnswers()
1154
    {
1155
        return $this->saveCorrectAnswers;
1156
    }
1157
1158
    /**
1159
     * Selects questions randomly in the question list.
1160
     *
1161
     * @author Olivier Brouckaert
1162
     * @author Hubert Borderiou 15 nov 2011
1163
     *
1164
     * @param bool $adminView Whether we should return all
1165
     *                        questions (admin view) or just a list limited by the max number of random questions
1166
     *
1167
     * @return array - if the exercise is not set to take questions randomly, returns the question list
1168
     *               without randomizing, otherwise, returns the list with questions selected randomly
1169
     */
1170
    public function getRandomList($adminView = false)
1171
    {
1172
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1173
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
1174
        $random = isset($this->random) && !empty($this->random) ? $this->random : 0;
1175
1176
        // Random with limit
1177
        $randomLimit = " ORDER BY RAND() LIMIT $random";
1178
1179
        // Random with no limit
1180
        if (-1 == $random) {
1181
            $randomLimit = ' ORDER BY RAND() ';
1182
        }
1183
1184
        // Admin see the list in default order
1185
        if (true === $adminView) {
1186
            // If viewing it as admin for edition, don't show it randomly, use title + id
1187
            $randomLimit = 'ORDER BY e.question_order';
1188
        }
1189
1190
        $sql = "SELECT e.question_id
1191
                FROM $quizRelQuestion e
1192
                INNER JOIN $question q
1193
                ON e.question_id= q.iid
1194
                WHERE
1195
                    e.c_id = {$this->course_id} AND
1196
                    e.exercice_id = '".Database::escape_string($this->iid)."'
1197
                    $randomLimit ";
1198
        $result = Database::query($sql);
1199
        $questionList = [];
1200
        while ($row = Database::fetch_object($result)) {
1201
            $questionList[] = $row->question_id;
1202
        }
1203
1204
        return $questionList;
1205
    }
1206
1207
    /**
1208
     * returns 'true' if the question ID is in the question list.
1209
     *
1210
     * @author Olivier Brouckaert
1211
     *
1212
     * @param int $questionId - question ID
1213
     *
1214
     * @return bool - true if in the list, otherwise false
1215
     */
1216
    public function isInList($questionId)
1217
    {
1218
        $inList = false;
1219
        if (is_array($this->questionList)) {
1220
            $inList = in_array($questionId, $this->questionList);
1221
        }
1222
1223
        return $inList;
1224
    }
1225
1226
    /**
1227
     * If current exercise has a question.
1228
     *
1229
     * @param int $questionId
1230
     *
1231
     * @return int
1232
     */
1233
    public function hasQuestion($questionId)
1234
    {
1235
        $questionId = (int) $questionId;
1236
1237
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1238
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1239
        $sql = "SELECT q.iid
1240
                FROM $TBL_EXERCICE_QUESTION e
1241
                INNER JOIN $TBL_QUESTIONS q
1242
                ON e.question_id = q.iid
1243
                WHERE
1244
                    q.iid = $questionId AND
1245
                    e.c_id = {$this->course_id} AND
1246
                    e.exercice_id = ".$this->iid;
1247
1248
        $result = Database::query($sql);
1249
1250
        return Database::num_rows($result) > 0;
1251
    }
1252
1253
    public function hasQuestionWithType($type)
1254
    {
1255
        $type = (int) $type;
1256
1257
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1258
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
1259
        $sql = "SELECT q.iid
1260
                FROM $table e
1261
                INNER JOIN $tableQuestion q
1262
                ON e.question_id = q.iid
1263
                WHERE
1264
                    q.type = $type AND
1265
                    e.c_id = {$this->course_id} AND
1266
                    e.exercice_id = ".$this->iid;
1267
1268
        $result = Database::query($sql);
1269
1270
        return Database::num_rows($result) > 0;
1271
    }
1272
1273
    public function hasQuestionWithTypeNotInList(array $questionTypeList)
1274
    {
1275
        if (empty($questionTypeList)) {
1276
            return false;
1277
        }
1278
1279
        $questionTypeToString = implode("','", array_map('intval', $questionTypeList));
1280
1281
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1282
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
1283
        $sql = "SELECT q.iid
1284
                FROM $table e
1285
                INNER JOIN $tableQuestion q
1286
                ON e.question_id = q.iid
1287
                WHERE
1288
                    q.type NOT IN ('$questionTypeToString')  AND
1289
                    e.c_id = {$this->course_id} AND
1290
                    e.exercice_id = ".$this->iid;
1291
1292
        $result = Database::query($sql);
1293
1294
        return Database::num_rows($result) > 0;
1295
    }
1296
1297
    /**
1298
     * changes the exercise title.
1299
     *
1300
     * @author Olivier Brouckaert
1301
     *
1302
     * @param string $title - exercise title
1303
     */
1304
    public function updateTitle($title)
1305
    {
1306
        $this->title = $this->exercise = $title;
1307
    }
1308
1309
    /**
1310
     * changes the exercise max attempts.
1311
     *
1312
     * @param int $attempts - exercise max attempts
1313
     */
1314
    public function updateAttempts($attempts)
1315
    {
1316
        $this->attempts = $attempts;
1317
    }
1318
1319
    /**
1320
     * changes the exercise feedback type.
1321
     *
1322
     * @param int $feedback_type
1323
     */
1324
    public function updateFeedbackType($feedback_type)
1325
    {
1326
        $this->feedback_type = $feedback_type;
1327
    }
1328
1329
    /**
1330
     * changes the exercise description.
1331
     *
1332
     * @author Olivier Brouckaert
1333
     *
1334
     * @param string $description - exercise description
1335
     */
1336
    public function updateDescription($description)
1337
    {
1338
        $this->description = $description;
1339
    }
1340
1341
    /**
1342
     * changes the exercise expired_time.
1343
     *
1344
     * @author Isaac flores
1345
     *
1346
     * @param int $expired_time The expired time of the quiz
1347
     */
1348
    public function updateExpiredTime($expired_time)
1349
    {
1350
        $this->expired_time = $expired_time;
1351
    }
1352
1353
    /**
1354
     * @param $value
1355
     */
1356
    public function updatePropagateNegative($value)
1357
    {
1358
        $this->propagate_neg = $value;
1359
    }
1360
1361
    /**
1362
     * @param int $value
1363
     */
1364
    public function updateSaveCorrectAnswers($value)
1365
    {
1366
        $this->saveCorrectAnswers = (int) $value;
1367
    }
1368
1369
    /**
1370
     * @param $value
1371
     */
1372
    public function updateReviewAnswers($value)
1373
    {
1374
        $this->review_answers = isset($value) && $value ? true : false;
1375
    }
1376
1377
    /**
1378
     * @param $value
1379
     */
1380
    public function updatePassPercentage($value)
1381
    {
1382
        $this->pass_percentage = $value;
1383
    }
1384
1385
    /**
1386
     * @param string $text
1387
     */
1388
    public function updateEmailNotificationTemplate($text)
1389
    {
1390
        $this->emailNotificationTemplate = $text;
1391
    }
1392
1393
    /**
1394
     * @param string $text
1395
     */
1396
    public function setEmailNotificationTemplateToUser($text)
1397
    {
1398
        $this->emailNotificationTemplateToUser = $text;
1399
    }
1400
1401
    /**
1402
     * @param string $value
1403
     */
1404
    public function setNotifyUserByEmail($value)
1405
    {
1406
        $this->notifyUserByEmail = $value;
1407
    }
1408
1409
    /**
1410
     * @param int $value
1411
     */
1412
    public function updateEndButton($value)
1413
    {
1414
        $this->endButton = (int) $value;
1415
    }
1416
1417
    /**
1418
     * @param string $value
1419
     */
1420
    public function setOnSuccessMessage($value)
1421
    {
1422
        $this->onSuccessMessage = $value;
1423
    }
1424
1425
    /**
1426
     * @param string $value
1427
     */
1428
    public function setOnFailedMessage($value)
1429
    {
1430
        $this->onFailedMessage = $value;
1431
    }
1432
1433
    /**
1434
     * @param $value
1435
     */
1436
    public function setModelType($value)
1437
    {
1438
        $this->modelType = (int) $value;
1439
    }
1440
1441
    /**
1442
     * @param int $value
1443
     */
1444
    public function setQuestionSelectionType($value)
1445
    {
1446
        $this->questionSelectionType = (int) $value;
1447
    }
1448
1449
    /**
1450
     * @return int
1451
     */
1452
    public function getQuestionSelectionType()
1453
    {
1454
        return (int) $this->questionSelectionType;
1455
    }
1456
1457
    /**
1458
     * @param array $categories
1459
     */
1460
    public function updateCategories($categories)
1461
    {
1462
        if (!empty($categories)) {
1463
            $categories = array_map('intval', $categories);
1464
            $this->categories = $categories;
1465
        }
1466
    }
1467
1468
    /**
1469
     * changes the exercise sound file.
1470
     *
1471
     * @author Olivier Brouckaert
1472
     *
1473
     * @param string $sound  - exercise sound file
1474
     * @param string $delete - ask to delete the file
1475
     */
1476
    public function updateSound($sound, $delete)
1477
    {
1478
        global $audioPath, $documentPath;
1479
        $TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
1480
1481
        if ($sound['size'] && (strstr($sound['type'], 'audio') || strstr($sound['type'], 'video'))) {
1482
            $this->sound = $sound['name'];
1483
1484
            if (@move_uploaded_file($sound['tmp_name'], $audioPath.'/'.$this->sound)) {
1485
                $sql = "SELECT 1 FROM $TBL_DOCUMENT
1486
                        WHERE
1487
                            c_id = ".$this->course_id." AND
1488
                            path = '".str_replace($documentPath, '', $audioPath).'/'.$this->sound."'";
1489
                $result = Database::query($sql);
1490
1491
                if (!Database::num_rows($result)) {
1492
                    $id = add_document(
1493
                        $this->course,
1494
                        str_replace($documentPath, '', $audioPath).'/'.$this->sound,
1495
                        'file',
1496
                        $sound['size'],
1497
                        $sound['name']
1498
                    );
1499
                    api_item_property_update(
1500
                        $this->course,
1501
                        TOOL_DOCUMENT,
1502
                        $id,
1503
                        'DocumentAdded',
1504
                        api_get_user_id()
1505
                    );
1506
                    item_property_update_on_folder(
1507
                        $this->course,
1508
                        str_replace($documentPath, '', $audioPath),
1509
                        api_get_user_id()
1510
                    );
1511
                }
1512
            }
1513
        } elseif ($delete && is_file($audioPath.'/'.$this->sound)) {
1514
            $this->sound = '';
1515
        }
1516
    }
1517
1518
    /**
1519
     * changes the exercise type.
1520
     *
1521
     * @author Olivier Brouckaert
1522
     *
1523
     * @param int $type - exercise type
1524
     */
1525
    public function updateType($type)
1526
    {
1527
        $this->type = $type;
1528
    }
1529
1530
    /**
1531
     * sets to 0 if questions are not selected randomly
1532
     * if questions are selected randomly, sets the draws.
1533
     *
1534
     * @author Olivier Brouckaert
1535
     *
1536
     * @param int $random - 0 if not random, otherwise the draws
1537
     */
1538
    public function setRandom($random)
1539
    {
1540
        $this->random = $random;
1541
    }
1542
1543
    /**
1544
     * sets to 0 if answers are not selected randomly
1545
     * if answers are selected randomly.
1546
     *
1547
     * @author Juan Carlos Rana
1548
     *
1549
     * @param int $random_answers - random answers
1550
     */
1551
    public function updateRandomAnswers($random_answers)
1552
    {
1553
        $this->random_answers = $random_answers;
1554
    }
1555
1556
    /**
1557
     * enables the exercise.
1558
     *
1559
     * @author Olivier Brouckaert
1560
     */
1561
    public function enable()
1562
    {
1563
        $this->active = 1;
1564
    }
1565
1566
    /**
1567
     * disables the exercise.
1568
     *
1569
     * @author Olivier Brouckaert
1570
     */
1571
    public function disable()
1572
    {
1573
        $this->active = 0;
1574
    }
1575
1576
    /**
1577
     * Set disable results.
1578
     */
1579
    public function disable_results()
1580
    {
1581
        $this->results_disabled = true;
1582
    }
1583
1584
    /**
1585
     * Enable results.
1586
     */
1587
    public function enable_results()
1588
    {
1589
        $this->results_disabled = false;
1590
    }
1591
1592
    /**
1593
     * @param int $results_disabled
1594
     */
1595
    public function updateResultsDisabled($results_disabled)
1596
    {
1597
        $this->results_disabled = (int) $results_disabled;
1598
    }
1599
1600
    /**
1601
     * updates the exercise in the data base.
1602
     *
1603
     * @param string $type_e
1604
     *
1605
     * @author Olivier Brouckaert
1606
     */
1607
    public function save($type_e = '')
1608
    {
1609
        $_course = $this->course;
1610
        $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
1611
1612
        $id = $this->iid;
1613
        $exercise = $this->exercise;
1614
        $description = $this->description;
1615
        $sound = $this->sound;
1616
        $type = $this->type;
1617
        $attempts = isset($this->attempts) ? $this->attempts : 0;
1618
        $feedback_type = isset($this->feedback_type) ? $this->feedback_type : 0;
1619
        $random = $this->random;
1620
        $random_answers = $this->random_answers;
1621
        $active = $this->active;
1622
        $propagate_neg = (int) $this->propagate_neg;
1623
        $saveCorrectAnswers = isset($this->saveCorrectAnswers) ? (int) $this->saveCorrectAnswers : 0;
1624
        $review_answers = isset($this->review_answers) && $this->review_answers ? 1 : 0;
1625
        $randomByCat = (int) $this->randomByCat;
1626
        $text_when_finished = $this->text_when_finished;
1627
        $text_when_finished_failure = $this->text_when_finished_failure;
1628
        $display_category_name = (int) $this->display_category_name;
1629
        $pass_percentage = (int) $this->pass_percentage;
1630
        $session_id = $this->sessionId;
1631
1632
        // If direct we do not show results
1633
        $results_disabled = (int) $this->results_disabled;
1634
        if (in_array($feedback_type, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1635
            $results_disabled = 0;
1636
        }
1637
        $expired_time = (int) $this->expired_time;
1638
        $showHideConfiguration = api_get_configuration_value('quiz_hide_question_number');
1639
1640
        // Exercise already exists
1641
        if ($id) {
1642
            // we prepare date in the database using the api_get_utc_datetime() function
1643
            $start_time = null;
1644
            if (!empty($this->start_time)) {
1645
                $start_time = $this->start_time;
1646
            }
1647
1648
            $end_time = null;
1649
            if (!empty($this->end_time)) {
1650
                $end_time = $this->end_time;
1651
            }
1652
1653
            $params = [
1654
                'title' => $exercise,
1655
                'description' => $description,
1656
            ];
1657
1658
            $paramsExtra = [];
1659
            if ($type_e != 'simple') {
1660
                $paramsExtra = [
1661
                    'sound' => $sound,
1662
                    'type' => $type,
1663
                    'random' => $random,
1664
                    'random_answers' => $random_answers,
1665
                    'active' => $active,
1666
                    'feedback_type' => $feedback_type,
1667
                    'start_time' => $start_time,
1668
                    'end_time' => $end_time,
1669
                    'max_attempt' => $attempts,
1670
                    'expired_time' => $expired_time,
1671
                    'propagate_neg' => $propagate_neg,
1672
                    'save_correct_answers' => $saveCorrectAnswers,
1673
                    'review_answers' => $review_answers,
1674
                    'random_by_category' => $randomByCat,
1675
                    'text_when_finished' => $text_when_finished,
1676
                    'display_category_name' => $display_category_name,
1677
                    'pass_percentage' => $pass_percentage,
1678
                    'results_disabled' => $results_disabled,
1679
                    'question_selection_type' => $this->getQuestionSelectionType(),
1680
                    'hide_question_title' => $this->getHideQuestionTitle(),
1681
                ];
1682
1683
                if (true === api_get_configuration_value('exercise_text_when_finished_failure')) {
1684
                    $paramsExtra['text_when_finished_failure'] = $text_when_finished_failure;
1685
                }
1686
1687
                $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
1688
                if ($allow === true) {
1689
                    $paramsExtra['show_previous_button'] = $this->showPreviousButton();
1690
                }
1691
1692
                if (api_get_configuration_value('quiz_prevent_backwards_move')) {
1693
                    $paramsExtra['prevent_backwards'] = $this->getPreventBackwards();
1694
                }
1695
1696
                $allow = api_get_configuration_value('allow_exercise_categories');
1697
                if ($allow === true) {
1698
                    if (!empty($this->getExerciseCategoryId())) {
1699
                        $paramsExtra['exercise_category_id'] = $this->getExerciseCategoryId();
1700
                    }
1701
                }
1702
1703
                $allow = api_get_configuration_value('allow_notification_setting_per_exercise');
1704
                if ($allow === true) {
1705
                    $notifications = $this->getNotifications();
1706
                    $notifications = implode(',', $notifications);
1707
                    $paramsExtra['notifications'] = $notifications;
1708
                }
1709
1710
                $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
1711
                if ($pageConfig && !empty($this->pageResultConfiguration)) {
1712
                    $paramsExtra['page_result_configuration'] = $this->pageResultConfiguration;
1713
                }
1714
            }
1715
1716
            if ($showHideConfiguration) {
1717
                $paramsExtra['hide_question_number'] = $this->hideQuestionNumber;
1718
            }
1719
            if (true === api_get_configuration_value('quiz_hide_attempts_table_on_start_page')) {
1720
                $paramsExtra['hide_attempts_table'] = $this->getHideAttemptsTableOnStartPage();
1721
            }
1722
1723
            $params = array_merge($params, $paramsExtra);
1724
1725
            Database::update(
1726
                $TBL_EXERCISES,
1727
                $params,
1728
                ['c_id = ? AND iid = ?' => [$this->course_id, $id]]
1729
            );
1730
1731
            // update into the item_property table
1732
            api_item_property_update(
1733
                $_course,
1734
                TOOL_QUIZ,
1735
                $id,
1736
                'QuizUpdated',
1737
                api_get_user_id()
1738
            );
1739
1740
            if (api_get_setting('search_enabled') === 'true') {
1741
                $this->search_engine_edit();
1742
            }
1743
            Event::addEvent(
1744
                LOG_EXERCISE_UPDATE,
1745
                LOG_EXERCISE_ID,
1746
                $id
1747
            );
1748
        } else {
1749
            // Creates a new exercise
1750
            // In this case of new exercise, we don't do the api_get_utc_datetime()
1751
            // for date because, bellow, we call function api_set_default_visibility()
1752
            // In this function, api_set_default_visibility,
1753
            // the Quiz is saved too, with an $id and api_get_utc_datetime() is done.
1754
            // If we do it now, it will be done twice (cf. https://support.chamilo.org/issues/6586)
1755
            $start_time = null;
1756
            if (!empty($this->start_time)) {
1757
                $start_time = $this->start_time;
1758
            }
1759
1760
            $end_time = null;
1761
            if (!empty($this->end_time)) {
1762
                $end_time = $this->end_time;
1763
            }
1764
1765
            $params = [
1766
                'c_id' => $this->course_id,
1767
                'start_time' => $start_time,
1768
                'end_time' => $end_time,
1769
                'title' => $exercise,
1770
                'description' => $description,
1771
                'sound' => $sound,
1772
                'type' => $type,
1773
                'random' => $random,
1774
                'random_answers' => $random_answers,
1775
                'active' => $active,
1776
                'results_disabled' => $results_disabled,
1777
                'max_attempt' => $attempts,
1778
                'feedback_type' => $feedback_type,
1779
                'expired_time' => $expired_time,
1780
                'session_id' => $session_id,
1781
                'review_answers' => $review_answers,
1782
                'random_by_category' => $randomByCat,
1783
                'text_when_finished' => $text_when_finished,
1784
                'display_category_name' => $display_category_name,
1785
                'pass_percentage' => $pass_percentage,
1786
                'save_correct_answers' => $saveCorrectAnswers,
1787
                'propagate_neg' => $propagate_neg,
1788
                'hide_question_title' => $this->getHideQuestionTitle(),
1789
            ];
1790
1791
            if (true === api_get_configuration_value('exercise_text_when_finished_failure')) {
1792
                $params['text_when_finished_failure'] = $text_when_finished_failure;
1793
            }
1794
1795
            $allow = api_get_configuration_value('allow_exercise_categories');
1796
            if (true === $allow) {
1797
                if (!empty($this->getExerciseCategoryId())) {
1798
                    $params['exercise_category_id'] = $this->getExerciseCategoryId();
1799
                }
1800
            }
1801
1802
            if (api_get_configuration_value('quiz_prevent_backwards_move')) {
1803
                $params['prevent_backwards'] = $this->getPreventBackwards();
1804
            }
1805
1806
            $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
1807
            if (true === $allow) {
1808
                $params['show_previous_button'] = $this->showPreviousButton();
1809
            }
1810
1811
            $allow = api_get_configuration_value('allow_notification_setting_per_exercise');
1812
            if (true === $allow) {
1813
                $notifications = $this->getNotifications();
1814
                $params['notifications'] = '';
1815
                if (!empty($notifications)) {
1816
                    $notifications = implode(',', $notifications);
1817
                    $params['notifications'] = $notifications;
1818
                }
1819
            }
1820
1821
            $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
1822
            if ($pageConfig && !empty($this->pageResultConfiguration)) {
1823
                $params['page_result_configuration'] = $this->pageResultConfiguration;
1824
            }
1825
            if ($showHideConfiguration) {
1826
                $params['hide_question_number'] = $this->hideQuestionNumber;
1827
            }
1828
1829
            $this->iid = Database::insert($TBL_EXERCISES, $params);
1830
1831
            if ($this->iid) {
1832
                $sql = "UPDATE $TBL_EXERCISES
1833
                        SET question_selection_type= ".$this->getQuestionSelectionType()."
1834
                        WHERE iid = ".$this->iid;
1835
                Database::query($sql);
1836
1837
                // insert into the item_property table
1838
                api_item_property_update(
1839
                    $this->course,
1840
                    TOOL_QUIZ,
1841
                    $this->iid,
1842
                    'QuizAdded',
1843
                    api_get_user_id()
1844
                );
1845
1846
                // This function save the quiz again, carefull about start_time
1847
                // and end_time if you remove this line (see above)
1848
                api_set_default_visibility(
1849
                    $this->iid,
1850
                    TOOL_QUIZ,
1851
                    null,
1852
                    $this->course
1853
                );
1854
1855
                if (api_get_setting('search_enabled') === 'true' && extension_loaded('xapian')) {
1856
                    $this->search_engine_save();
1857
                }
1858
                Event::addEvent(
1859
                    LOG_EXERCISE_CREATE,
1860
                    LOG_EXERCISE_ID,
1861
                    $this->iid
1862
                );
1863
            }
1864
        }
1865
1866
        $this->save_categories_in_exercise($this->categories);
1867
1868
        return $this->iid;
1869
    }
1870
1871
    /**
1872
     * Updates question position.
1873
     *
1874
     * @return bool
1875
     */
1876
    public function update_question_positions()
1877
    {
1878
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1879
        // Fixes #3483 when updating order
1880
        $questionList = $this->selectQuestionList(true);
1881
1882
        if (empty($this->iid)) {
1883
            return false;
1884
        }
1885
1886
        if (!empty($questionList)) {
1887
            foreach ($questionList as $position => $questionId) {
1888
                $position = (int) $position;
1889
                $questionId = (int) $questionId;
1890
                $sql = "UPDATE $table SET
1891
                            question_order ='".$position."'
1892
                        WHERE
1893
                            question_id = ".$questionId." AND
1894
                            exercice_id=".$this->iid;
1895
                Database::query($sql);
1896
            }
1897
        }
1898
1899
        return true;
1900
    }
1901
1902
    /**
1903
     * Adds a question into the question list.
1904
     *
1905
     * @author Olivier Brouckaert
1906
     *
1907
     * @param int $questionId - question ID
1908
     *
1909
     * @return bool - true if the question has been added, otherwise false
1910
     */
1911
    public function addToList($questionId)
1912
    {
1913
        // checks if the question ID is not in the list
1914
        if (!$this->isInList($questionId)) {
1915
            // selects the max position
1916
            if (!$this->selectNbrQuestions()) {
1917
                $pos = 1;
1918
            } else {
1919
                if (is_array($this->questionList)) {
1920
                    $pos = max(array_keys($this->questionList)) + 1;
1921
                }
1922
            }
1923
            $this->questionList[$pos] = $questionId;
1924
1925
            return true;
1926
        }
1927
1928
        return false;
1929
    }
1930
1931
    /**
1932
     * removes a question from the question list.
1933
     *
1934
     * @author Olivier Brouckaert
1935
     *
1936
     * @param int $questionId - question ID
1937
     *
1938
     * @return bool - true if the question has been removed, otherwise false
1939
     */
1940
    public function removeFromList($questionId)
1941
    {
1942
        // searches the position of the question ID in the list
1943
        $pos = array_search($questionId, $this->questionList);
1944
        // question not found
1945
        if (false === $pos) {
1946
            return false;
1947
        } else {
1948
            // dont reduce the number of random question if we use random by category option, or if
1949
            // random all questions
1950
            if ($this->isRandom() && 0 == $this->isRandomByCat()) {
1951
                if (count($this->questionList) >= $this->random && $this->random > 0) {
1952
                    $this->random--;
1953
                    $this->save();
1954
                }
1955
            }
1956
            // deletes the position from the array containing the wanted question ID
1957
            unset($this->questionList[$pos]);
1958
1959
            return true;
1960
        }
1961
    }
1962
1963
    /**
1964
     * Marks the exercise as deleted.
1965
     * If $delete argument set, completely deletes it from the database.
1966
     * Note: leaves the questions in the database as "orphan" questions
1967
     * (unless used by other tests).
1968
     *
1969
     * @param bool $delete          Whether to really delete the test (true) or only mark it (false = default)
1970
     * @param bool $deleteQuestions Whether to delete the test questions (true)
1971
     *
1972
     * @return bool Whether the operation was successful or not
1973
     *
1974
     * @author Olivier Brouckaert
1975
     * @author Yannick Warnier
1976
     */
1977
    public function delete(bool $delete = false, bool $deleteQuestions = false): bool
1978
    {
1979
        $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
1980
1981
        if ($limitTeacherAccess && !api_is_platform_admin()) {
1982
            return false;
1983
        }
1984
1985
        $locked = api_resource_is_locked_by_gradebook(
1986
            $this->iid,
1987
            LINK_EXERCISE
1988
        );
1989
1990
        if ($locked) {
1991
            return false;
1992
        }
1993
1994
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
1995
        $sql = "UPDATE $table SET active='-1'
1996
                WHERE iid = ".$this->iid;
1997
        Database::query($sql);
1998
1999
        api_item_property_update(
2000
            $this->course,
2001
            TOOL_QUIZ,
2002
            $this->iid,
2003
            'QuizDeleted',
2004
            api_get_user_id()
2005
        );
2006
        api_item_property_update(
2007
            $this->course,
2008
            TOOL_QUIZ,
2009
            $this->iid,
2010
            'delete',
2011
            api_get_user_id()
2012
        );
2013
2014
        Skill::deleteSkillsFromItem($this->iid, ITEM_TYPE_EXERCISE);
2015
2016
        if (api_get_setting('search_enabled') === 'true' &&
2017
            extension_loaded('xapian')
2018
        ) {
2019
            $this->search_engine_delete();
2020
        }
2021
2022
        $linkInfo = GradebookUtils::isResourceInCourseGradebook(
2023
            $this->course['code'],
2024
            LINK_EXERCISE,
2025
            $this->iid,
2026
            $this->sessionId
2027
        );
2028
2029
        if ($linkInfo !== false) {
2030
            GradebookUtils::remove_resource_from_course_gradebook($linkInfo['id']);
2031
        }
2032
2033
        $questions = [];
2034
2035
        if ($delete || $deleteQuestions) {
2036
            $questions = $this->getQuestionOrderedList(true);
2037
        }
2038
2039
        if ($deleteQuestions) {
2040
            // Delete the questions of the test (this could delete questions reused on other tests)
2041
            foreach ($questions as $order => $questionId) {
2042
                $masterExerciseId = Question::getMasterQuizForQuestion($questionId);
2043
                if ($masterExerciseId == $this->iid) {
2044
                    $objQuestionTmp = Question::read($questionId);
2045
                    $objQuestionTmp->delete();
2046
                    $this->removeFromList($questionId);
2047
                }
2048
            }
2049
        }
2050
2051
        if ($delete) {
2052
            // Really delete the test (if questions were not previously deleted these will be orphaned)
2053
            foreach ($questions as $order => $questionId) {
2054
                $question = Question::read($questionId, $this->course);
2055
                $question->delete($this->course_id);
2056
            }
2057
            $sql = "DELETE FROM $table
2058
                WHERE iid = ".$this->iid;
2059
            Database::query($sql);
2060
        }
2061
        Event::addEvent(
2062
            LOG_EXERCISE_DELETE,
2063
            LOG_EXERCISE_ID,
2064
            $this->iid
2065
        );
2066
2067
        return true;
2068
    }
2069
2070
    /**
2071
     * Creates the form to create / edit an exercise.
2072
     *
2073
     * @param FormValidator $form
2074
     * @param string        $type
2075
     */
2076
    public function createForm($form, $type = 'full')
2077
    {
2078
        if (empty($type)) {
2079
            $type = 'full';
2080
        }
2081
2082
        // Form title
2083
        $form_title = get_lang('NewEx');
2084
        if (!empty($_GET['exerciseId'])) {
2085
            $form_title = get_lang('ModifyExercise');
2086
        }
2087
2088
        $form->addHeader($form_title);
2089
        $form->protect();
2090
2091
        // Title.
2092
        if (api_get_configuration_value('save_titles_as_html')) {
2093
            $form->addHtmlEditor(
2094
                'exerciseTitle',
2095
                get_lang('ExerciseName'),
2096
                false,
2097
                false,
2098
                ['ToolbarSet' => 'TitleAsHtml']
2099
            );
2100
        } else {
2101
            $form->addElement(
2102
                'text',
2103
                'exerciseTitle',
2104
                get_lang('ExerciseName'),
2105
                ['id' => 'exercise_title']
2106
            );
2107
        }
2108
2109
        $form->addElement('advanced_settings', 'advanced_params', get_lang('AdvancedParameters'));
2110
        $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
2111
2112
        if (api_get_configuration_value('allow_exercise_categories')) {
2113
            $categoryManager = new ExerciseCategoryManager();
2114
            $categories = $categoryManager->getCategories(api_get_course_int_id());
2115
            $options = [];
2116
            if (!empty($categories)) {
2117
                /** @var CExerciseCategory $category */
2118
                foreach ($categories as $category) {
2119
                    $options[$category->getId()] = $category->getName();
2120
                }
2121
            }
2122
2123
            $form->addSelect(
2124
                'exercise_category_id',
2125
                get_lang('Category'),
2126
                $options,
2127
                ['placeholder' => get_lang('SelectAnOption')]
2128
            );
2129
        }
2130
2131
        $editor_config = [
2132
            'ToolbarSet' => 'TestQuestionDescription',
2133
            'Width' => '100%',
2134
            'Height' => '150',
2135
        ];
2136
2137
        if (is_array($type)) {
2138
            $editor_config = array_merge($editor_config, $type);
2139
        }
2140
2141
        $form->addHtmlEditor(
2142
            'exerciseDescription',
2143
            get_lang('ExerciseDescription'),
2144
            false,
2145
            false,
2146
            $editor_config
2147
        );
2148
2149
        $skillList = [];
2150
        if ('full' === $type) {
2151
            // Can't modify a DirectFeedback question.
2152
            if (!in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
2153
                $this->setResultFeedbackGroup($form);
2154
2155
                // Type of results display on the final page
2156
                $this->setResultDisabledGroup($form);
2157
2158
                // Type of questions disposition on page
2159
                $radios = [];
2160
                $radios[] = $form->createElement(
2161
                    'radio',
2162
                    'exerciseType',
2163
                    null,
2164
                    get_lang('SimpleExercise'),
2165
                    '1',
2166
                    [
2167
                        'onclick' => 'check_per_page_all()',
2168
                        'id' => 'option_page_all',
2169
                    ]
2170
                );
2171
                $radios[] = $form->createElement(
2172
                    'radio',
2173
                    'exerciseType',
2174
                    null,
2175
                    get_lang('SequentialExercise'),
2176
                    '2',
2177
                    [
2178
                        'onclick' => 'check_per_page_one()',
2179
                        'id' => 'option_page_one',
2180
                    ]
2181
                );
2182
2183
                $form->addGroup($radios, null, get_lang('QuestionsPerPage'));
2184
            } else {
2185
                // if is Direct feedback but has not questions we can allow to modify the question type
2186
                if (empty($this->iid) || 0 === $this->getQuestionCount()) {
2187
                    $this->setResultFeedbackGroup($form);
2188
                    $this->setResultDisabledGroup($form);
2189
2190
                    // Type of questions disposition on page
2191
                    $radios = [];
2192
                    $radios[] = $form->createElement('radio', 'exerciseType', null, get_lang('SimpleExercise'), '1');
2193
                    $radios[] = $form->createElement(
2194
                        'radio',
2195
                        'exerciseType',
2196
                        null,
2197
                        get_lang('SequentialExercise'),
2198
                        '2'
2199
                    );
2200
                    $form->addGroup($radios, null, get_lang('ExerciseType'));
2201
                } else {
2202
                    $this->setResultFeedbackGroup($form, true);
2203
                    $group = $this->setResultDisabledGroup($form);
2204
                    $group->freeze();
2205
2206
                    // we force the options to the DirectFeedback exercisetype
2207
                    //$form->addElement('hidden', 'exerciseFeedbackType', $this->getFeedbackType());
2208
                    //$form->addElement('hidden', 'exerciseType', ONE_PER_PAGE);
2209
2210
                    // Type of questions disposition on page
2211
                    $radios[] = $form->createElement(
2212
                        'radio',
2213
                        'exerciseType',
2214
                        null,
2215
                        get_lang('SimpleExercise'),
2216
                        '1',
2217
                        [
2218
                            'onclick' => 'check_per_page_all()',
2219
                            'id' => 'option_page_all',
2220
                        ]
2221
                    );
2222
                    $radios[] = $form->createElement(
2223
                        'radio',
2224
                        'exerciseType',
2225
                        null,
2226
                        get_lang('SequentialExercise'),
2227
                        '2',
2228
                        [
2229
                            'onclick' => 'check_per_page_one()',
2230
                            'id' => 'option_page_one',
2231
                        ]
2232
                    );
2233
2234
                    $type_group = $form->addGroup($radios, null, get_lang('QuestionsPerPage'));
2235
                    $type_group->freeze();
2236
                }
2237
            }
2238
2239
            $option = [
2240
                EX_Q_SELECTION_ORDERED => get_lang('OrderedByUser'),
2241
                //  Defined by user
2242
                EX_Q_SELECTION_RANDOM => get_lang('Random'),
2243
                // 1-10, All
2244
                'per_categories' => '--------'.get_lang('UsingCategories').'----------',
2245
                // Base (A 123 {3} B 456 {3} C 789{2} D 0{0}) --> Matrix {3, 3, 2, 0}
2246
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED => get_lang('OrderedCategoriesAlphabeticallyWithQuestionsOrdered'),
2247
                // A 123 B 456 C 78 (0, 1, all)
2248
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED => get_lang('RandomCategoriesWithQuestionsOrdered'),
2249
                // C 78 B 456 A 123
2250
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM => get_lang('OrderedCategoriesAlphabeticallyWithRandomQuestions'),
2251
                // A 321 B 654 C 87
2252
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM => get_lang('RandomCategoriesWithRandomQuestions'),
2253
                // C 87 B 654 A 321
2254
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED => get_lang('RandomCategoriesWithQuestionsOrderedNoQuestionGrouped'),
2255
                /*    B 456 C 78 A 123
2256
                        456 78 123
2257
                        123 456 78
2258
                */
2259
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED => get_lang('RandomCategoriesWithRandomQuestionsNoQuestionGrouped'),
2260
                /*
2261
                    A 123 B 456 C 78
2262
                    B 456 C 78 A 123
2263
                    B 654 C 87 A 321
2264
                    654 87 321
2265
                    165 842 73
2266
                */
2267
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED => get_lang('OrderedCategoriesByParentWithQuestionsOrdered'),
2268
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM => get_lang('OrderedCategoriesByParentWithQuestionsRandom'),
2269
            ];
2270
2271
            $form->addElement(
2272
                'select',
2273
                'question_selection_type',
2274
                [get_lang('QuestionSelection')],
2275
                $option,
2276
                [
2277
                    'id' => 'questionSelection',
2278
                    'onchange' => 'checkQuestionSelection()',
2279
                ]
2280
            );
2281
2282
            $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
2283
            if ($pageConfig) {
2284
                $group = [
2285
                    $form->createElement(
2286
                        'checkbox',
2287
                        'hide_expected_answer',
2288
                        null,
2289
                        get_lang('HideExpectedAnswer')
2290
                    ),
2291
                    $form->createElement(
2292
                        'checkbox',
2293
                        'hide_total_score',
2294
                        null,
2295
                        get_lang('HideTotalScore')
2296
                    ),
2297
                    $form->createElement(
2298
                        'checkbox',
2299
                        'hide_question_score',
2300
                        null,
2301
                        get_lang('HideQuestionScore')
2302
                    ),
2303
                    $form->createElement(
2304
                        'checkbox',
2305
                        'hide_category_table',
2306
                        null,
2307
                        get_lang('HideCategoryTable')
2308
                    ),
2309
                    $form->createElement(
2310
                        'checkbox',
2311
                        'hide_correct_answered_questions',
2312
                        null,
2313
                        get_lang('HideCorrectAnsweredQuestions')
2314
                    ),
2315
                    $form->createElement(
2316
                        'checkbox',
2317
                        'hide_comment',
2318
                        null,
2319
                        get_lang('HideComment')
2320
                    ),
2321
                ];
2322
                $form->addGroup($group, null, get_lang('ResultsConfigurationPage'));
2323
            }
2324
            $showHideConfiguration = api_get_configuration_value('quiz_hide_question_number');
2325
            if ($showHideConfiguration) {
2326
                $group = [
2327
                    $form->createElement('radio', 'hide_question_number', null, get_lang('Yes'), '1'),
2328
                    $form->createElement('radio', 'hide_question_number', null, get_lang('No'), '0'),
2329
                ];
2330
                $form->addGroup($group, null, get_lang('HideQuestionNumber'));
2331
            }
2332
2333
            $displayMatrix = 'none';
2334
            $displayRandom = 'none';
2335
            $selectionType = $this->getQuestionSelectionType();
2336
            switch ($selectionType) {
2337
                case EX_Q_SELECTION_RANDOM:
2338
                    $displayRandom = 'block';
2339
                    break;
2340
                case $selectionType >= EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED:
2341
                    $displayMatrix = 'block';
2342
                    break;
2343
            }
2344
2345
            $form->addHtml('<div id="hidden_random" style="display:'.$displayRandom.'">');
2346
            // Number of random question.
2347
            $max = ($this->iid > 0) ? $this->getQuestionCount() : 10;
2348
            $option = range(0, $max);
2349
            $option[0] = get_lang('No');
2350
            $option[-1] = get_lang('AllQuestionsShort');
2351
            $form->addElement(
2352
                'select',
2353
                'randomQuestions',
2354
                [
2355
                    get_lang('RandomQuestions'),
2356
                    get_lang('RandomQuestionsHelp'),
2357
                ],
2358
                $option,
2359
                ['id' => 'randomQuestions']
2360
            );
2361
            $form->addHtml('</div>');
2362
            $form->addHtml('<div id="hidden_matrix" style="display:'.$displayMatrix.'">');
2363
2364
            // Category selection.
2365
            $cat = new TestCategory();
2366
            $cat_form = $cat->returnCategoryForm($this);
2367
            if (empty($cat_form)) {
2368
                $cat_form = '<span class="label label-warning">'.get_lang('NoCategoriesDefined').'</span>';
2369
            }
2370
            $form->addElement('label', null, $cat_form);
2371
            $form->addHtml('</div>');
2372
2373
            // Random answers.
2374
            $radios_random_answers = [
2375
                $form->createElement('radio', 'randomAnswers', null, get_lang('Yes'), '1'),
2376
                $form->createElement('radio', 'randomAnswers', null, get_lang('No'), '0'),
2377
            ];
2378
            $form->addGroup($radios_random_answers, null, get_lang('RandomAnswers'));
2379
2380
            // Category name.
2381
            $radio_display_cat_name = [
2382
                $form->createElement('radio', 'display_category_name', null, get_lang('Yes'), '1'),
2383
                $form->createElement('radio', 'display_category_name', null, get_lang('No'), '0'),
2384
            ];
2385
            $form->addGroup($radio_display_cat_name, null, get_lang('QuestionDisplayCategoryName'));
2386
2387
            // Hide question title.
2388
            $group = [
2389
                $form->createElement('radio', 'hide_question_title', null, get_lang('Yes'), '1'),
2390
                $form->createElement('radio', 'hide_question_title', null, get_lang('No'), '0'),
2391
            ];
2392
            $form->addGroup($group, null, get_lang('HideQuestionTitle'));
2393
2394
            $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
2395
            if (true === $allow) {
2396
                // Hide question title.
2397
                $group = [
2398
                    $form->createElement(
2399
                        'radio',
2400
                        'show_previous_button',
2401
                        null,
2402
                        get_lang('Yes'),
2403
                        '1'
2404
                    ),
2405
                    $form->createElement(
2406
                        'radio',
2407
                        'show_previous_button',
2408
                        null,
2409
                        get_lang('No'),
2410
                        '0'
2411
                    ),
2412
                ];
2413
                $form->addGroup($group, null, get_lang('ShowPreviousButton'));
2414
            }
2415
2416
            $form->addElement(
2417
                'number',
2418
                'exerciseAttempts',
2419
                get_lang('ExerciseAttempts'),
2420
                null,
2421
                ['id' => 'exerciseAttempts']
2422
            );
2423
2424
            // Exercise time limit
2425
            $form->addElement(
2426
                'checkbox',
2427
                'activate_start_date_check',
2428
                null,
2429
                get_lang('EnableStartTime'),
2430
                ['onclick' => 'activate_start_date()']
2431
            );
2432
2433
            if (!empty($this->start_time)) {
2434
                $form->addElement('html', '<div id="start_date_div" style="display:block;">');
2435
            } else {
2436
                $form->addElement('html', '<div id="start_date_div" style="display:none;">');
2437
            }
2438
2439
            $form->addElement('date_time_picker', 'start_time');
2440
            $form->addElement('html', '</div>');
2441
            $form->addElement(
2442
                'checkbox',
2443
                'activate_end_date_check',
2444
                null,
2445
                get_lang('EnableEndTime'),
2446
                ['onclick' => 'activate_end_date()']
2447
            );
2448
2449
            if (!empty($this->end_time)) {
2450
                $form->addHtml('<div id="end_date_div" style="display:block;">');
2451
            } else {
2452
                $form->addHtml('<div id="end_date_div" style="display:none;">');
2453
            }
2454
2455
            $form->addElement('date_time_picker', 'end_time');
2456
            $form->addElement('html', '</div>');
2457
2458
            $display = 'block';
2459
            $form->addElement(
2460
                'checkbox',
2461
                'propagate_neg',
2462
                null,
2463
                get_lang('PropagateNegativeResults')
2464
            );
2465
2466
            if (api_get_configuration_value('allow_quiz_save_correct_options')) {
2467
                $options = [
2468
                    '' => get_lang('SelectAnOption'),
2469
                    1 => get_lang('SaveTheCorrectAnswersForTheNextAttempt'),
2470
                    2 => get_lang('SaveAllAnswers'),
2471
                ];
2472
                $form->addSelect(
2473
                    'save_correct_answers',
2474
                    get_lang('SaveAnswers'),
2475
                    $options
2476
                );
2477
            } else {
2478
                $form->addCheckBox(
2479
                    'save_correct_answers',
2480
                    null,
2481
                    get_lang('SaveTheCorrectAnswersForTheNextAttempt')
2482
                );
2483
            }
2484
2485
            $form->addElement('html', '<div class="clear">&nbsp;</div>');
2486
            $form->addElement('checkbox', 'review_answers', null, get_lang('ReviewAnswers'));
2487
            $form->addElement('html', '<div id="divtimecontrol"  style="display:'.$display.';">');
2488
2489
            // Timer control
2490
            $form->addElement(
2491
                'checkbox',
2492
                'enabletimercontrol',
2493
                null,
2494
                get_lang('EnableTimerControl'),
2495
                [
2496
                    'onclick' => 'option_time_expired()',
2497
                    'id' => 'enabletimercontrol',
2498
                    'onload' => 'check_load_time()',
2499
                ]
2500
            );
2501
2502
            $expired_date = (int) $this->selectExpiredTime();
2503
2504
            if (($expired_date != '0')) {
2505
                $form->addElement('html', '<div id="timercontrol" style="display:block;">');
2506
            } else {
2507
                $form->addElement('html', '<div id="timercontrol" style="display:none;">');
2508
            }
2509
            $form->addText(
2510
                'enabletimercontroltotalminutes',
2511
                get_lang('ExerciseTotalDurationInMinutes'),
2512
                false,
2513
                [
2514
                    'id' => 'enabletimercontroltotalminutes',
2515
                    'cols-size' => [2, 2, 8],
2516
                ]
2517
            );
2518
            $form->addElement('html', '</div>');
2519
2520
            if (api_get_configuration_value('quiz_prevent_backwards_move')) {
2521
                $form->addCheckBox(
2522
                    'prevent_backwards',
2523
                    null,
2524
                    get_lang('QuizPreventBackwards')
2525
                );
2526
            }
2527
2528
            $form->addElement(
2529
                'text',
2530
                'pass_percentage',
2531
                [get_lang('PassPercentage'), null, '%'],
2532
                ['id' => 'pass_percentage']
2533
            );
2534
2535
            $form->addRule('pass_percentage', get_lang('Numeric'), 'numeric');
2536
            $form->addRule('pass_percentage', get_lang('ValueTooSmall'), 'min_numeric_length', 0);
2537
            $form->addRule('pass_percentage', get_lang('ValueTooBig'), 'max_numeric_length', 100);
2538
2539
            // add the text_when_finished textbox
2540
            $form->addHtmlEditor(
2541
                'text_when_finished',
2542
                get_lang('TextWhenFinished'),
2543
                false,
2544
                false,
2545
                $editor_config
2546
            );
2547
2548
            if (true === api_get_configuration_value('exercise_text_when_finished_failure')) {
2549
                $form->addHtmlEditor(
2550
                    'text_when_finished_failure',
2551
                    get_lang('TextAppearingAtTheEndOfTheTestWhenTheUserHasFailed'),
2552
                    false,
2553
                    false,
2554
                    $editor_config
2555
                );
2556
            }
2557
2558
            $allow = api_get_configuration_value('allow_notification_setting_per_exercise');
2559
            if ($allow === true) {
2560
                $settings = ExerciseLib::getNotificationSettings();
2561
                $group = [];
2562
                foreach ($settings as $itemId => $label) {
2563
                    $group[] = $form->createElement(
2564
                        'checkbox',
2565
                        'notifications[]',
2566
                        null,
2567
                        $label,
2568
                        ['value' => $itemId]
2569
                    );
2570
                }
2571
                $form->addGroup($group, '', [get_lang('EmailNotifications')]);
2572
            }
2573
2574
            $form->addCheckBox(
2575
                'update_title_in_lps',
2576
                null,
2577
                get_lang('UpdateTitleInLps')
2578
            );
2579
2580
            $allowHideAttempts = api_get_configuration_value('quiz_hide_attempts_table_on_start_page');
2581
            if ($allowHideAttempts) {
2582
                $group = [
2583
                    $form->createElement('radio', 'hide_attempts_table', null, get_lang('Yes'), '1'),
2584
                    $form->createElement('radio', 'hide_attempts_table', null, get_lang('No'), '0'),
2585
                ];
2586
                $form->addGroup($group, null, get_lang('HideAttemptsTableOnStartPage'));
2587
            }
2588
2589
            $defaults = [];
2590
            if (api_get_setting('search_enabled') === 'true') {
2591
                require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
2592
                $form->addElement('checkbox', 'index_document', '', get_lang('SearchFeatureDoIndexDocument'));
2593
                $form->addSelectLanguage('language', get_lang('SearchFeatureDocumentLanguage'));
2594
                $specific_fields = get_specific_field_list();
2595
2596
                foreach ($specific_fields as $specific_field) {
2597
                    $form->addElement('text', $specific_field['code'], $specific_field['name']);
2598
                    $filter = [
2599
                        'c_id' => api_get_course_int_id(),
2600
                        'field_id' => $specific_field['id'],
2601
                        'ref_id' => $this->iid,
2602
                        'tool_id' => "'".TOOL_QUIZ."'",
2603
                    ];
2604
                    $values = get_specific_field_values_list($filter, ['value']);
2605
                    if (!empty($values)) {
2606
                        $arr_str_values = [];
2607
                        foreach ($values as $value) {
2608
                            $arr_str_values[] = $value['value'];
2609
                        }
2610
                        $defaults[$specific_field['code']] = implode(', ', $arr_str_values);
2611
                    }
2612
                }
2613
            }
2614
2615
            Skill::addSkillsToForm($form, api_get_course_int_id(), api_get_session_id(), ITEM_TYPE_EXERCISE, $this->iid);
2616
2617
            $extraField = new ExtraField('exercise');
2618
            $extraField->addElements(
2619
                $form,
2620
                $this->iid,
2621
                [
2622
                    'notifications',
2623
                    'remedialcourselist',
2624
                    'advancedcourselist',
2625
                    'subscribe_session_when_finished_failure',
2626
                ], //exclude
2627
                false, // filter
2628
                false, // tag as select
2629
                [], //show only fields
2630
                [], // order fields
2631
                [] // extra data
2632
            );
2633
2634
            // See BT#18165
2635
            $remedialList = [
2636
                'remedialcourselist' => 'RemedialCourses',
2637
                'advancedcourselist' => 'AdvancedCourses',
2638
            ];
2639
            $extraFieldExercice = new ExtraField('exercise');
2640
            $extraFieldExerciceValue = new ExtraFieldValue('exercise');
2641
            $pluginRemedial = api_get_plugin_setting('remedial_course', 'enabled') === 'true';
2642
            if ($pluginRemedial) {
2643
                $sessionId = api_get_session_id();
2644
                $userId = api_get_user_id();
2645
                foreach ($remedialList as $item => $label) {
2646
                    $remedialField = $extraFieldExercice->get_handler_field_info_by_field_variable($item);
2647
                    $optionRemedial = [];
2648
                    $defaults[$item] = [];
2649
                    $remedialExtraValue = $extraFieldExerciceValue->get_values_by_handler_and_field_id($this->iid, $remedialField['id']);
2650
                    $defaults[$item] = isset($remedialExtraValue['value']) ? explode(';', $remedialExtraValue['value']) : [];
2651
                    if ($sessionId != 0) {
2652
                        $courseList = SessionManager::getCoursesInSession($sessionId);
2653
                        foreach ($courseList as $course) {
2654
                            $courseSession = api_get_course_info_by_id($course);
2655
                            if (!empty($courseSession) && isset($courseSession['real_id'])) {
2656
                                $courseId = $courseSession['real_id'];
2657
                                if (api_get_course_int_id() != $courseId) {
2658
                                    $optionRemedial[$courseId] = $courseSession['title'];
2659
                                }
2660
                            }
2661
                        }
2662
                    } else {
2663
                        $courseList = CourseManager::get_course_list();
2664
                        foreach ($courseList as $course) {
2665
                            if (!empty($course) && isset($course['real_id'])) {
2666
                                $courseId = $course['real_id'];
2667
                                if (api_get_course_int_id() != $courseId) {
2668
                                    $optionRemedial[$courseId] = $course['title'];
2669
                                }
2670
                            }
2671
                        }
2672
                    }
2673
                    unset($optionRemedial[0]);
2674
                    $form->addSelect(
2675
                        "extra_".$item,
2676
                        get_plugin_lang($label, RemedialCoursePlugin::class),
2677
                        $optionRemedial,
2678
                        [
2679
                            'placeholder' => get_lang('SelectAnOption'),
2680
                            'multiple' => 'multiple',
2681
                        ]
2682
                    );
2683
                }
2684
            }
2685
2686
            if (true === api_get_configuration_value('exercise_subscribe_session_when_finished_failure')) {
2687
                $optionSessionWhenFailure = [];
2688
2689
                if ($failureSession = ExerciseLib::getSessionWhenFinishedFailure($this->iid)) {
2690
                    $defaults['subscribe_session_when_finished_failure'] = $failureSession->getId();
2691
                    $optionSessionWhenFailure[$failureSession->getId()] = $failureSession->getName();
2692
                }
2693
2694
                $form->addSelectAjax(
2695
                    'extra_subscribe_session_when_finished_failure',
2696
                    get_lang('SubscribeSessionWhenFinishedFailure'),
2697
                    $optionSessionWhenFailure,
2698
                    [
2699
                        'url' => api_get_path(WEB_AJAX_PATH).'session.ajax.php?'
2700
                            .http_build_query(['a' => 'search_session']),
2701
                    ]
2702
                );
2703
            }
2704
2705
            $settings = api_get_configuration_value('exercise_finished_notification_settings');
2706
            if (!empty($settings)) {
2707
                $options = [];
2708
                foreach ($settings as $name => $data) {
2709
                    $options[$name] = $name;
2710
                }
2711
                $form->addSelect(
2712
                    'extra_notifications',
2713
                    get_lang('Notifications'),
2714
                    $options,
2715
                    ['placeholder' => get_lang('SelectAnOption')]
2716
                );
2717
            }
2718
            $form->addElement('html', '</div>'); //End advanced setting
2719
            $form->addElement('html', '</div>');
2720
        }
2721
2722
        // submit
2723
        if (isset($_GET['exerciseId'])) {
2724
            $form->addButtonSave(get_lang('ModifyExercise'), 'submitExercise');
2725
        } else {
2726
            $form->addButtonUpdate(get_lang('ProcedToQuestions'), 'submitExercise');
2727
        }
2728
2729
        $form->addRule('exerciseTitle', get_lang('GiveExerciseName'), 'required');
2730
2731
        // defaults
2732
        if ($type == 'full') {
2733
            // rules
2734
            $form->addRule('exerciseAttempts', get_lang('Numeric'), 'numeric');
2735
            $form->addRule('start_time', get_lang('InvalidDate'), 'datetime');
2736
            $form->addRule('end_time', get_lang('InvalidDate'), 'datetime');
2737
2738
            if ($this->iid > 0) {
2739
                $defaults['randomQuestions'] = $this->random;
2740
                $defaults['randomAnswers'] = $this->getRandomAnswers();
2741
                $defaults['exerciseType'] = $this->selectType();
2742
                $defaults['exerciseTitle'] = $this->get_formated_title();
2743
                $defaults['exerciseDescription'] = $this->selectDescription();
2744
                $defaults['exerciseAttempts'] = $this->selectAttempts();
2745
                $defaults['exerciseFeedbackType'] = $this->getFeedbackType();
2746
                $defaults['results_disabled'] = $this->selectResultsDisabled();
2747
                $defaults['propagate_neg'] = $this->selectPropagateNeg();
2748
                $defaults['save_correct_answers'] = $this->getSaveCorrectAnswers();
2749
                $defaults['review_answers'] = $this->review_answers;
2750
                $defaults['randomByCat'] = $this->getRandomByCategory();
2751
                $defaults['text_when_finished'] = $this->getTextWhenFinished();
2752
                $defaults['display_category_name'] = $this->selectDisplayCategoryName();
2753
                $defaults['pass_percentage'] = $this->selectPassPercentage();
2754
                $defaults['question_selection_type'] = $this->getQuestionSelectionType();
2755
                $defaults['hide_question_title'] = $this->getHideQuestionTitle();
2756
                $defaults['show_previous_button'] = $this->showPreviousButton();
2757
                $defaults['exercise_category_id'] = $this->getExerciseCategoryId();
2758
                $defaults['prevent_backwards'] = $this->getPreventBackwards();
2759
2760
                if (true === api_get_configuration_value('exercise_text_when_finished_failure')) {
2761
                    $defaults['text_when_finished_failure'] = $this->getTextWhenFinishedFailure();
2762
                }
2763
2764
                if (!empty($this->start_time)) {
2765
                    $defaults['activate_start_date_check'] = 1;
2766
                }
2767
                if (!empty($this->end_time)) {
2768
                    $defaults['activate_end_date_check'] = 1;
2769
                }
2770
2771
                $defaults['start_time'] = !empty($this->start_time) ? api_get_local_time($this->start_time) : date('Y-m-d 12:00:00');
2772
                $defaults['end_time'] = !empty($this->end_time) ? api_get_local_time($this->end_time) : date('Y-m-d 12:00:00', time() + 84600);
2773
2774
                // Get expired time
2775
                if ($this->expired_time != '0') {
2776
                    $defaults['enabletimercontrol'] = 1;
2777
                    $defaults['enabletimercontroltotalminutes'] = $this->expired_time;
2778
                } else {
2779
                    $defaults['enabletimercontroltotalminutes'] = 0;
2780
                }
2781
                $defaults['notifications'] = $this->getNotifications();
2782
            } else {
2783
                $defaults['exerciseType'] = 2;
2784
                $defaults['exerciseAttempts'] = 0;
2785
                $defaults['randomQuestions'] = 0;
2786
                $defaults['randomAnswers'] = 0;
2787
                $defaults['exerciseDescription'] = '';
2788
                $defaults['exerciseFeedbackType'] = 0;
2789
                $defaults['results_disabled'] = 0;
2790
                $defaults['randomByCat'] = 0;
2791
                $defaults['text_when_finished'] = '';
2792
2793
                if (true === api_get_configuration_value('exercise_text_when_finished_failure')) {
2794
                    $defaults['text_when_finished_failure'] = '';
2795
                }
2796
2797
                $defaults['start_time'] = date('Y-m-d 12:00:00');
2798
                $defaults['display_category_name'] = 1;
2799
                $defaults['end_time'] = date('Y-m-d 12:00:00', time() + 84600);
2800
                $defaults['pass_percentage'] = '';
2801
                $defaults['end_button'] = $this->selectEndButton();
2802
                $defaults['question_selection_type'] = 1;
2803
                $defaults['hide_question_title'] = 0;
2804
                $defaults['show_previous_button'] = 1;
2805
                $defaults['on_success_message'] = null;
2806
                $defaults['on_failed_message'] = null;
2807
            }
2808
        } else {
2809
            $defaults['exerciseTitle'] = $this->selectTitle();
2810
            $defaults['exerciseDescription'] = $this->selectDescription();
2811
        }
2812
2813
        if (api_get_setting('search_enabled') === 'true') {
2814
            $defaults['index_document'] = 'checked="checked"';
2815
        }
2816
2817
        $this->setPageResultConfigurationDefaults($defaults);
2818
        $this->setHideQuestionNumberDefaults($defaults);
2819
        $this->setHideAttemptsTableOnStartPageDefaults($defaults);
2820
        $form->setDefaults($defaults);
2821
2822
        // Freeze some elements.
2823
        if ($this->iid != 0 && $this->edit_exercise_in_lp == false) {
2824
            $elementsToFreeze = [
2825
                'randomQuestions',
2826
                //'randomByCat',
2827
                'exerciseAttempts',
2828
                'propagate_neg',
2829
                'enabletimercontrol',
2830
                'review_answers',
2831
            ];
2832
2833
            foreach ($elementsToFreeze as $elementName) {
2834
                /** @var HTML_QuickForm_element $element */
2835
                $element = $form->getElement($elementName);
2836
                $element->freeze();
2837
            }
2838
        }
2839
    }
2840
2841
    public function setResultFeedbackGroup(FormValidator $form, $checkFreeze = true)
2842
    {
2843
        // Feedback type.
2844
        $feedback = [];
2845
        $warning = sprintf(
2846
            get_lang('TheSettingXWillChangeToX'),
2847
            get_lang('ShowResultsToStudents'),
2848
            get_lang('ShowScoreAndRightAnswer')
2849
        );
2850
        $endTest = $form->createElement(
2851
            'radio',
2852
            'exerciseFeedbackType',
2853
            null,
2854
            get_lang('ExerciseAtTheEndOfTheTest'),
2855
            EXERCISE_FEEDBACK_TYPE_END,
2856
            [
2857
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_END,
2858
                //'onclick' => 'if confirm() check_feedback()',
2859
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_feedback(); } else { return false;} ',
2860
            ]
2861
        );
2862
2863
        $noFeedBack = $form->createElement(
2864
            'radio',
2865
            'exerciseFeedbackType',
2866
            null,
2867
            get_lang('NoFeedback'),
2868
            EXERCISE_FEEDBACK_TYPE_EXAM,
2869
            [
2870
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_EXAM,
2871
            ]
2872
        );
2873
2874
        $feedback[] = $endTest;
2875
        $feedback[] = $noFeedBack;
2876
2877
        $scenarioEnabled = 'true' === api_get_setting('enable_quiz_scenario');
2878
        $freeze = true;
2879
        if ($scenarioEnabled) {
2880
            if ($this->getQuestionCount() > 0) {
2881
                $hasDifferentQuestion = $this->hasQuestionWithTypeNotInList([UNIQUE_ANSWER, HOT_SPOT_DELINEATION]);
2882
2883
                if (false === $hasDifferentQuestion) {
2884
                    $freeze = false;
2885
                }
2886
            } else {
2887
                $freeze = false;
2888
            }
2889
2890
            $direct = $form->createElement(
2891
                'radio',
2892
                'exerciseFeedbackType',
2893
                null,
2894
                get_lang('DirectFeedback'),
2895
                EXERCISE_FEEDBACK_TYPE_DIRECT,
2896
                [
2897
                    'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_DIRECT,
2898
                    'onclick' => 'check_direct_feedback()',
2899
                ]
2900
            );
2901
2902
            $directPopUp = $form->createElement(
2903
                'radio',
2904
                'exerciseFeedbackType',
2905
                null,
2906
                get_lang('ExerciseDirectPopUp'),
2907
                EXERCISE_FEEDBACK_TYPE_POPUP,
2908
                ['id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_POPUP, 'onclick' => 'check_direct_feedback()']
2909
            );
2910
2911
            if ($freeze) {
2912
                $direct->freeze();
2913
                $directPopUp->freeze();
2914
            }
2915
2916
            // If has delineation freeze all.
2917
            $hasDelineation = $this->hasQuestionWithType(HOT_SPOT_DELINEATION);
2918
            if ($hasDelineation) {
2919
                $endTest->freeze();
2920
                $noFeedBack->freeze();
2921
                $direct->freeze();
2922
                $directPopUp->freeze();
2923
            }
2924
2925
            $feedback[] = $direct;
2926
            $feedback[] = $directPopUp;
2927
        }
2928
2929
        $form->addGroup(
2930
            $feedback,
2931
            null,
2932
            [
2933
                get_lang('FeedbackType'),
2934
                get_lang('FeedbackDisplayOptions'),
2935
            ]
2936
        );
2937
    }
2938
2939
    /**
2940
     * function which process the creation of exercises.
2941
     *
2942
     * @param FormValidator $form
2943
     * @param string
2944
     *
2945
     * @return int c_quiz.iid
2946
     */
2947
    public function processCreation($form, $type = '')
2948
    {
2949
        $this->updateTitle(self::format_title_variable($form->getSubmitValue('exerciseTitle')));
2950
        $this->updateDescription($form->getSubmitValue('exerciseDescription'));
2951
        $this->updateAttempts($form->getSubmitValue('exerciseAttempts'));
2952
        $this->updateFeedbackType($form->getSubmitValue('exerciseFeedbackType'));
2953
        $this->updateType($form->getSubmitValue('exerciseType'));
2954
2955
        // If direct feedback then force to One per page
2956
        if (EXERCISE_FEEDBACK_TYPE_DIRECT == $form->getSubmitValue('exerciseFeedbackType')) {
2957
            $this->updateType(ONE_PER_PAGE);
2958
        }
2959
2960
        $this->setRandom($form->getSubmitValue('randomQuestions'));
2961
        $this->updateRandomAnswers($form->getSubmitValue('randomAnswers'));
2962
        $this->updateResultsDisabled($form->getSubmitValue('results_disabled'));
2963
        $this->updateExpiredTime($form->getSubmitValue('enabletimercontroltotalminutes'));
2964
        $this->updatePropagateNegative($form->getSubmitValue('propagate_neg'));
2965
        $this->updateSaveCorrectAnswers($form->getSubmitValue('save_correct_answers'));
2966
        $this->updateRandomByCat($form->getSubmitValue('randomByCat'));
2967
        $this->updateTextWhenFinished($form->getSubmitValue('text_when_finished'));
2968
2969
        if (true === api_get_configuration_value('exercise_text_when_finished_failure')) {
2970
            $this->setTextWhenFinishedFailure($form->getSubmitValue('text_when_finished_failure'));
2971
        }
2972
2973
        $this->updateDisplayCategoryName($form->getSubmitValue('display_category_name'));
2974
        $this->updateReviewAnswers($form->getSubmitValue('review_answers'));
2975
        $this->updatePassPercentage($form->getSubmitValue('pass_percentage'));
2976
        $this->updateCategories($form->getSubmitValue('category'));
2977
        $this->updateEndButton($form->getSubmitValue('end_button'));
2978
        $this->setOnSuccessMessage($form->getSubmitValue('on_success_message'));
2979
        $this->setOnFailedMessage($form->getSubmitValue('on_failed_message'));
2980
        $this->updateEmailNotificationTemplate($form->getSubmitValue('email_notification_template'));
2981
        $this->setEmailNotificationTemplateToUser($form->getSubmitValue('email_notification_template_to_user'));
2982
        $this->setNotifyUserByEmail($form->getSubmitValue('notify_user_by_email'));
2983
        $this->setModelType($form->getSubmitValue('model_type'));
2984
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2985
        $this->setHideQuestionTitle($form->getSubmitValue('hide_question_title'));
2986
        $this->sessionId = api_get_session_id();
2987
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2988
        $this->setScoreTypeModel($form->getSubmitValue('score_type_model'));
2989
        $this->setGlobalCategoryId($form->getSubmitValue('global_category_id'));
2990
        $this->setShowPreviousButton($form->getSubmitValue('show_previous_button'));
2991
        $this->setNotifications($form->getSubmitValue('notifications'));
2992
        $this->setExerciseCategoryId($form->getSubmitValue('exercise_category_id'));
2993
        $this->setPageResultConfiguration($form->getSubmitValues());
2994
        $showHideConfiguration = api_get_configuration_value('quiz_hide_question_number');
2995
        if ($showHideConfiguration) {
2996
            $this->setHideQuestionNumber($form->getSubmitValue('hide_question_number'));
2997
        }
2998
2999
        $this->setHideAttemptsTableOnStartPage($form->getSubmitValue('hide_attempts_table'));
3000
3001
        $this->preventBackwards = (int) $form->getSubmitValue('prevent_backwards');
3002
3003
        $this->start_time = null;
3004
        if ($form->getSubmitValue('activate_start_date_check') == 1) {
3005
            $start_time = $form->getSubmitValue('start_time');
3006
            $this->start_time = api_get_utc_datetime($start_time);
3007
        }
3008
3009
        $this->end_time = null;
3010
        if ($form->getSubmitValue('activate_end_date_check') == 1) {
3011
            $end_time = $form->getSubmitValue('end_time');
3012
            $this->end_time = api_get_utc_datetime($end_time);
3013
        }
3014
3015
        $this->expired_time = 0;
3016
        if ($form->getSubmitValue('enabletimercontrol') == 1) {
3017
            $expired_total_time = $form->getSubmitValue('enabletimercontroltotalminutes');
3018
            if ($this->expired_time == 0) {
3019
                $this->expired_time = $expired_total_time;
3020
            }
3021
        }
3022
3023
        $this->random_answers = 0;
3024
        if ($form->getSubmitValue('randomAnswers') == 1) {
3025
            $this->random_answers = 1;
3026
        }
3027
3028
        // Update title in all LPs that have this quiz added
3029
        if ($form->getSubmitValue('update_title_in_lps') == 1) {
3030
            $courseId = api_get_course_int_id();
3031
            $table = Database::get_course_table(TABLE_LP_ITEM);
3032
            $sql = "SELECT * FROM $table
3033
                    WHERE
3034
                        c_id = $courseId AND
3035
                        item_type = 'quiz' AND
3036
                        path = '".$this->iid."'
3037
                    ";
3038
            $result = Database::query($sql);
3039
            $items = Database::store_result($result);
3040
            if (!empty($items)) {
3041
                foreach ($items as $item) {
3042
                    $itemId = $item['iid'];
3043
                    $sql = "UPDATE $table SET title = '".$this->title."'
3044
                            WHERE iid = $itemId AND c_id = $courseId ";
3045
                    Database::query($sql);
3046
                }
3047
            }
3048
        }
3049
3050
        $iid = $this->save($type);
3051
        if (!empty($iid)) {
3052
            $values = $form->getSubmitValues();
3053
            $values['item_id'] = $iid;
3054
            $extraFieldValue = new ExtraFieldValue('exercise');
3055
            $extraFieldValue->saveFieldValues($values);
3056
3057
            Skill::saveSkills($form, ITEM_TYPE_EXERCISE, $iid);
3058
        }
3059
    }
3060
3061
    public function search_engine_save()
3062
    {
3063
        if ($_POST['index_document'] != 1) {
3064
            return;
3065
        }
3066
        $course_id = api_get_course_id();
3067
3068
        require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
3069
3070
        $specific_fields = get_specific_field_list();
3071
        $ic_slide = new IndexableChunk();
3072
3073
        $all_specific_terms = '';
3074
        foreach ($specific_fields as $specific_field) {
3075
            if (isset($_REQUEST[$specific_field['code']])) {
3076
                $sterms = trim($_REQUEST[$specific_field['code']]);
3077
                if (!empty($sterms)) {
3078
                    $all_specific_terms .= ' '.$sterms;
3079
                    $sterms = explode(',', $sterms);
3080
                    foreach ($sterms as $sterm) {
3081
                        $ic_slide->addTerm(trim($sterm), $specific_field['code']);
3082
                        add_specific_field_value($specific_field['id'], $course_id, TOOL_QUIZ, $this->iid, $sterm);
3083
                    }
3084
                }
3085
            }
3086
        }
3087
3088
        // build the chunk to index
3089
        $ic_slide->addValue("title", $this->exercise);
3090
        $ic_slide->addCourseId($course_id);
3091
        $ic_slide->addToolId(TOOL_QUIZ);
3092
        $xapian_data = [
3093
            SE_COURSE_ID => $course_id,
3094
            SE_TOOL_ID => TOOL_QUIZ,
3095
            SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->iid],
3096
            SE_USER => (int) api_get_user_id(),
3097
        ];
3098
        $ic_slide->xapian_data = serialize($xapian_data);
3099
        $exercise_description = $all_specific_terms.' '.$this->description;
3100
        $ic_slide->addValue("content", $exercise_description);
3101
3102
        $di = new ChamiloIndexer();
3103
        isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
3104
        $di->connectDb(null, null, $lang);
3105
        $di->addChunk($ic_slide);
3106
3107
        //index and return search engine document id
3108
        $did = $di->index();
3109
        if ($did) {
3110
            // save it to db
3111
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
3112
            $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
3113
			    VALUES (NULL , \'%s\', \'%s\', %s, %s)';
3114
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->iid, $did);
3115
            Database::query($sql);
3116
        }
3117
    }
3118
3119
    public function search_engine_edit()
3120
    {
3121
        // update search enchine and its values table if enabled
3122
        if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
3123
            $course_id = api_get_course_id();
3124
3125
            // actually, it consists on delete terms from db,
3126
            // insert new ones, create a new search engine document, and remove the old one
3127
            // get search_did
3128
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
3129
            $sql = 'SELECT * FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s LIMIT 1';
3130
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->iid);
3131
            $res = Database::query($sql);
3132
3133
            if (Database::num_rows($res) > 0) {
3134
                require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
3135
3136
                $se_ref = Database::fetch_array($res);
3137
                $specific_fields = get_specific_field_list();
3138
                $ic_slide = new IndexableChunk();
3139
3140
                $all_specific_terms = '';
3141
                foreach ($specific_fields as $specific_field) {
3142
                    delete_all_specific_field_value($course_id, $specific_field['id'], TOOL_QUIZ, $this->iid);
3143
                    if (isset($_REQUEST[$specific_field['code']])) {
3144
                        $sterms = trim($_REQUEST[$specific_field['code']]);
3145
                        $all_specific_terms .= ' '.$sterms;
3146
                        $sterms = explode(',', $sterms);
3147
                        foreach ($sterms as $sterm) {
3148
                            $ic_slide->addTerm(trim($sterm), $specific_field['code']);
3149
                            add_specific_field_value($specific_field['id'], $course_id, TOOL_QUIZ, $this->iid, $sterm);
3150
                        }
3151
                    }
3152
                }
3153
3154
                // build the chunk to index
3155
                $ic_slide->addValue('title', $this->exercise);
3156
                $ic_slide->addCourseId($course_id);
3157
                $ic_slide->addToolId(TOOL_QUIZ);
3158
                $xapian_data = [
3159
                    SE_COURSE_ID => $course_id,
3160
                    SE_TOOL_ID => TOOL_QUIZ,
3161
                    SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => $this->iid],
3162
                    SE_USER => api_get_user_id(),
3163
                ];
3164
                $ic_slide->xapian_data = serialize($xapian_data);
3165
                $exercise_description = $all_specific_terms.' '.$this->description;
3166
                $ic_slide->addValue('content', $exercise_description);
3167
3168
                $di = new ChamiloIndexer();
3169
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
3170
                $di->connectDb(null, null, $lang);
3171
                $di->remove_document($se_ref['search_did']);
3172
                $di->addChunk($ic_slide);
3173
3174
                //index and return search engine document id
3175
                $did = $di->index();
3176
                if ($did) {
3177
                    // save it to db
3178
                    $sql = 'DELETE FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=\'%s\'';
3179
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->iid);
3180
                    Database::query($sql);
3181
                    $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
3182
                        VALUES (NULL , \'%s\', \'%s\', %s, %s)';
3183
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->iid, $did);
3184
                    Database::query($sql);
3185
                }
3186
            } else {
3187
                $this->search_engine_save();
3188
            }
3189
        }
3190
    }
3191
3192
    public function search_engine_delete()
3193
    {
3194
        // remove from search engine if enabled
3195
        if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
3196
            $course_id = api_get_course_id();
3197
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
3198
            $sql = 'SELECT * FROM %s
3199
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
3200
                    LIMIT 1';
3201
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->iid);
3202
            $res = Database::query($sql);
3203
            if (Database::num_rows($res) > 0) {
3204
                $row = Database::fetch_array($res);
3205
                $di = new ChamiloIndexer();
3206
                $di->remove_document($row['search_did']);
3207
                unset($di);
3208
                $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
3209
                foreach ($this->questionList as $question_i) {
3210
                    $sql = 'SELECT type FROM %s WHERE iid = %s';
3211
                    $sql = sprintf($sql, $tbl_quiz_question, $question_i);
3212
                    $qres = Database::query($sql);
3213
                    if (Database::num_rows($qres) > 0) {
3214
                        $qrow = Database::fetch_array($qres);
3215
                        $objQuestion = Question::getInstance($qrow['type']);
3216
                        $objQuestion = Question::read((int) $question_i);
3217
                        $objQuestion->search_engine_edit($this->iid, false, true);
3218
                        unset($objQuestion);
3219
                    }
3220
                }
3221
            }
3222
            $sql = 'DELETE FROM %s
3223
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
3224
                    LIMIT 1';
3225
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->iid);
3226
            Database::query($sql);
3227
3228
            // remove terms from db
3229
            require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
3230
            delete_all_values_for_item($course_id, TOOL_QUIZ, $this->iid);
3231
        }
3232
    }
3233
3234
    public function selectExpiredTime()
3235
    {
3236
        return $this->expired_time;
3237
    }
3238
3239
    /**
3240
     * Cleans the student's results only for the Exercise tool (Not from the LP)
3241
     * The LP results are NOT deleted by default, otherwise put $cleanLpTests = true
3242
     * Works with exercises in sessions.
3243
     *
3244
     * @param bool   $cleanLpTests
3245
     * @param string $cleanResultBeforeDate
3246
     *
3247
     * @return int quantity of user's exercises deleted
3248
     */
3249
    public function cleanResults($cleanLpTests = false, $cleanResultBeforeDate = null)
3250
    {
3251
        $sessionId = api_get_session_id();
3252
        $table_track_e_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3253
        $table_track_e_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3254
3255
        $sql_where = '  AND
3256
                        orig_lp_id = 0 AND
3257
                        orig_lp_item_id = 0';
3258
3259
        // if we want to delete results from LP too
3260
        if ($cleanLpTests) {
3261
            $sql_where = '';
3262
        }
3263
3264
        // if we want to delete attempts before date $cleanResultBeforeDate
3265
        // $cleanResultBeforeDate must be a valid UTC-0 date yyyy-mm-dd
3266
3267
        if (!empty($cleanResultBeforeDate)) {
3268
            $cleanResultBeforeDate = Database::escape_string($cleanResultBeforeDate);
3269
            if (api_is_valid_date($cleanResultBeforeDate)) {
3270
                $sql_where .= "  AND exe_date <= '$cleanResultBeforeDate' ";
3271
            } else {
3272
                return 0;
3273
            }
3274
        }
3275
3276
        $sql = "SELECT exe_id
3277
            FROM $table_track_e_exercises
3278
            WHERE
3279
                c_id = ".api_get_course_int_id()." AND
3280
                exe_exo_id = ".$this->iid." AND
3281
                session_id = ".$sessionId." ".
3282
                $sql_where;
3283
3284
        $result = Database::query($sql);
3285
        $exe_list = Database::store_result($result);
3286
3287
        // deleting TRACK_E_ATTEMPT table
3288
        // check if exe in learning path or not
3289
        $i = 0;
3290
        if (is_array($exe_list) && count($exe_list) > 0) {
3291
            foreach ($exe_list as $item) {
3292
                $sql = "DELETE FROM $table_track_e_attempt
3293
                        WHERE exe_id = '".$item['exe_id']."'";
3294
                Database::query($sql);
3295
                $i++;
3296
            }
3297
        }
3298
3299
        // delete TRACK_E_EXERCISES table
3300
        $sql = "DELETE FROM $table_track_e_exercises
3301
                WHERE
3302
                  c_id = ".api_get_course_int_id()." AND
3303
                  exe_exo_id = ".$this->iid." $sql_where AND
3304
                  session_id = ".$sessionId;
3305
        Database::query($sql);
3306
3307
        $this->generateStats($this->iid, api_get_course_info(), $sessionId);
3308
3309
        Event::addEvent(
3310
            LOG_EXERCISE_RESULT_DELETE,
3311
            LOG_EXERCISE_ID,
3312
            $this->iid,
3313
            null,
3314
            null,
3315
            api_get_course_int_id(),
3316
            $sessionId
3317
        );
3318
3319
        return $i;
3320
    }
3321
3322
    /**
3323
     * Copies an exercise (duplicate all questions and answers).
3324
     */
3325
    public function copyExercise()
3326
    {
3327
        $exerciseObject = $this;
3328
        $categories = $exerciseObject->getCategoriesInExercise(true);
3329
        // Get all questions no matter the order/category settings
3330
        $questionList = $exerciseObject->getQuestionOrderedList();
3331
        $sourceId = $exerciseObject->iid;
3332
        // Force the creation of a new exercise
3333
        $exerciseObject->updateTitle($exerciseObject->selectTitle().' - '.get_lang('Copy'));
3334
        // Hides the new exercise
3335
        $exerciseObject->updateStatus(false);
3336
        $exerciseObject->updateId(0);
3337
        $exerciseObject->sessionId = api_get_session_id();
3338
        $courseId = api_get_course_int_id();
3339
        $exerciseObject->save();
3340
        $newId = $exerciseObject->selectId();
3341
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
3342
3343
        $count = 1;
3344
        $batchSize = 20;
3345
        $em = Database::getManager();
3346
3347
        if ($newId && !empty($questionList)) {
3348
            $extraField = new ExtraFieldValue('exercise');
3349
            $extraField->copy($sourceId, $newId);
3350
3351
            // Question creation
3352
            foreach ($questionList as $oldQuestionId) {
3353
                $oldQuestionObj = Question::read($oldQuestionId, null, false);
3354
                $newQuestionId = $oldQuestionObj->duplicate();
3355
                if ($newQuestionId) {
3356
                    $newQuestionObj = Question::read($newQuestionId, null, false);
3357
                    if (isset($newQuestionObj) && $newQuestionObj) {
3358
                        $sql = "INSERT INTO $exerciseRelQuestionTable (c_id, question_id, exercice_id, question_order)
3359
                                VALUES ($courseId, ".$newQuestionId.", ".$newId.", '$count')";
3360
                        Database::query($sql);
3361
                        $count++;
3362
3363
                        if (!empty($oldQuestionObj->category)) {
3364
                            $newQuestionObj->saveCategory($oldQuestionObj->category);
3365
                        }
3366
3367
                        // This should be moved to the duplicate function
3368
                        $newAnswerObj = new Answer($oldQuestionId, $courseId, $exerciseObject);
3369
                        $newAnswerObj->read();
3370
                        $newAnswerObj->duplicate($newQuestionObj);
3371
3372
                        if (($count % $batchSize) === 0) {
3373
                            $em->clear(); // Detaches all objects from Doctrine!
3374
                        }
3375
                    }
3376
                }
3377
            }
3378
            if (!empty($categories)) {
3379
                $newCategoryList = [];
3380
                foreach ($categories as $category) {
3381
                    $newCategoryList[$category['category_id']] = $category['count_questions'];
3382
                }
3383
                $exerciseObject->save_categories_in_exercise($newCategoryList);
3384
            }
3385
        }
3386
    }
3387
3388
    /**
3389
     * Changes the exercise status.
3390
     *
3391
     * @param string $status - exercise status
3392
     */
3393
    public function updateStatus($status)
3394
    {
3395
        $this->active = $status;
3396
    }
3397
3398
    /**
3399
     * Get the contents of the track_e_exercises table for the current
3400
     * exercise object, in the specific context (if defined) of a
3401
     * learning path and optionally a current progress status.
3402
     *
3403
     * @param int    $lp_id
3404
     * @param int    $lp_item_id
3405
     * @param int    $lp_item_view_id
3406
     * @param string $status
3407
     *
3408
     * @return array
3409
     */
3410
    public function get_stat_track_exercise_info(
3411
        $lp_id = 0,
3412
        $lp_item_id = 0,
3413
        $lp_item_view_id = 0,
3414
        $status = 'incomplete'
3415
    ) {
3416
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3417
        if (empty($lp_id)) {
3418
            $lp_id = 0;
3419
        }
3420
        if (empty($lp_item_id)) {
3421
            $lp_item_id = 0;
3422
        }
3423
        if (empty($lp_item_view_id)) {
3424
            $lp_item_view_id = 0;
3425
        }
3426
        $condition = ' WHERE exe_exo_id 	= '.$this->iid.' AND
3427
					   exe_user_id 			= '.api_get_user_id().' AND
3428
					   c_id                 = '.api_get_course_int_id().' AND
3429
					   status 				= \''.Database::escape_string($status).'\' AND
3430
					   orig_lp_id 			= \''.$lp_id.'\' AND
3431
					   orig_lp_item_id 		= \''.$lp_item_id.'\' AND
3432
                       orig_lp_item_view_id = \''.$lp_item_view_id.'\' AND
3433
					   session_id 			= \''.api_get_session_id().'\' LIMIT 1'; //Adding limit 1 just in case
3434
3435
        $sql_track = 'SELECT * FROM '.$track_exercises.$condition;
3436
3437
        $result = Database::query($sql_track);
3438
        $new_array = [];
3439
        if (Database::num_rows($result) > 0) {
3440
            $new_array = Database::fetch_array($result, 'ASSOC');
3441
            $new_array['num_exe'] = Database::num_rows($result);
3442
        }
3443
3444
        return $new_array;
3445
    }
3446
3447
    /**
3448
     * Saves a test attempt.
3449
     *
3450
     * @param int $clock_expired_time clock_expired_time
3451
     * @param int  int lp id
3452
     * @param int  int lp item id
3453
     * @param int  int lp item_view id
3454
     * @param array $questionList
3455
     * @param float $weight
3456
     *
3457
     * @return int
3458
     */
3459
    public function save_stat_track_exercise_info(
3460
        $clock_expired_time = 0,
3461
        $safe_lp_id = 0,
3462
        $safe_lp_item_id = 0,
3463
        $safe_lp_item_view_id = 0,
3464
        $questionList = [],
3465
        $weight = 0
3466
    ) {
3467
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3468
        $safe_lp_id = (int) $safe_lp_id;
3469
        $safe_lp_item_id = (int) $safe_lp_item_id;
3470
        $safe_lp_item_view_id = (int) $safe_lp_item_view_id;
3471
3472
        if (empty($clock_expired_time)) {
3473
            $clock_expired_time = null;
3474
        }
3475
3476
        $questionList = array_map('intval', $questionList);
3477
3478
        $params = [
3479
            'exe_exo_id' => $this->iid,
3480
            'exe_user_id' => api_get_user_id(),
3481
            'c_id' => api_get_course_int_id(),
3482
            'status' => 'incomplete',
3483
            'session_id' => api_get_session_id(),
3484
            'data_tracking' => implode(',', $questionList),
3485
            'start_date' => api_get_utc_datetime(),
3486
            'orig_lp_id' => $safe_lp_id,
3487
            'orig_lp_item_id' => $safe_lp_item_id,
3488
            'orig_lp_item_view_id' => $safe_lp_item_view_id,
3489
            'exe_weighting' => $weight,
3490
            'user_ip' => Database::escape_string(api_get_real_ip()),
3491
            'exe_date' => api_get_utc_datetime(),
3492
            'exe_result' => 0,
3493
            'steps_counter' => 0,
3494
            'exe_duration' => 0,
3495
            'expired_time_control' => $clock_expired_time,
3496
            'questions_to_check' => '',
3497
        ];
3498
3499
        return Database::insert($track_exercises, $params);
3500
    }
3501
3502
    /**
3503
     * @param int    $question_id
3504
     * @param int    $questionNum
3505
     * @param array  $questions_in_media
3506
     * @param string $currentAnswer
3507
     * @param array  $myRemindList
3508
     * @param bool   $showPreviousButton
3509
     *
3510
     * @return string
3511
     */
3512
    public function show_button(
3513
        $question_id,
3514
        $questionNum,
3515
        $questions_in_media = [],
3516
        $currentAnswer = '',
3517
        $myRemindList = [],
3518
        $showPreviousButton = true,
3519
        int $lpId = 0,
3520
        int $lpItemId = 0,
3521
        int $lpItemViewId = 0
3522
    ) {
3523
        $nbrQuestions = $this->countQuestionsInExercise();
3524
        $buttonList = [];
3525
        $html = $label = '';
3526
        $hotspotGet = isset($_POST['hotspot']) ? Security::remove_XSS($_POST['hotspot']) : null;
3527
3528
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]) &&
3529
            $this->type == ONE_PER_PAGE
3530
        ) {
3531
            $urlTitle = get_lang('ContinueTest');
3532
            if ($questionNum == count($this->questionList)) {
3533
                $urlTitle = get_lang('EndTest');
3534
            }
3535
3536
            $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit_modal.php?'.api_get_cidreq();
3537
            $url .= '&'.http_build_query([
3538
                'learnpath_id' => $lpId,
3539
                'learnpath_item_id' => $lpItemId,
3540
                'learnpath_item_view_id' => $lpItemViewId,
3541
                'hotspot' => $hotspotGet,
3542
                'nbrQuestions' => $nbrQuestions,
3543
                'num' => $questionNum,
3544
                'exerciseType' => $this->type,
3545
                'exerciseId' => $this->iid,
3546
                'reminder' => empty($myRemindList) ? null : 2,
3547
                'tryagain' => isset($_REQUEST['tryagain']) && 1 === (int) $_REQUEST['tryagain'] ? 1 : 0,
3548
            ]);
3549
3550
            $params = [
3551
                'class' => 'ajax btn btn-default no-close-button',
3552
                'data-title' => Security::remove_XSS(get_lang('Comment')),
3553
                'data-size' => 'md',
3554
                'id' => "button_$question_id",
3555
            ];
3556
3557
            if ($this->getFeedbackType() === EXERCISE_FEEDBACK_TYPE_POPUP) {
3558
                $params['data-block-closing'] = 'true';
3559
                $params['class'] .= ' no-header ';
3560
            }
3561
3562
            $html .= Display::url($urlTitle, $url, $params);
3563
            $html .= '<br />';
3564
3565
            return $html;
3566
        }
3567
3568
        if (!api_is_allowed_to_session_edit()) {
3569
            return '';
3570
        }
3571
3572
        $isReviewingAnswers = isset($_REQUEST['reminder']) && 2 === (int) $_REQUEST['reminder'];
3573
        $endReminderValue = false;
3574
        if (!empty($myRemindList) && $isReviewingAnswers) {
3575
            $endValue = end($myRemindList);
3576
            if ($endValue == $question_id) {
3577
                $endReminderValue = true;
3578
            }
3579
        }
3580
        $endTest = false;
3581
        if ($this->type == ALL_ON_ONE_PAGE || $nbrQuestions == $questionNum || $endReminderValue) {
3582
            if ($this->review_answers) {
3583
                $label = get_lang('ReviewQuestions');
3584
                $class = 'btn btn-success';
3585
            } else {
3586
                $endTest = true;
3587
                $label = get_lang('EndTest');
3588
                $class = 'btn btn-warning';
3589
            }
3590
        } else {
3591
            $label = get_lang('NextQuestion');
3592
            $class = 'btn btn-primary';
3593
        }
3594
        // used to select it with jquery
3595
        $class .= ' question-validate-btn';
3596
        if ($this->type == ONE_PER_PAGE) {
3597
            if ($questionNum != 1 && $this->showPreviousButton()) {
3598
                $prev_question = $questionNum - 2;
3599
                $showPreview = true;
3600
                if (!empty($myRemindList) && $isReviewingAnswers) {
3601
                    $beforeId = null;
3602
                    for ($i = 0; $i < count($myRemindList); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
3603
                        if (isset($myRemindList[$i]) && $myRemindList[$i] == $question_id) {
3604
                            $beforeId = isset($myRemindList[$i - 1]) ? $myRemindList[$i - 1] : null;
3605
                            break;
3606
                        }
3607
                    }
3608
3609
                    if (empty($beforeId)) {
3610
                        $showPreview = false;
3611
                    } else {
3612
                        $num = 0;
3613
                        foreach ($this->questionList as $originalQuestionId) {
3614
                            if ($originalQuestionId == $beforeId) {
3615
                                break;
3616
                            }
3617
                            $num++;
3618
                        }
3619
                        $prev_question = $num;
3620
                    }
3621
                }
3622
3623
                if ($showPreviousButton && $showPreview && 0 === $this->getPreventBackwards()) {
3624
                    $buttonList[] = Display::button(
3625
                        'previous_question_and_save',
3626
                        get_lang('PreviousQuestion'),
3627
                        [
3628
                            'type' => 'button',
3629
                            'class' => 'btn btn-default',
3630
                            'data-prev' => $prev_question,
3631
                            'data-question' => $question_id,
3632
                        ]
3633
                    );
3634
                }
3635
            }
3636
3637
            // Next question
3638
            if (!empty($questions_in_media)) {
3639
                $buttonList[] = Display::button(
3640
                    'save_question_list',
3641
                    $label,
3642
                    [
3643
                        'type' => 'button',
3644
                        'class' => $class,
3645
                        'data-list' => implode(",", $questions_in_media),
3646
                    ]
3647
                );
3648
            } else {
3649
                $attributes = ['type' => 'button', 'class' => $class, 'data-question' => $question_id];
3650
                $name = 'save_now';
3651
                if ($endTest && api_get_configuration_value('quiz_check_all_answers_before_end_test')) {
3652
                    $name = 'check_answers';
3653
                }
3654
                $buttonList[] = Display::button(
3655
                    $name,
3656
                    $label,
3657
                    $attributes
3658
                );
3659
            }
3660
            $buttonList[] = '<span id="save_for_now_'.$question_id.'" class="exercise_save_mini_message"></span>';
3661
3662
            $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3663
3664
            return $html;
3665
        }
3666
3667
        if ($this->review_answers) {
3668
            $all_label = get_lang('ReviewQuestions');
3669
            $class = 'btn btn-success';
3670
        } else {
3671
            $all_label = get_lang('EndTest');
3672
            $class = 'btn btn-warning';
3673
        }
3674
        // used to select it with jquery
3675
        $class .= ' question-validate-btn';
3676
        $buttonList[] = Display::button(
3677
            'validate_all',
3678
            $all_label,
3679
            ['type' => 'button', 'class' => $class]
3680
        );
3681
        $buttonList[] = Display::span(null, ['id' => 'save_all_response']);
3682
        $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3683
3684
        return $html;
3685
    }
3686
3687
    /**
3688
     * @param int    $timeLeft in seconds
3689
     * @param string $url
3690
     *
3691
     * @return string
3692
     */
3693
    public function showSimpleTimeControl($timeLeft, $url = '')
3694
    {
3695
        $timeLeft = (int) $timeLeft;
3696
3697
        return "<script>
3698
            function openClockWarning() {
3699
                $('#clock_warning').dialog({
3700
                    modal:true,
3701
                    height:320,
3702
                    width:550,
3703
                    closeOnEscape: false,
3704
                    resizable: false,
3705
                    buttons: {
3706
                        '".addslashes(get_lang('Close'))."': function() {
3707
                            $('#clock_warning').dialog('close');
3708
                        }
3709
                    },
3710
                    close: function() {
3711
                        window.location.href = '$url';
3712
                    }
3713
                });
3714
                $('#clock_warning').dialog('open');
3715
                $('#counter_to_redirect').epiclock({
3716
                    mode: $.epiclock.modes.countdown,
3717
                    offset: {seconds: 5},
3718
                    format: 's'
3719
                }).bind('timer', function () {
3720
                    window.location.href = '$url';
3721
                });
3722
            }
3723
3724
            function onExpiredTimeExercise() {
3725
                $('#wrapper-clock').hide();
3726
                $('#expired-message-id').show();
3727
                // Fixes bug #5263
3728
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3729
                openClockWarning();
3730
            }
3731
3732
			$(function() {
3733
				// time in seconds when using minutes there are some seconds lost
3734
                var time_left = parseInt(".$timeLeft.");
3735
                $('#exercise_clock_warning').epiclock({
3736
                    mode: $.epiclock.modes.countdown,
3737
                    offset: {seconds: time_left},
3738
                    format: 'x:i:s',
3739
                    renderer: 'minute'
3740
                }).bind('timer', function () {
3741
                    onExpiredTimeExercise();
3742
                });
3743
	       		$('#submit_save').click(function () {});
3744
	        });
3745
	    </script>";
3746
    }
3747
3748
    /**
3749
     * So the time control will work.
3750
     *
3751
     * @param int    $timeLeft
3752
     * @param string $redirectToUrl
3753
     *
3754
     * @return string
3755
     */
3756
    public function showTimeControlJS($timeLeft, $redirectToUrl = '')
3757
    {
3758
        $timeLeft = (int) $timeLeft;
3759
        $script = 'redirectExerciseToResult();';
3760
        if (ALL_ON_ONE_PAGE == $this->type) {
3761
            $script = "save_now_all('validate');";
3762
        } elseif (ONE_PER_PAGE == $this->type) {
3763
            $script = 'window.quizTimeEnding = true;
3764
                $(\'[name="save_now"]\').trigger(\'click\');';
3765
        }
3766
3767
        $exerciseSubmitRedirect = '';
3768
        if (!empty($redirectToUrl)) {
3769
            $exerciseSubmitRedirect = "window.location = '$redirectToUrl'";
3770
        }
3771
3772
        return "<script>
3773
            function openClockWarning() {
3774
                $('#clock_warning').dialog({
3775
                    modal:true,
3776
                    height:320,
3777
                    width:550,
3778
                    closeOnEscape: false,
3779
                    resizable: false,
3780
                    buttons: {
3781
                        '".addslashes(get_lang('EndTest'))."': function() {
3782
                            $('#clock_warning').dialog('close');
3783
                        }
3784
                    },
3785
                    close: function() {
3786
                        send_form();
3787
                    }
3788
                });
3789
3790
                $('#clock_warning').dialog('open');
3791
                $('#counter_to_redirect').epiclock({
3792
                    mode: $.epiclock.modes.countdown,
3793
                    offset: {seconds: 5},
3794
                    format: 's'
3795
                }).bind('timer', function () {
3796
                    send_form();
3797
                });
3798
            }
3799
3800
            function send_form() {
3801
                if ($('#exercise_form').length) {
3802
                    $script
3803
                } else {
3804
                    $exerciseSubmitRedirect
3805
                    // In exercise_reminder.php
3806
                    final_submit();
3807
                }
3808
            }
3809
3810
            function onExpiredTimeExercise() {
3811
                $('#wrapper-clock').hide();
3812
                $('#expired-message-id').show();
3813
                // Fixes bug #5263
3814
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3815
                openClockWarning();
3816
            }
3817
3818
			$(function() {
3819
				// time in seconds when using minutes there are some seconds lost
3820
                var time_left = parseInt(".$timeLeft.");
3821
                $('#exercise_clock_warning').epiclock({
3822
                    mode: $.epiclock.modes.countdown,
3823
                    offset: {seconds: time_left},
3824
                    format: 'x:C:s',
3825
                    renderer: 'minute'
3826
                }).bind('timer', function () {
3827
                    onExpiredTimeExercise();
3828
                });
3829
	       		$('#submit_save').click(function () {});
3830
	        });
3831
	    </script>";
3832
    }
3833
3834
    /**
3835
     * Prepare, calculate result and save answer to the database by calling
3836
     * Event::saveQuestionAttempt() once everything is ready.
3837
     *
3838
     * @param int    $exeId
3839
     * @param int    $questionId
3840
     * @param mixed  $choice                                    the user-selected option
3841
     * @param string $from                                      function is called from 'exercise_show' or 'exercise_result'
3842
     * @param array  $exerciseResultCoordinates                 the hotspot coordinates $hotspot[$question_id] = coordinates
3843
     * @param bool   $save_results                              save results in the DB or just show the response
3844
     * @param bool   $from_database                             gets information from DB or from the current selection
3845
     * @param bool   $show_result                               show results or not
3846
     * @param int    $propagate_neg
3847
     * @param array  $hotspot_delineation_result
3848
     * @param bool   $showTotalScoreAndUserChoicesInLastAttempt
3849
     * @param bool   $updateResults
3850
     * @param bool   $showHotSpotDelineationTable
3851
     * @param int    $questionDuration                          seconds
3852
     *
3853
     * @return array|false
3854
     */
3855
    public function manage_answer(
3856
        $exeId,
3857
        $questionId,
3858
        $choice,
3859
        $from = 'exercise_show',
3860
        $exerciseResultCoordinates = [],
3861
        $save_results = true,
3862
        $from_database = false,
3863
        $show_result = true,
3864
        $propagate_neg = 0,
3865
        $hotspot_delineation_result = [],
3866
        $showTotalScoreAndUserChoicesInLastAttempt = true,
3867
        $updateResults = false,
3868
        $showHotSpotDelineationTable = false,
3869
        $questionDuration = 0
3870
    ) {
3871
        $debug = false;
3872
        //needed in order to use in the exercise_attempt() for the time
3873
        global $learnpath_id, $learnpath_item_id;
3874
        require_once api_get_path(LIBRARY_PATH).'geometry.lib.php';
3875
        $em = Database::getManager();
3876
        $feedback_type = $this->getFeedbackType();
3877
        $results_disabled = $this->selectResultsDisabled();
3878
        $questionDuration = (int) $questionDuration;
3879
3880
        if ($debug) {
3881
            error_log("<------ manage_answer ------> ");
3882
            error_log('exe_id: '.$exeId);
3883
            error_log('$from:  '.$from);
3884
            error_log('$save_results: '.intval($save_results));
3885
            error_log('$from_database: '.intval($from_database));
3886
            error_log('$show_result: '.intval($show_result));
3887
            error_log('$propagate_neg: '.$propagate_neg);
3888
            error_log('$exerciseResultCoordinates: '.print_r($exerciseResultCoordinates, 1));
3889
            error_log('$hotspot_delineation_result: '.print_r($hotspot_delineation_result, 1));
3890
            error_log('$learnpath_id: '.$learnpath_id);
3891
            error_log('$learnpath_item_id: '.$learnpath_item_id);
3892
            error_log('$choice: '.print_r($choice, 1));
3893
            error_log('-----------------------------');
3894
        }
3895
3896
        $final_overlap = 0;
3897
        $final_missing = 0;
3898
        $final_excess = 0;
3899
        $overlap_color = 0;
3900
        $missing_color = 0;
3901
        $excess_color = 0;
3902
        $threadhold1 = 0;
3903
        $threadhold2 = 0;
3904
        $threadhold3 = 0;
3905
        $arrques = null;
3906
        $arrans = null;
3907
        $studentChoice = null;
3908
        $expectedAnswer = '';
3909
        $calculatedChoice = '';
3910
        $calculatedStatus = '';
3911
        $questionId = (int) $questionId;
3912
        $exeId = (int) $exeId;
3913
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3914
        $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
3915
        $studentChoiceDegree = null;
3916
3917
        // Creates a temporary Question object
3918
        $course_id = $this->course_id;
3919
        $objQuestionTmp = Question::read($questionId, $this->course);
3920
3921
        if (false === $objQuestionTmp) {
3922
            return false;
3923
        }
3924
3925
        $questionName = $objQuestionTmp->selectTitle();
3926
        $questionWeighting = $objQuestionTmp->selectWeighting();
3927
        $answerType = $objQuestionTmp->selectType();
3928
        $quesId = $objQuestionTmp->selectId();
3929
        $extra = $objQuestionTmp->extra;
3930
        $next = 1; //not for now
3931
        $totalWeighting = 0;
3932
        $totalScore = 0;
3933
3934
        // Extra information of the question
3935
        if ((
3936
            $answerType == MULTIPLE_ANSWER_TRUE_FALSE ||
3937
            $answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY
3938
            )
3939
            && !empty($extra)
3940
        ) {
3941
            $extra = explode(':', $extra);
3942
            // Fixes problems with negatives values using intval
3943
            $true_score = (float) trim($extra[0]);
3944
            $false_score = (float) trim($extra[1]);
3945
            $doubt_score = (float) trim($extra[2]);
3946
        }
3947
3948
        // Construction of the Answer object
3949
        $objAnswerTmp = new Answer($questionId, $course_id);
3950
        $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
3951
3952
        if ($debug) {
3953
            error_log('Count of possible answers: '.$nbrAnswers);
3954
            error_log('$answerType: '.$answerType);
3955
        }
3956
3957
        if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
3958
            $choiceTmp = $choice;
3959
            $choice = $choiceTmp['choice'] ?? '';
3960
            $choiceDegreeCertainty = $choiceTmp['choiceDegreeCertainty'] ?? '';
3961
        }
3962
3963
        if ($answerType == FREE_ANSWER ||
3964
            $answerType == ORAL_EXPRESSION ||
3965
            $answerType == CALCULATED_ANSWER ||
3966
            $answerType == ANNOTATION ||
3967
            $answerType == UPLOAD_ANSWER ||
3968
            $answerType == ANSWER_IN_OFFICE_DOC
3969
        ) {
3970
            $nbrAnswers = 1;
3971
        }
3972
3973
        $generatedFile = '';
3974
        if ($answerType == ORAL_EXPRESSION) {
3975
            $exe_info = Event::get_exercise_results_by_attempt($exeId);
3976
            $exe_info = $exe_info[$exeId] ?? null;
3977
            $objQuestionTmp->initFile(
3978
                api_get_session_id(),
3979
                $exe_info['exe_user_id'] ?? api_get_user_id(),
3980
                $exe_info['exe_exo_id'] ?? $this->iid,
3981
                $exe_info['exe_id'] ?? $exeId
3982
            );
3983
3984
            // Probably this attempt came in an exercise all question by page
3985
            if ($feedback_type == 0) {
3986
                $objQuestionTmp->replaceWithRealExe($exeId);
3987
            }
3988
            $generatedFile = $objQuestionTmp->getFileUrl();
3989
        }
3990
3991
        $user_answer = '';
3992
        // Get answer list for matching.
3993
        $sql = "SELECT iid, answer
3994
                FROM $table_ans
3995
                WHERE question_id = $questionId";
3996
        $res_answer = Database::query($sql);
3997
3998
        $answerMatching = [];
3999
        while ($real_answer = Database::fetch_array($res_answer)) {
4000
            $answerMatching[$real_answer['iid']] = $real_answer['answer'];
4001
        }
4002
4003
        // Get first answer needed for global question, no matter the answer shuffle option;
4004
        $firstAnswer = [];
4005
        if ($answerType == MULTIPLE_ANSWER_COMBINATION ||
4006
            $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE
4007
        ) {
4008
            $sql = "SELECT *
4009
                    FROM $table_ans
4010
                    WHERE question_id = $questionId
4011
                    ORDER BY position
4012
                    LIMIT 1";
4013
            $result = Database::query($sql);
4014
            if (Database::num_rows($result)) {
4015
                $firstAnswer = Database::fetch_array($result);
4016
            }
4017
        }
4018
4019
        $real_answers = [];
4020
        $quiz_question_options = Question::readQuestionOption($questionId, $course_id);
4021
        $organs_at_risk_hit = 0;
4022
        $questionScore = 0;
4023
        $orderedHotSpots = [];
4024
        if (in_array($answerType, [HOT_SPOT_COMBINATION, HOT_SPOT, ANNOTATION])) {
4025
            $orderedHotSpots = $em->getRepository('ChamiloCoreBundle:TrackEHotspot')->findBy(
4026
                [
4027
                    'hotspotQuestionId' => $questionId,
4028
                    'cId' => $course_id,
4029
                    'hotspotExeId' => $exeId,
4030
                ],
4031
                ['hotspotAnswerId' => 'ASC']
4032
            );
4033
        }
4034
4035
        if (in_array($answerType, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION])) {
4036
            if (MULTIPLE_ANSWER_DROPDOWN_COMBINATION == $answerType) {
4037
                $questionScore = $questionWeighting;
4038
            }
4039
4040
            if ($from_database) {
4041
                $studentChoices = Database::store_result(
4042
                    Database::query(
4043
                        "SELECT answer FROM $TBL_TRACK_ATTEMPT WHERE exe_id = $exeId AND question_id = $questionId"
4044
                    ),
4045
                    'ASSOC'
4046
                );
4047
                $studentChoices = array_column($studentChoices, 'answer');
4048
            } else {
4049
                $studentChoices = array_values($choice);
4050
            }
4051
4052
            $correctChoices = array_filter(
4053
                $answerMatching,
4054
                function ($answerId) use ($objAnswerTmp) {
4055
                    $index = array_search($answerId, $objAnswerTmp->iid);
4056
4057
                    return true === (bool) $objAnswerTmp->correct[$index];
4058
                },
4059
                ARRAY_FILTER_USE_KEY
4060
            );
4061
4062
            $correctChoices = array_keys($correctChoices);
4063
4064
            if (MULTIPLE_ANSWER_DROPDOWN_COMBINATION == $answerType
4065
                && (array_diff($studentChoices, $correctChoices) || array_diff($correctChoices, $studentChoices))
4066
            ) {
4067
                $questionScore = 0;
4068
            }
4069
4070
            if ($show_result) {
4071
                echo ExerciseShowFunctions::displayMultipleAnswerDropdown(
4072
                    $this,
4073
                    $objAnswerTmp,
4074
                    $correctChoices,
4075
                    $studentChoices,
4076
                    $showTotalScoreAndUserChoicesInLastAttempt
4077
                );
4078
            }
4079
        }
4080
4081
        if ($debug) {
4082
            error_log('-- Start answer loop --');
4083
        }
4084
4085
        $answerDestination = null;
4086
        $userAnsweredQuestion = false;
4087
        $correctAnswerId = [];
4088
        $matchingCorrectAnswers = [];
4089
        for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
4090
            $answer = $objAnswerTmp->selectAnswer($answerId);
4091
            $hideComment = (int) $this->getPageConfigurationAttribute('hide_comment');
4092
            if (1 === $hideComment) {
4093
                $answerComment = null;
4094
            } else {
4095
                $answerComment = $objAnswerTmp->selectComment($answerId);
4096
            }
4097
            $answerCorrect = $objAnswerTmp->isCorrect($answerId);
4098
            $answerWeighting = (float) $objAnswerTmp->selectWeighting($answerId);
4099
            $answerAutoId = $objAnswerTmp->selectId($answerId);
4100
            $answerIid = isset($objAnswerTmp->iid[$answerId]) ? (int) $objAnswerTmp->iid[$answerId] : 0;
4101
4102
            if ($debug) {
4103
                error_log("c_quiz_answer.iid: $answerAutoId ");
4104
                error_log("Answer marked as correct in db (0/1)?: $answerCorrect ");
4105
                error_log("answerWeighting: $answerWeighting");
4106
            }
4107
4108
            // Delineation.
4109
            $delineation_cord = $objAnswerTmp->selectHotspotCoordinates(1);
4110
            $answer_delineation_destination = $objAnswerTmp->selectDestination(1);
4111
4112
            switch ($answerType) {
4113
                case UNIQUE_ANSWER:
4114
                case UNIQUE_ANSWER_IMAGE:
4115
                case UNIQUE_ANSWER_NO_OPTION:
4116
                case READING_COMPREHENSION:
4117
                    if ($from_database) {
4118
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4119
                                WHERE
4120
                                    exe_id = $exeId AND
4121
                                    question_id = $questionId";
4122
                        $result = Database::query($sql);
4123
                        $choice = Database::result($result, 0, 'answer');
4124
4125
                        if (false === $userAnsweredQuestion) {
4126
                            $userAnsweredQuestion = !empty($choice);
4127
                        }
4128
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
4129
                        if ($studentChoice) {
4130
                            $questionScore += $answerWeighting;
4131
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
4132
                            $correctAnswerId[] = $answerId;
4133
                        }
4134
                    } else {
4135
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
4136
                        if ($studentChoice) {
4137
                            $questionScore += $answerWeighting;
4138
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
4139
                            $correctAnswerId[] = $answerAutoId;
4140
                        }
4141
                    }
4142
                    break;
4143
                case MULTIPLE_ANSWER_TRUE_FALSE:
4144
                    if ($from_database) {
4145
                        $choice = [];
4146
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4147
                                WHERE
4148
                                    exe_id = $exeId AND
4149
                                    question_id = ".$questionId;
4150
4151
                        $result = Database::query($sql);
4152
                        while ($row = Database::fetch_array($result)) {
4153
                            $values = explode(':', $row['answer']);
4154
                            $my_answer_id = isset($values[0]) ? $values[0] : '';
4155
                            $option = isset($values[1]) ? $values[1] : '';
4156
                            $choice[$my_answer_id] = $option;
4157
                        }
4158
                        $userAnsweredQuestion = !empty($choice);
4159
                    }
4160
4161
                    $studentChoice = $choice[$answerAutoId] ?? null;
4162
                    if (isset($studentChoice)) {
4163
                        $correctAnswerId[] = $answerAutoId;
4164
                        if ($studentChoice == $answerCorrect) {
4165
                            $questionScore += $true_score;
4166
                        } else {
4167
                            if ($quiz_question_options[$studentChoice]['name'] === "Don't know" ||
4168
                                $quiz_question_options[$studentChoice]['name'] === "DoubtScore"
4169
                            ) {
4170
                                $questionScore += $doubt_score;
4171
                            } else {
4172
                                $questionScore += $false_score;
4173
                            }
4174
                        }
4175
                    } else {
4176
                        // If no result then the user just hit don't know
4177
                        $studentChoice = 3;
4178
                        $questionScore += $doubt_score;
4179
                    }
4180
                    $totalScore = $questionScore;
4181
                    break;
4182
                case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
4183
                    if ($from_database) {
4184
                        $choice = [];
4185
                        $choiceDegreeCertainty = [];
4186
                        $sql = "SELECT answer
4187
                            FROM $TBL_TRACK_ATTEMPT
4188
                            WHERE
4189
                            exe_id = $exeId AND question_id = $questionId";
4190
4191
                        $result = Database::query($sql);
4192
                        while ($row = Database::fetch_array($result)) {
4193
                            $ind = $row['answer'];
4194
                            $values = explode(':', $ind);
4195
                            $myAnswerId = $values[0];
4196
                            $option = $values[1];
4197
                            $percent = $values[2];
4198
                            $choice[$myAnswerId] = $option;
4199
                            $choiceDegreeCertainty[$myAnswerId] = $percent;
4200
                        }
4201
                    }
4202
4203
                    $studentChoice = $choice[$answerAutoId] ?? null;
4204
                    $studentChoiceDegree = $choiceDegreeCertainty[$answerAutoId] ?? null;
4205
4206
                    // student score update
4207
                    if (!empty($studentChoice)) {
4208
                        if ($studentChoice == $answerCorrect) {
4209
                            // correct answer and student is Unsure or PrettySur
4210
                            if (isset($quiz_question_options[$studentChoiceDegree]) &&
4211
                                $quiz_question_options[$studentChoiceDegree]['position'] >= 3 &&
4212
                                $quiz_question_options[$studentChoiceDegree]['position'] < 9
4213
                            ) {
4214
                                $questionScore += $true_score;
4215
                            } else {
4216
                                // student ignore correct answer
4217
                                $questionScore += $doubt_score;
4218
                            }
4219
                        } else {
4220
                            // false answer and student is Unsure or PrettySur
4221
                            if ($quiz_question_options[$studentChoiceDegree]['position'] >= 3
4222
                                && $quiz_question_options[$studentChoiceDegree]['position'] < 9) {
4223
                                $questionScore += $false_score;
4224
                            } else {
4225
                                // student ignore correct answer
4226
                                $questionScore += $doubt_score;
4227
                            }
4228
                        }
4229
                    }
4230
                    $totalScore = $questionScore;
4231
                    break;
4232
                case MULTIPLE_ANSWER:
4233
                case MULTIPLE_ANSWER_DROPDOWN:
4234
                    if ($from_database) {
4235
                        $choice = [];
4236
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4237
                                WHERE exe_id = $exeId AND question_id = $questionId ";
4238
                        $resultans = Database::query($sql);
4239
                        while ($row = Database::fetch_array($resultans)) {
4240
                            $choice[$row['answer']] = 1;
4241
                        }
4242
4243
                        $studentChoice = $choice[$answerAutoId] ?? null;
4244
                        $real_answers[$answerId] = (bool) $studentChoice;
4245
4246
                        if ($studentChoice) {
4247
                            $questionScore += $answerWeighting;
4248
                        }
4249
                    } else {
4250
                        $studentChoice = $choice[$answerAutoId] ?? null;
4251
                        $real_answers[$answerId] = (bool) $studentChoice;
4252
4253
                        if (isset($studentChoice)
4254
                            || (MULTIPLE_ANSWER_DROPDOWN == $answerType && in_array($answerAutoId, $choice))
4255
                        ) {
4256
                            $correctAnswerId[] = $answerAutoId;
4257
                            $questionScore += $answerWeighting;
4258
                        }
4259
                    }
4260
                    $totalScore += $answerWeighting;
4261
                    break;
4262
                case GLOBAL_MULTIPLE_ANSWER:
4263
                    if ($from_database) {
4264
                        $choice = [];
4265
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4266
                                WHERE exe_id = $exeId AND question_id = $questionId ";
4267
                        $resultans = Database::query($sql);
4268
                        while ($row = Database::fetch_array($resultans)) {
4269
                            $choice[$row['answer']] = 1;
4270
                        }
4271
                        $studentChoice = $choice[$answerAutoId] ?? null;
4272
                        $real_answers[$answerId] = (bool) $studentChoice;
4273
                        if ($studentChoice) {
4274
                            $questionScore += $answerWeighting;
4275
                        }
4276
                    } else {
4277
                        $studentChoice = $choice[$answerAutoId] ?? null;
4278
                        if (isset($studentChoice)) {
4279
                            $questionScore += $answerWeighting;
4280
                        }
4281
                        $real_answers[$answerId] = (bool) $studentChoice;
4282
                    }
4283
                    $totalScore += $answerWeighting;
4284
                    if ($debug) {
4285
                        error_log("studentChoice: $studentChoice");
4286
                    }
4287
                    break;
4288
                case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
4289
                    if ($from_database) {
4290
                        $choice = [];
4291
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4292
                                WHERE exe_id = $exeId AND question_id = $questionId";
4293
                        $resultans = Database::query($sql);
4294
                        while ($row = Database::fetch_array($resultans)) {
4295
                            $result = explode(':', $row['answer']);
4296
                            if (isset($result[0])) {
4297
                                $my_answer_id = isset($result[0]) ? $result[0] : '';
4298
                                $option = isset($result[1]) ? $result[1] : '';
4299
                                $choice[$my_answer_id] = $option;
4300
                            }
4301
                        }
4302
                        $studentChoice = $choice[$answerAutoId] ?? '';
4303
                        $real_answers[$answerId] = false;
4304
                        if ($answerCorrect == $studentChoice) {
4305
                            $real_answers[$answerId] = true;
4306
                        }
4307
                    } else {
4308
                        $studentChoice = $choice[$answerAutoId] ?? '';
4309
                        $real_answers[$answerId] = false;
4310
                        if ($answerCorrect == $studentChoice) {
4311
                            $real_answers[$answerId] = true;
4312
                        }
4313
                    }
4314
                    break;
4315
                case MULTIPLE_ANSWER_COMBINATION:
4316
                    if ($from_database) {
4317
                        $choice = [];
4318
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4319
                                WHERE exe_id = $exeId AND question_id = $questionId";
4320
                        $resultans = Database::query($sql);
4321
                        while ($row = Database::fetch_array($resultans)) {
4322
                            $choice[$row['answer']] = 1;
4323
                        }
4324
4325
                        $studentChoice = $choice[$answerAutoId] ?? null;
4326
                        if (1 == $answerCorrect) {
4327
                            $real_answers[$answerId] = false;
4328
                            if ($studentChoice) {
4329
                                $real_answers[$answerId] = true;
4330
                            }
4331
                        } else {
4332
                            $real_answers[$answerId] = true;
4333
                            if ($studentChoice) {
4334
                                $real_answers[$answerId] = false;
4335
                            }
4336
                        }
4337
                    } else {
4338
                        $studentChoice = $choice[$answerAutoId] ?? null;
4339
                        if (1 == $answerCorrect) {
4340
                            $real_answers[$answerId] = false;
4341
                            if ($studentChoice) {
4342
                                $real_answers[$answerId] = true;
4343
                            }
4344
                        } else {
4345
                            $real_answers[$answerId] = true;
4346
                            if ($studentChoice) {
4347
                                $real_answers[$answerId] = false;
4348
                            }
4349
                        }
4350
                    }
4351
                    break;
4352
                case FILL_IN_BLANKS:
4353
                case FILL_IN_BLANKS_COMBINATION:
4354
                    $str = '';
4355
                    $answerFromDatabase = '';
4356
                    if ($from_database) {
4357
                        $sql = "SELECT answer
4358
                                FROM $TBL_TRACK_ATTEMPT
4359
                                WHERE
4360
                                    exe_id = $exeId AND
4361
                                    question_id= $questionId ";
4362
                        $result = Database::query($sql);
4363
                        $str = $answerFromDatabase = Database::result($result, 0, 'answer');
4364
                    }
4365
4366
                    // if ($save_results == false && strpos($answerFromDatabase, 'font color') !== false) {
4367
                    if (false) {
4368
                        // the question is encoded like this
4369
                        // [A] B [C] D [E] F::10,10,10@1
4370
                        // number 1 before the "@" means that is a switchable fill in blank question
4371
                        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4372
                        // means that is a normal fill blank question
4373
                        // first we explode the "::"
4374
                        $pre_array = explode('::', $answer);
4375
4376
                        // is switchable fill blank or not
4377
                        $last = count($pre_array) - 1;
4378
                        $is_set_switchable = explode('@', $pre_array[$last]);
4379
                        $switchable_answer_set = false;
4380
                        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
4381
                            $switchable_answer_set = true;
4382
                        }
4383
                        $answer = '';
4384
                        for ($k = 0; $k < $last; $k++) {
4385
                            $answer .= $pre_array[$k];
4386
                        }
4387
                        // splits weightings that are joined with a comma
4388
                        $answerWeighting = explode(',', $is_set_switchable[0]);
4389
                        // we save the answer because it will be modified
4390
                        $temp = $answer;
4391
                        $answer = '';
4392
                        $j = 0;
4393
                        //initialise answer tags
4394
                        $user_tags = $correct_tags = $real_text = [];
4395
                        // the loop will stop at the end of the text
4396
                        while (1) {
4397
                            // quits the loop if there are no more blanks (detect '[')
4398
                            if ($temp == false || ($pos = api_strpos($temp, '[')) === false) {
4399
                                // adds the end of the text
4400
                                $answer = $temp;
4401
                                $real_text[] = $answer;
4402
                                break; //no more "blanks", quit the loop
4403
                            }
4404
                            // adds the piece of text that is before the blank
4405
                            //and ends with '[' into a general storage array
4406
                            $real_text[] = api_substr($temp, 0, $pos + 1);
4407
                            $answer .= api_substr($temp, 0, $pos + 1);
4408
                            //take the string remaining (after the last "[" we found)
4409
                            $temp = api_substr($temp, $pos + 1);
4410
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4411
                            if (($pos = api_strpos($temp, ']')) === false) {
4412
                                // adds the end of the text
4413
                                $answer .= $temp;
4414
                                break;
4415
                            }
4416
                            if ($from_database) {
4417
                                $str = $answerFromDatabase;
4418
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4419
                                $str = str_replace('\r\n', '', $str);
4420
4421
                                $choice = $arr[1];
4422
                                if (isset($choice[$j])) {
4423
                                    $tmp = api_strrpos($choice[$j], ' / ');
4424
                                    $choice[$j] = api_substr($choice[$j], 0, $tmp);
4425
                                    $choice[$j] = trim($choice[$j]);
4426
                                    // Needed to let characters ' and " to work as part of an answer
4427
                                    $choice[$j] = stripslashes($choice[$j]);
4428
                                } else {
4429
                                    $choice[$j] = null;
4430
                                }
4431
                            } else {
4432
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4433
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4434
                            }
4435
4436
                            $user_tags[] = $choice[$j];
4437
                            // Put the contents of the [] answer tag into correct_tags[]
4438
                            $correct_tags[] = api_substr($temp, 0, $pos);
4439
                            $j++;
4440
                            $temp = api_substr($temp, $pos + 1);
4441
                        }
4442
                        $answer = '';
4443
                        $real_correct_tags = $correct_tags;
4444
                        $chosen_list = [];
4445
4446
                        for ($i = 0; $i < count($real_correct_tags); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4447
                            if (0 == $i) {
4448
                                $answer .= $real_text[0];
4449
                            }
4450
                            if (!$switchable_answer_set) {
4451
                                // Needed to parse ' and " characters
4452
                                $user_tags[$i] = stripslashes($user_tags[$i]);
4453
                                if ($correct_tags[$i] == $user_tags[$i]) {
4454
                                    // gives the related weighting to the student
4455
                                    $questionScore += $answerWeighting[$i];
4456
                                    // increments total score
4457
                                    $totalScore += $answerWeighting[$i];
4458
                                    // adds the word in green at the end of the string
4459
                                    $answer .= $correct_tags[$i];
4460
                                } elseif (!empty($user_tags[$i])) {
4461
                                    // else if the word entered by the student IS NOT the same as
4462
                                    // the one defined by the professor
4463
                                    // adds the word in red at the end of the string, and strikes it
4464
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4465
                                } else {
4466
                                    // adds a tabulation if no word has been typed by the student
4467
                                    $answer .= ''; // remove &nbsp; that causes issue
4468
                                }
4469
                            } else {
4470
                                // switchable fill in the blanks
4471
                                if (in_array($user_tags[$i], $correct_tags)) {
4472
                                    $chosen_list[] = $user_tags[$i];
4473
                                    $correct_tags = array_diff($correct_tags, $chosen_list);
4474
                                    // gives the related weighting to the student
4475
                                    $questionScore += $answerWeighting[$i];
4476
                                    // increments total score
4477
                                    $totalScore += $answerWeighting[$i];
4478
                                    // adds the word in green at the end of the string
4479
                                    $answer .= $user_tags[$i];
4480
                                } elseif (!empty($user_tags[$i])) {
4481
                                    // else if the word entered by the student IS NOT the same
4482
                                    // as the one defined by the professor
4483
                                    // adds the word in red at the end of the string, and strikes it
4484
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4485
                                } else {
4486
                                    // adds a tabulation if no word has been typed by the student
4487
                                    $answer .= ''; // remove &nbsp; that causes issue
4488
                                }
4489
                            }
4490
4491
                            // adds the correct word, followed by ] to close the blank
4492
                            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4493
                            if (isset($real_text[$i + 1])) {
4494
                                $answer .= $real_text[$i + 1];
4495
                            }
4496
                        }
4497
                    } else {
4498
                        // insert the student result in the track_e_attempt table, field answer
4499
                        // $answer is the answer like in the c_quiz_answer table for the question
4500
                        // student data are choice[]
4501
                        $listCorrectAnswers = FillBlanks::getAnswerInfo($answer);
4502
                        $switchableAnswerSet = $listCorrectAnswers['switchable'];
4503
                        $answerWeighting = $listCorrectAnswers['weighting'];
4504
                        // user choices is an array $choice
4505
4506
                        // get existing user data in n the BDD
4507
                        if ($from_database) {
4508
                            $listStudentResults = FillBlanks::getAnswerInfo(
4509
                                $answerFromDatabase,
4510
                                true
4511
                            );
4512
                            $choice = $listStudentResults['student_answer'];
4513
                        }
4514
4515
                        // loop other all blanks words
4516
                        if (!$switchableAnswerSet) {
4517
                            // not switchable answer, must be in the same place than teacher order
4518
                            for ($i = 0; $i < count($listCorrectAnswers['words']); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4519
                                $studentAnswer = $choice[$i] ?? '';
4520
                                $correctAnswer = $listCorrectAnswers['words'][$i];
4521
4522
                                if ($debug) {
4523
                                    error_log("Student answer: $i");
4524
                                    error_log($studentAnswer);
4525
                                }
4526
4527
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4528
                                // Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
4529
                                // ENT_QUOTES is used in order to transform ' to &#039;
4530
                                //if (!$from_database) {
4531
                                $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4532
                                if ($debug) {
4533
                                    error_log('Student answer cleaned:');
4534
                                    error_log($studentAnswer);
4535
                                }
4536
                                //}
4537
4538
                                $isAnswerCorrect = 0;
4539
                                if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
4540
                                    // gives the related weighting to the student
4541
                                    $questionScore += $answerWeighting[$i];
4542
                                    // increments total score
4543
                                    $totalScore += $answerWeighting[$i];
4544
                                    $isAnswerCorrect = 1;
4545
                                }
4546
                                if ($debug) {
4547
                                    error_log("isAnswerCorrect $i: $isAnswerCorrect");
4548
                                }
4549
4550
                                $studentAnswerToShow = $studentAnswer;
4551
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4552
                                if ($debug) {
4553
                                    error_log("Fill in blank type: $type");
4554
                                }
4555
                                if ($type == FillBlanks::FILL_THE_BLANK_MENU) {
4556
                                    $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4557
                                    if ($studentAnswer != '') {
4558
                                        foreach ($listMenu as $item) {
4559
                                            if (sha1($item) == $studentAnswer) {
4560
                                                $studentAnswerToShow = $item;
4561
                                            }
4562
                                        }
4563
                                    }
4564
                                }
4565
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4566
                                $listCorrectAnswers['student_score'][$i] = $isAnswerCorrect;
4567
                            }
4568
                        } else {
4569
                            // switchable answer
4570
                            $listStudentAnswerTemp = $choice;
4571
                            $listTeacherAnswerTemp = $listCorrectAnswers['words'];
4572
4573
                            // for every teacher answer, check if there is a student answer
4574
                            for ($i = 0; $i < count($listStudentAnswerTemp); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4575
                                $studentAnswer = trim($listStudentAnswerTemp[$i]);
4576
                                $studentAnswerToShow = $studentAnswer;
4577
4578
                                if (empty($studentAnswer)) {
4579
                                    break;
4580
                                }
4581
4582
                                if ($debug) {
4583
                                    error_log("Student answer: $i");
4584
                                    error_log($studentAnswer);
4585
                                }
4586
4587
                                if (!$from_database) {
4588
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4589
                                    if ($debug) {
4590
                                        error_log("Student answer cleaned:");
4591
                                        error_log($studentAnswer);
4592
                                    }
4593
                                }
4594
4595
                                $found = false;
4596
                                for ($j = 0; $j < count($listTeacherAnswerTemp); $j++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4597
                                    $correctAnswer = $listTeacherAnswerTemp[$j] ?? '';
4598
                                    if (is_array($listTeacherAnswerTemp)) {
4599
                                        $correctAnswer = implode('||', $listTeacherAnswerTemp);
4600
                                    }
4601
4602
                                    if (empty($correctAnswer)) {
4603
                                        break;
4604
                                    }
4605
4606
                                    if (FillBlanks::isStudentAnswerGood(
4607
                                        $studentAnswer,
4608
                                        $correctAnswer,
4609
                                        $from_database,
4610
                                        true
4611
                                    )) {
4612
                                        $questionScore += $answerWeighting[$i];
4613
                                        $totalScore += $answerWeighting[$i];
4614
                                        if (is_array($listTeacherAnswerTemp)) {
4615
                                            $searchAnswer = array_search(api_htmlentities($studentAnswer), $listTeacherAnswerTemp);
4616
                                            if (false !== $searchAnswer) {
4617
                                                // Remove from array
4618
                                                unset($listTeacherAnswerTemp[$searchAnswer]);
4619
                                            }
4620
                                        } else {
4621
                                            $listTeacherAnswerTemp[$j] = null;
4622
                                        }
4623
                                        $found = true;
4624
                                    }
4625
4626
                                    if (FillBlanks::FILL_THE_BLANK_MENU != $listCorrectAnswers['words_types'][$j]) {
4627
                                        break;
4628
                                    }
4629
4630
                                    $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4631
                                    foreach ($listMenu as $item) {
4632
                                        if (sha1($item) === $studentAnswer) {
4633
                                            $studentAnswerToShow = $item;
4634
                                        }
4635
                                    }
4636
                                }
4637
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4638
                                $listCorrectAnswers['student_score'][$i] = $found ? 1 : 0;
4639
                            }
4640
                        }
4641
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
4642
                    }
4643
                    break;
4644
                case CALCULATED_ANSWER:
4645
                    $calculatedAnswerList = Session::read('calculatedAnswerId');
4646
                    $calculatedAnswerId = null;
4647
                    if (!empty($calculatedAnswerList) && isset($calculatedAnswerList[$questionId])) {
4648
                        $calculatedAnswerId = $calculatedAnswerList[$questionId];
4649
                        $answer = $objAnswerTmp->selectAnswer($calculatedAnswerId);
4650
                        $preArray = explode('@@', $answer);
4651
                        $last = count($preArray) - 1;
4652
                        $answer = '';
4653
                        for ($k = 0; $k < $last; $k++) {
4654
                            $answer .= $preArray[$k];
4655
                        }
4656
                        $answerWeighting = [$answerWeighting];
4657
                        // we save the answer because it will be modified
4658
                        $temp = $answer;
4659
                        $answer = '';
4660
                        $j = 0;
4661
                        // initialise answer tags
4662
                        $userTags = $correctTags = $realText = [];
4663
                        // the loop will stop at the end of the text
4664
                        while (1) {
4665
                            // quits the loop if there are no more blanks (detect '[')
4666
                            if ($temp == false || ($pos = api_strpos($temp, '[')) === false) {
4667
                                // adds the end of the text
4668
                                $answer = $temp;
4669
                                $realText[] = $answer;
4670
                                break; //no more "blanks", quit the loop
4671
                            }
4672
                            // adds the piece of text that is before the blank
4673
                            // and ends with '[' into a general storage array
4674
                            $realText[] = api_substr($temp, 0, $pos + 1);
4675
                            $answer .= api_substr($temp, 0, $pos + 1);
4676
                            // take the string remaining (after the last "[" we found)
4677
                            $temp = api_substr($temp, $pos + 1);
4678
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4679
                            if (false === ($pos = api_strpos($temp, ']'))) {
4680
                                // adds the end of the text
4681
                                $answer .= $temp;
4682
                                break;
4683
                            }
4684
4685
                            if ($from_database) {
4686
                                $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4687
                                        WHERE
4688
                                            exe_id = $exeId AND
4689
                                            question_id = $questionId ";
4690
                                $result = Database::query($sql);
4691
                                $str = Database::result($result, 0, 'answer');
4692
4693
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4694
                                $str = str_replace('\r\n', '', $str);
4695
                                $choice = $arr[1];
4696
                                if (isset($choice[$j])) {
4697
                                    $tmp = api_strrpos($choice[$j], ' / ');
4698
                                    if ($tmp) {
4699
                                        $choice[$j] = api_substr($choice[$j], 0, $tmp);
4700
                                    } else {
4701
                                        $tmp = ltrim($tmp, '[');
4702
                                        $tmp = rtrim($tmp, ']');
4703
                                    }
4704
                                    $choice[$j] = trim($choice[$j]);
4705
                                    // Needed to let characters ' and " to work as part of an answer
4706
                                    $choice[$j] = stripslashes($choice[$j]);
4707
                                } else {
4708
                                    $choice[$j] = null;
4709
                                }
4710
                            } else {
4711
                                // This value is the user input not escaped while correct answer is escaped by ckeditor
4712
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4713
                            }
4714
                            $userTags[] = $choice[$j];
4715
                            // put the contents of the [] answer tag into correct_tags[]
4716
                            $correctTags[] = api_substr($temp, 0, $pos);
4717
                            $j++;
4718
                            $temp = api_substr($temp, $pos + 1);
4719
                        }
4720
                        $answer = '';
4721
                        $realCorrectTags = $correctTags;
4722
                        $calculatedStatus = Display::label(get_lang('Incorrect'), 'danger');
4723
                        $expectedAnswer = '';
4724
                        $calculatedChoice = '';
4725
4726
                        for ($i = 0; $i < count($realCorrectTags); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4727
                            if (0 == $i) {
4728
                                $answer .= $realText[0];
4729
                            }
4730
                            // Needed to parse ' and " characters
4731
                            $userTags[$i] = stripslashes($userTags[$i]);
4732
                            if ($correctTags[$i] == $userTags[$i]) {
4733
                                // gives the related weighting to the student
4734
                                $questionScore += $answerWeighting[$i];
4735
                                // increments total score
4736
                                $totalScore += $answerWeighting[$i];
4737
                                // adds the word in green at the end of the string
4738
                                $answer .= $correctTags[$i];
4739
                                $calculatedChoice = $correctTags[$i];
4740
                            } elseif (!empty($userTags[$i])) {
4741
                                // else if the word entered by the student IS NOT the same as
4742
                                // the one defined by the professor
4743
                                // adds the word in red at the end of the string, and strikes it
4744
                                $answer .= '<font color="red"><s>'.$userTags[$i].'</s></font>';
4745
                                $calculatedChoice = $userTags[$i];
4746
                            } else {
4747
                                // adds a tabulation if no word has been typed by the student
4748
                                $answer .= ''; // remove &nbsp; that causes issue
4749
                            }
4750
                            // adds the correct word, followed by ] to close the blank
4751
                            if (EXERCISE_FEEDBACK_TYPE_EXAM != $this->results_disabled) {
4752
                                $answer .= ' / <font color="green"><b>'.$realCorrectTags[$i].'</b></font>';
4753
                                $calculatedStatus = Display::label(get_lang('Correct'), 'success');
4754
                                $expectedAnswer = $realCorrectTags[$i];
4755
                            }
4756
                            $answer .= ']';
4757
                            if (isset($realText[$i + 1])) {
4758
                                $answer .= $realText[$i + 1];
4759
                            }
4760
                        }
4761
                    } else {
4762
                        if ($from_database) {
4763
                            $sql = "SELECT *
4764
                                    FROM $TBL_TRACK_ATTEMPT
4765
                                    WHERE
4766
                                        exe_id = $exeId AND
4767
                                        question_id = $questionId ";
4768
                            $result = Database::query($sql);
4769
                            $resultData = Database::fetch_array($result, 'ASSOC');
4770
                            $answer = $resultData['answer'];
4771
                            $questionScore = $resultData['marks'];
4772
                        }
4773
                    }
4774
                    break;
4775
                case UPLOAD_ANSWER:
4776
                case FREE_ANSWER:
4777
                case ANSWER_IN_OFFICE_DOC:
4778
                    if ($from_database) {
4779
                        $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
4780
                                 WHERE
4781
                                    exe_id = $exeId AND
4782
                                    question_id= ".$questionId;
4783
                        $result = Database::query($sql);
4784
                        $data = Database::fetch_array($result);
4785
                        $choice = '';
4786
                        $questionScore = 0;
4787
                        if ($data) {
4788
                            $choice = $data['answer'];
4789
                            $questionScore = $data['marks'];
4790
                        }
4791
                        $choice = str_replace('\r\n', '', $choice);
4792
                        $choice = stripslashes($choice);
4793
                        if (-1 == $questionScore) {
4794
                            $totalScore += 0;
4795
                        } else {
4796
                            $totalScore += $questionScore;
4797
                        }
4798
                        if ('' == $questionScore) {
4799
                            $questionScore = 0;
4800
                        }
4801
                        $arrques = $questionName;
4802
                        $arrans = $choice;
4803
                    } else {
4804
                        $studentChoice = $choice;
4805
                        if ($studentChoice) {
4806
                            //Fixing negative puntation see #2193
4807
                            $questionScore = 0;
4808
                            $totalScore += 0;
4809
                        }
4810
                    }
4811
                    break;
4812
                case ORAL_EXPRESSION:
4813
                    if ($from_database) {
4814
                        $query = "SELECT answer, marks
4815
                                  FROM $TBL_TRACK_ATTEMPT
4816
                                  WHERE
4817
                                        exe_id = $exeId AND
4818
                                        question_id = $questionId
4819
                                 ";
4820
                        $resq = Database::query($query);
4821
                        $row = Database::fetch_assoc($resq);
4822
4823
                        $choice = [
4824
                            'answer' => '',
4825
                            'marks' => 0,
4826
                        ];
4827
                        $questionScore = 0;
4828
4829
                        if (is_array($row)) {
4830
                            $choice = $row['answer'];
4831
                            $choice = str_replace('\r\n', '', $choice);
4832
                            $choice = stripslashes($choice);
4833
                            $questionScore = $row['marks'];
4834
                        }
4835
4836
                        if ($questionScore == -1) {
4837
                            $totalScore += 0;
4838
                        } else {
4839
                            $totalScore += $questionScore;
4840
                        }
4841
                        $arrques = $questionName;
4842
                        $arrans = $choice;
4843
                    } else {
4844
                        $studentChoice = $choice;
4845
                        if ($studentChoice) {
4846
                            //Fixing negative puntation see #2193
4847
                            $questionScore = 0;
4848
                            $totalScore += 0;
4849
                        }
4850
                    }
4851
                    break;
4852
                case DRAGGABLE:
4853
                case MATCHING_DRAGGABLE:
4854
                case MATCHING_DRAGGABLE_COMBINATION:
4855
                case MATCHING_COMBINATION:
4856
                case MATCHING:
4857
                    if ($from_database) {
4858
                        $sql = "SELECT iid, answer, id_auto
4859
                                FROM $table_ans
4860
                                WHERE
4861
                                    question_id = $questionId AND
4862
                                    correct = 0
4863
                                ";
4864
                        $result = Database::query($sql);
4865
                        // Getting the real answer
4866
                        $real_list = [];
4867
                        while ($realAnswer = Database::fetch_array($result)) {
4868
                            $real_list[$realAnswer['iid']] = $realAnswer['answer'];
4869
                        }
4870
4871
                        $orderBy = ' ORDER BY iid ';
4872
                        if (DRAGGABLE == $answerType) {
4873
                            $orderBy = ' ORDER BY correct ';
4874
                        }
4875
4876
                        $sql = "SELECT iid, answer, correct, id_auto, ponderation
4877
                                FROM $table_ans
4878
                                WHERE
4879
                                    question_id = $questionId AND
4880
                                    correct <> 0
4881
                                $orderBy";
4882
                        $result = Database::query($sql);
4883
                        $options = [];
4884
                        $correctAnswers = [];
4885
                        while ($row = Database::fetch_array($result, 'ASSOC')) {
4886
                            $options[] = $row;
4887
                            $correctAnswers[$row['correct']] = $row['answer'];
4888
                        }
4889
4890
                        $questionScore = 0;
4891
                        $counterAnswer = 1;
4892
                        foreach ($options as $a_answers) {
4893
                            $i_answer_id = $a_answers['iid']; //3
4894
                            $s_answer_label = $a_answers['answer']; // your daddy - your mother
4895
                            $i_answer_correct_answer = $a_answers['correct']; //1 - 2
4896
4897
                            $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4898
                                    WHERE
4899
                                        exe_id = '$exeId' AND
4900
                                        question_id = '$questionId' AND
4901
                                        position = '$i_answer_id'";
4902
                            $result = Database::query($sql);
4903
                            $s_user_answer = 0;
4904
                            if (Database::num_rows($result) > 0) {
4905
                                //  rich - good looking
4906
                                $s_user_answer = Database::result($result, 0, 0);
4907
                            }
4908
                            $i_answerWeighting = $a_answers['ponderation'];
4909
                            $user_answer = '';
4910
                            $status = Display::label(get_lang('Incorrect'), 'danger');
4911
4912
                            if (!empty($s_user_answer)) {
4913
                                if (DRAGGABLE == $answerType) {
4914
                                    $sql = "SELECT answer FROM $table_ans WHERE iid = $s_user_answer";
4915
                                    $rsDragAnswer = Database::query($sql);
4916
                                    $dragAns = Database::result($rsDragAnswer, 0, 0);
4917
                                    if ($s_user_answer == $i_answer_correct_answer) {
4918
                                        $questionScore += $i_answerWeighting;
4919
                                        $totalScore += $i_answerWeighting;
4920
                                        $user_answer = Display::label(get_lang('Correct'), 'success');
4921
                                        if ($this->showExpectedChoice() && !empty($i_answer_id)) {
4922
                                            $user_answer = $answerMatching[$i_answer_id];
4923
                                        } else {
4924
                                            $user_answer = $dragAns;
4925
                                        }
4926
                                        $status = Display::label(get_lang('Correct'), 'success');
4927
                                    } else {
4928
                                        $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4929
                                        if ($this->showExpectedChoice() && !empty($s_user_answer)) {
4930
                                            /*$data = $options[$real_list[$s_user_answer] - 1];
4931
                                            $user_answer = $data['answer'];*/
4932
                                            $user_answer = $correctAnswers[$s_user_answer] ?? '';
4933
                                        } else {
4934
                                            $user_answer = $dragAns;
4935
                                        }
4936
                                    }
4937
                                } else {
4938
                                    if ($s_user_answer == $i_answer_correct_answer) {
4939
                                        $questionScore += $i_answerWeighting;
4940
                                        $totalScore += $i_answerWeighting;
4941
                                        $status = Display::label(get_lang('Correct'), 'success');
4942
                                        // Try with id
4943
                                        if (isset($real_list[$i_answer_id])) {
4944
                                            $user_answer = Display::span(
4945
                                                $real_list[$i_answer_id],
4946
                                                ['style' => 'color: #008000; font-weight: bold;']
4947
                                            );
4948
                                        }
4949
4950
                                        // Try with $i_answer_id_auto
4951
                                        if (empty($user_answer)) {
4952
                                            if (isset($real_list[$i_answer_id])) {
4953
                                                $user_answer = Display::span(
4954
                                                    $real_list[$i_answer_id],
4955
                                                    ['style' => 'color: #008000; font-weight: bold;']
4956
                                                );
4957
                                            }
4958
                                        }
4959
4960
                                        if (isset($real_list[$i_answer_correct_answer])) {
4961
                                            $matchingCorrectAnswers[$questionId]['from_database']['correct'][$i_answer_correct_answer] = $real_list[$i_answer_correct_answer];
4962
                                            $user_answer = Display::span(
4963
                                                $real_list[$i_answer_correct_answer],
4964
                                                ['style' => 'color: #008000; font-weight: bold;']
4965
                                            );
4966
                                        }
4967
                                    } else {
4968
                                        $user_answer = Display::span(
4969
                                            $real_list[$s_user_answer],
4970
                                            ['style' => 'color: #FF0000; text-decoration: line-through;']
4971
                                        );
4972
                                        if ($this->showExpectedChoice()) {
4973
                                            if (isset($real_list[$s_user_answer])) {
4974
                                                $user_answer = Display::span($real_list[$s_user_answer]);
4975
                                            }
4976
                                        }
4977
                                    }
4978
                                }
4979
                            } elseif (DRAGGABLE == $answerType) {
4980
                                $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4981
                                if ($this->showExpectedChoice()) {
4982
                                    $user_answer = '';
4983
                                }
4984
                            } else {
4985
                                $user_answer = Display::span(
4986
                                    get_lang('Incorrect').' &nbsp;',
4987
                                    ['style' => 'color: #FF0000; text-decoration: line-through;']
4988
                                );
4989
                                if ($this->showExpectedChoice()) {
4990
                                    $user_answer = '';
4991
                                }
4992
                            }
4993
4994
                            if ($show_result) {
4995
                                if (false === $this->showExpectedChoice() &&
4996
                                    false === $showTotalScoreAndUserChoicesInLastAttempt
4997
                                ) {
4998
                                    $this->hideExpectedAnswer = true;
4999
                                }
5000
                                switch ($answerType) {
5001
                                    case MATCHING:
5002
                                    case MATCHING_COMBINATION:
5003
                                    case MATCHING_DRAGGABLE_COMBINATION:
5004
                                    case MATCHING_DRAGGABLE:
5005
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
5006
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
5007
                                                break;
5008
                                            }
5009
                                        }
5010
5011
                                        echo '<tr>';
5012
                                        if (!in_array(
5013
                                            $this->results_disabled,
5014
                                            [
5015
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
5016
                                            ]
5017
                                        )
5018
                                        ) {
5019
                                            echo '<td>'.$s_answer_label.'</td>';
5020
                                            echo '<td>'.$user_answer.'</td>';
5021
                                        } else {
5022
                                            echo '<td>'.$s_answer_label.'</td>';
5023
                                            $status = Display::label(get_lang('Correct'), 'success');
5024
                                        }
5025
5026
                                        if ($this->showExpectedChoice()) {
5027
                                            if ($this->showExpectedChoiceColumn()) {
5028
                                                echo '<td>';
5029
                                                if (in_array($answerType, [MATCHING, MATCHING_COMBINATION, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION])) {
5030
                                                    if (isset($real_list[$i_answer_correct_answer]) &&
5031
                                                        $showTotalScoreAndUserChoicesInLastAttempt == true
5032
                                                    ) {
5033
                                                        echo Display::span(
5034
                                                            $real_list[$i_answer_correct_answer]
5035
                                                        );
5036
                                                    }
5037
                                                }
5038
                                                echo '</td>';
5039
                                            }
5040
                                            echo '<td>'.$status.'</td>';
5041
                                        } else {
5042
                                            if (in_array($answerType, [MATCHING, MATCHING_COMBINATION, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION])) {
5043
                                                if (isset($real_list[$i_answer_correct_answer]) &&
5044
                                                    $showTotalScoreAndUserChoicesInLastAttempt === true
5045
                                                ) {
5046
                                                    if ($this->showExpectedChoiceColumn()) {
5047
                                                        echo '<td>';
5048
                                                        echo Display::span(
5049
                                                            $real_list[$i_answer_correct_answer],
5050
                                                            ['style' => 'color: #008000; font-weight: bold;']
5051
                                                        );
5052
                                                        echo '</td>';
5053
                                                    }
5054
                                                }
5055
                                            }
5056
                                        }
5057
                                        echo '</tr>';
5058
                                        break;
5059
                                    case DRAGGABLE:
5060
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
5061
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
5062
                                                break;
5063
                                            }
5064
                                        }
5065
5066
                                        echo '<tr>';
5067
                                        if ($this->showExpectedChoice() || $this->showExpectedChoiceColumn()) {
5068
                                            echo '<td>'.$s_answer_label.'</td>';
5069
                                            echo '<td>'.$user_answer.'</td>';
5070
                                            echo '<td>'.$real_list[$i_answer_correct_answer].'</td>';
5071
                                            echo '<td>'.$status.'</td>';
5072
                                        } else {
5073
                                            echo '<td>'.$s_answer_label.'</td>';
5074
                                            echo '<td>'.$user_answer.'</td>';
5075
                                            echo '<td>'.$status.'</td>';
5076
                                        }
5077
                                        echo '</tr>';
5078
                                        break;
5079
                                }
5080
                            }
5081
                            $counterAnswer++;
5082
                        }
5083
                        $matchingCorrectAnswers[$questionId]['from_database']['count_options'] = count($options);
5084
                        break 2; // break the switch and the "for" condition
5085
                    } else {
5086
                        if ($answerCorrect) {
5087
                            if (isset($choice[$answerAutoId]) &&
5088
                                $answerCorrect == $choice[$answerAutoId]
5089
                            ) {
5090
                                $matchingCorrectAnswers[$questionId]['form_values']['correct'][$answerAutoId] = $choice[$answerAutoId];
5091
                                $correctAnswerId[] = $answerAutoId;
5092
                                $questionScore += $answerWeighting;
5093
                                $totalScore += $answerWeighting;
5094
                                $user_answer = Display::span($answerMatching[$choice[$answerAutoId]]);
5095
                            } else {
5096
                                if (isset($answerMatching[$choice[$answerAutoId]])) {
5097
                                    $user_answer = Display::span(
5098
                                        $answerMatching[$choice[$answerAutoId]],
5099
                                        ['style' => 'color: #FF0000; text-decoration: line-through;']
5100
                                    );
5101
                                }
5102
                            }
5103
                            $matching[$answerAutoId] = $choice[$answerAutoId];
5104
                        }
5105
                        $matchingCorrectAnswers[$questionId]['form_values']['count_options'] = count($choice);
5106
                    }
5107
                    break;
5108
                case HOT_SPOT:
5109
                case HOT_SPOT_COMBINATION:
5110
                    if ($from_database) {
5111
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
5112
                        // Check auto id
5113
                        $foundAnswerId = $answerAutoId;
5114
                        $sql = "SELECT hotspot_correct
5115
                                FROM $TBL_TRACK_HOTSPOT
5116
                                WHERE
5117
                                    hotspot_exe_id = $exeId AND
5118
                                    hotspot_question_id= $questionId AND
5119
                                    hotspot_answer_id = $answerAutoId
5120
                                ORDER BY hotspot_id ASC";
5121
                        $result = Database::query($sql);
5122
                        if (Database::num_rows($result)) {
5123
                            $studentChoice = Database::result(
5124
                                $result,
5125
                                0,
5126
                                'hotspot_correct'
5127
                            );
5128
5129
                            if ($studentChoice) {
5130
                                $questionScore += $answerWeighting;
5131
                                $totalScore += $answerWeighting;
5132
                            }
5133
                        } else {
5134
                            // If answer.id is different:
5135
                            $sql = "SELECT hotspot_correct
5136
                                FROM $TBL_TRACK_HOTSPOT
5137
                                WHERE
5138
                                    hotspot_exe_id = $exeId AND
5139
                                    hotspot_question_id= $questionId AND
5140
                                    hotspot_answer_id = ".intval($answerId)."
5141
                                ORDER BY hotspot_id ASC";
5142
                            $result = Database::query($sql);
5143
                            $foundAnswerId = $answerId;
5144
                            if (Database::num_rows($result)) {
5145
                                $studentChoice = Database::result(
5146
                                    $result,
5147
                                    0,
5148
                                    'hotspot_correct'
5149
                                );
5150
5151
                                if ($studentChoice) {
5152
                                    $questionScore += $answerWeighting;
5153
                                    $totalScore += $answerWeighting;
5154
                                }
5155
                            } else {
5156
                                // check answer.iid
5157
                                if (!empty($answerIid)) {
5158
                                    $sql = "SELECT hotspot_correct
5159
                                            FROM $TBL_TRACK_HOTSPOT
5160
                                            WHERE
5161
                                                hotspot_exe_id = $exeId AND
5162
                                                hotspot_question_id= $questionId AND
5163
                                                hotspot_answer_id = $answerIid
5164
                                            ORDER BY hotspot_id ASC";
5165
                                    $result = Database::query($sql);
5166
                                    $foundAnswerId = $answerIid;
5167
                                    $studentChoice = Database::result(
5168
                                        $result,
5169
                                        0,
5170
                                        'hotspot_correct'
5171
                                    );
5172
5173
                                    if ($studentChoice) {
5174
                                        $questionScore += $answerWeighting;
5175
                                        $totalScore += $answerWeighting;
5176
                                    }
5177
                                }
5178
                            }
5179
                        }
5180
                    } else {
5181
                        if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
5182
                            $choice[$answerAutoId] = 0;
5183
                            $choice[$answerIid] = 0;
5184
                        } else {
5185
                            $studentChoice = $choice[$answerAutoId];
5186
                            if (empty($studentChoice)) {
5187
                                $studentChoice = $choice[$answerIid];
5188
                            }
5189
                            $choiceIsValid = false;
5190
                            if (!empty($studentChoice)) {
5191
                                $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
5192
                                $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
5193
                                $choicePoint = Geometry::decodePoint($studentChoice);
5194
5195
                                switch ($hotspotType) {
5196
                                    case 'square':
5197
                                        $hotspotProperties = Geometry::decodeSquare($hotspotCoordinates);
5198
                                        $choiceIsValid = Geometry::pointIsInSquare($hotspotProperties, $choicePoint);
5199
                                        break;
5200
                                    case 'circle':
5201
                                        $hotspotProperties = Geometry::decodeEllipse($hotspotCoordinates);
5202
                                        $choiceIsValid = Geometry::pointIsInEllipse($hotspotProperties, $choicePoint);
5203
                                        break;
5204
                                    case 'poly':
5205
                                        $hotspotProperties = Geometry::decodePolygon($hotspotCoordinates);
5206
                                        $choiceIsValid = Geometry::pointIsInPolygon($hotspotProperties, $choicePoint);
5207
                                        break;
5208
                                }
5209
                            }
5210
5211
                            $choice[$answerAutoId] = 0;
5212
                            if ($choiceIsValid) {
5213
                                $questionScore += $answerWeighting;
5214
                                $totalScore += $answerWeighting;
5215
                                $choice[$answerAutoId] = 1;
5216
                                $choice[$answerIid] = 1;
5217
                            }
5218
                        }
5219
                    }
5220
                    break;
5221
                case HOT_SPOT_ORDER:
5222
                    // @todo never added to chamilo
5223
                    // for hotspot with fixed order
5224
                    $studentChoice = $choice['order'][$answerId];
5225
                    if ($studentChoice == $answerId) {
5226
                        $questionScore += $answerWeighting;
5227
                        $totalScore += $answerWeighting;
5228
                        $studentChoice = true;
5229
                    } else {
5230
                        $studentChoice = false;
5231
                    }
5232
                    break;
5233
                case HOT_SPOT_DELINEATION:
5234
                    // for hotspot with delineation
5235
                    if ($from_database) {
5236
                        // getting the user answer
5237
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
5238
                        $query = "SELECT hotspot_correct, hotspot_coordinate
5239
                                    FROM $TBL_TRACK_HOTSPOT
5240
                                    WHERE
5241
                                        hotspot_exe_id = $exeId AND
5242
                                        hotspot_question_id= $questionId AND
5243
                                        hotspot_answer_id = '1'";
5244
                        // By default we take 1 because it's a delineation
5245
                        $resq = Database::query($query);
5246
                        $row = Database::fetch_array($resq, 'ASSOC');
5247
5248
                        $choice = $row['hotspot_correct'];
5249
                        $user_answer = $row['hotspot_coordinate'];
5250
5251
                        // THIS is very important otherwise the poly_compile will throw an error!!
5252
                        // round-up the coordinates
5253
                        $coords = explode('/', $user_answer);
5254
                        $coords = array_filter($coords);
5255
                        $user_array = '';
5256
                        foreach ($coords as $coord) {
5257
                            [$x, $y] = explode(';', $coord);
5258
                            $user_array .= round($x).';'.round($y).'/';
5259
                        }
5260
                        $user_array = substr($user_array, 0, -1) ?: '';
5261
                    } else {
5262
                        if (!empty($studentChoice)) {
5263
                            $correctAnswerId[] = $answerAutoId;
5264
                            $newquestionList[] = $questionId;
5265
                        }
5266
5267
                        if (1 === $answerId) {
5268
                            $studentChoice = $choice[$answerId];
5269
                            $questionScore += $answerWeighting;
5270
                        }
5271
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
5272
                            $user_array = $_SESSION['exerciseResultCoordinates'][$questionId];
5273
                        }
5274
                    }
5275
                    $_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
5276
                    $_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
5277
                    break;
5278
                case ANNOTATION:
5279
                    if ($from_database) {
5280
                        $sql = "SELECT answer, marks
5281
                                FROM $TBL_TRACK_ATTEMPT
5282
                                WHERE
5283
                                  exe_id = $exeId AND
5284
                                  question_id = $questionId ";
5285
                        $resq = Database::query($sql);
5286
                        $data = Database::fetch_array($resq);
5287
5288
                        $questionScore = empty($data['marks']) ? 0 : $data['marks'];
5289
                        $arrques = $questionName;
5290
                        break;
5291
                    }
5292
                    $studentChoice = $choice;
5293
                    if ($studentChoice) {
5294
                        $questionScore = 0;
5295
                    }
5296
                    break;
5297
            }
5298
5299
            if ($show_result) {
5300
                if ('exercise_result' === $from) {
5301
                    // Display answers (if not matching type, or if the answer is correct)
5302
                    if (!in_array($answerType, [MATCHING, MATCHING_COMBINATION, DRAGGABLE, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION]) ||
5303
                        $answerCorrect
5304
                    ) {
5305
                        if (in_array(
5306
                            $answerType,
5307
                            [
5308
                                UNIQUE_ANSWER,
5309
                                UNIQUE_ANSWER_IMAGE,
5310
                                UNIQUE_ANSWER_NO_OPTION,
5311
                                MULTIPLE_ANSWER,
5312
                                MULTIPLE_ANSWER_COMBINATION,
5313
                                GLOBAL_MULTIPLE_ANSWER,
5314
                                READING_COMPREHENSION,
5315
                            ]
5316
                        )) {
5317
                            ExerciseShowFunctions::display_unique_or_multiple_answer(
5318
                                $this,
5319
                                $feedback_type,
5320
                                $answerType,
5321
                                $studentChoice,
5322
                                $answer,
5323
                                $answerComment,
5324
                                $answerCorrect,
5325
                                0,
5326
                                0,
5327
                                0,
5328
                                $results_disabled,
5329
                                $showTotalScoreAndUserChoicesInLastAttempt,
5330
                                $this->export
5331
                            );
5332
                        } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE) {
5333
                            ExerciseShowFunctions::display_multiple_answer_true_false(
5334
                                $this,
5335
                                $feedback_type,
5336
                                $answerType,
5337
                                $studentChoice,
5338
                                $answer,
5339
                                $answerComment,
5340
                                $answerCorrect,
5341
                                0,
5342
                                $questionId,
5343
                                0,
5344
                                $results_disabled,
5345
                                $showTotalScoreAndUserChoicesInLastAttempt
5346
                            );
5347
                        } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
5348
                            ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5349
                                $this,
5350
                                $feedback_type,
5351
                                $studentChoice,
5352
                                $studentChoiceDegree,
5353
                                $answer,
5354
                                $answerComment,
5355
                                $answerCorrect,
5356
                                $questionId,
5357
                                $results_disabled
5358
                            );
5359
                        } elseif ($answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE) {
5360
                            ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5361
                                $this,
5362
                                $feedback_type,
5363
                                $answerType,
5364
                                $studentChoice,
5365
                                $answer,
5366
                                $answerComment,
5367
                                $answerCorrect,
5368
                                0,
5369
                                0,
5370
                                0,
5371
                                $results_disabled,
5372
                                $showTotalScoreAndUserChoicesInLastAttempt
5373
                            );
5374
                        } elseif (in_array($answerType, [FILL_IN_BLANKS, FILL_IN_BLANKS_COMBINATION])) {
5375
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5376
                                $this,
5377
                                $feedback_type,
5378
                                $answer,
5379
                                0,
5380
                                0,
5381
                                $results_disabled,
5382
                                '',
5383
                                $showTotalScoreAndUserChoicesInLastAttempt
5384
                            );
5385
                        } elseif ($answerType == CALCULATED_ANSWER) {
5386
                            ExerciseShowFunctions::display_calculated_answer(
5387
                                $this,
5388
                                $feedback_type,
5389
                                $answer,
5390
                                0,
5391
                                0,
5392
                                $results_disabled,
5393
                                $showTotalScoreAndUserChoicesInLastAttempt,
5394
                                $expectedAnswer,
5395
                                $calculatedChoice,
5396
                                $calculatedStatus
5397
                            );
5398
                        } elseif ($answerType == FREE_ANSWER) {
5399
                            ExerciseShowFunctions::display_free_answer(
5400
                                $feedback_type,
5401
                                $choice,
5402
                                $exeId,
5403
                                $questionId,
5404
                                $questionScore,
5405
                                $results_disabled
5406
                            );
5407
                        } elseif ($answerType == UPLOAD_ANSWER) {
5408
                            ExerciseShowFunctions::displayUploadAnswer(
5409
                                $feedback_type,
5410
                                $choice,
5411
                                $exeId,
5412
                                $questionId,
5413
                                $questionScore,
5414
                                $results_disabled
5415
                            );
5416
                        } elseif ($answerType == ANSWER_IN_OFFICE_DOC) {
5417
                            $exe_info = Event::get_exercise_results_by_attempt($exeId);
5418
                            $exe_info = $exe_info[$exeId] ?? null;
5419
                            ExerciseShowFunctions::displayOnlyOfficeAnswer(
5420
                                $feedback_type,
5421
                                $exeId,
5422
                                $exe_info['exe_user_id'] ?? api_get_user_id(),
5423
                                $this->iid,
5424
                                $questionId,
5425
                                $questionScore,
5426
                                true
5427
                            );
5428
                        } elseif ($answerType == ORAL_EXPRESSION) {
5429
                            // to store the details of open questions in an array to be used in mail
5430
                            /** @var OralExpression $objQuestionTmp */
5431
                            ExerciseShowFunctions::display_oral_expression_answer(
5432
                                $feedback_type,
5433
                                $choice,
5434
                                0,
5435
                                0,
5436
                                $objQuestionTmp->getFileUrl(true),
5437
                                $results_disabled,
5438
                                $questionScore
5439
                            );
5440
                        } elseif (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION])) {
5441
                            $correctAnswerId = 0;
5442
                            /** @var TrackEHotspot $hotspot */
5443
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5444
                                if ($hotspot->getHotspotAnswerId() == $answerIid) {
5445
                                    break;
5446
                                }
5447
                            }
5448
5449
                            // force to show whether the choice is correct or not
5450
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
5451
                            ExerciseShowFunctions::display_hotspot_answer(
5452
                                $this,
5453
                                $feedback_type,
5454
                                $answerId,
5455
                                $answer,
5456
                                $studentChoice,
5457
                                $answerComment,
5458
                                $results_disabled,
5459
                                $answerId,
5460
                                $showTotalScoreAndUserChoicesInLastAttempt
5461
                            );
5462
                        } elseif ($answerType == HOT_SPOT_ORDER) {
5463
                            ExerciseShowFunctions::display_hotspot_order_answer(
5464
                                $feedback_type,
5465
                                $answerId,
5466
                                $answer,
5467
                                $studentChoice,
5468
                                $answerComment
5469
                            );
5470
                        } elseif ($answerType == HOT_SPOT_DELINEATION) {
5471
                            $user_answer = $_SESSION['exerciseResultCoordinates'][$questionId];
5472
5473
                            // Round-up the coordinates
5474
                            $coords = explode('/', $user_answer);
5475
                            $coords = array_filter($coords);
5476
                            $user_array = '';
5477
                            foreach ($coords as $coord) {
5478
                                if (!empty($coord)) {
5479
                                    $parts = explode(';', $coord);
5480
                                    if (!empty($parts)) {
5481
                                        $user_array .= round($parts[0]).';'.round($parts[1]).'/';
5482
                                    }
5483
                                }
5484
                            }
5485
                            $user_array = substr($user_array, 0, -1) ?: '';
5486
                            if ($next) {
5487
                                $user_answer = $user_array;
5488
                                // We compare only the delineation not the other points
5489
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5490
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5491
5492
                                // Calculating the area
5493
                                $poly_user = convert_coordinates($user_answer, '/');
5494
                                $poly_answer = convert_coordinates($answer_question, '|');
5495
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5496
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5497
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5498
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5499
5500
                                $overlap = $poly_results['both'];
5501
                                $poly_answer_area = $poly_results['s1'];
5502
                                $poly_user_area = $poly_results['s2'];
5503
                                $missing = $poly_results['s1Only'];
5504
                                $excess = $poly_results['s2Only'];
5505
5506
                                // //this is an area in pixels
5507
                                if ($debug > 0) {
5508
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5509
                                }
5510
5511
                                if ($overlap < 1) {
5512
                                    // Shortcut to avoid complicated calculations
5513
                                    $final_overlap = 0;
5514
                                    $final_missing = 100;
5515
                                    $final_excess = 100;
5516
                                } else {
5517
                                    // the final overlap is the percentage of the initial polygon
5518
                                    // that is overlapped by the user's polygon
5519
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5520
                                    if ($debug > 1) {
5521
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap, 0);
5522
                                    }
5523
                                    // the final missing area is the percentage of the initial polygon
5524
                                    // that is not overlapped by the user's polygon
5525
                                    $final_missing = 100 - $final_overlap;
5526
                                    if ($debug > 1) {
5527
                                        error_log(__LINE__.' - Final missing is '.$final_missing, 0);
5528
                                    }
5529
                                    // the final excess area is the percentage of the initial polygon's size
5530
                                    // that is covered by the user's polygon outside of the initial polygon
5531
                                    $final_excess = round((((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100);
5532
                                    if ($debug > 1) {
5533
                                        error_log(__LINE__.' - Final excess is '.$final_excess, 0);
5534
                                    }
5535
                                }
5536
5537
                                // Checking the destination parameters parsing the "@@"
5538
                                $destination_items = explode('@@', $answerDestination);
5539
                                $threadhold_total = $destination_items[0];
5540
                                $threadhold_items = explode(';', $threadhold_total);
5541
                                $threadhold1 = $threadhold_items[0]; // overlap
5542
                                $threadhold2 = $threadhold_items[1]; // excess
5543
                                $threadhold3 = $threadhold_items[2]; // missing
5544
5545
                                // if is delineation
5546
                                if ($answerId === 1) {
5547
                                    //setting colors
5548
                                    if ($final_overlap >= $threadhold1) {
5549
                                        $overlap_color = true;
5550
                                    }
5551
                                    if ($final_excess <= $threadhold2) {
5552
                                        $excess_color = true;
5553
                                    }
5554
                                    if ($final_missing <= $threadhold3) {
5555
                                        $missing_color = true;
5556
                                    }
5557
5558
                                    // if pass
5559
                                    if ($final_overlap >= $threadhold1 &&
5560
                                        $final_missing <= $threadhold3 &&
5561
                                        $final_excess <= $threadhold2
5562
                                    ) {
5563
                                        $next = 1; //go to the oars
5564
                                        $result_comment = get_lang('Acceptable');
5565
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5566
                                    } else {
5567
                                        $next = 0;
5568
                                        $result_comment = get_lang('Unacceptable');
5569
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5570
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5571
                                        // checking the destination parameters parsing the "@@"
5572
                                        $destination_items = explode('@@', $answerDestination);
5573
                                    }
5574
                                } elseif ($answerId > 1) {
5575
                                    if ($objAnswerTmp->selectHotspotType($answerId) == 'noerror') {
5576
                                        if ($debug > 0) {
5577
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5578
                                        }
5579
                                        //type no error shouldn't be treated
5580
                                        $next = 1;
5581
                                        continue;
5582
                                    }
5583
                                    if ($debug > 0) {
5584
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5585
                                    }
5586
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5587
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5588
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5589
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5590
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5591
5592
                                    if ($overlap == false) {
5593
                                        //all good, no overlap
5594
                                        $next = 1;
5595
                                        continue;
5596
                                    } else {
5597
                                        if ($debug > 0) {
5598
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5599
                                        }
5600
                                        $organs_at_risk_hit++;
5601
                                        //show the feedback
5602
                                        $next = 0;
5603
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5604
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5605
5606
                                        $destination_items = explode('@@', $answerDestination);
5607
                                        $try_hotspot = $destination_items[1];
5608
                                        $lp_hotspot = $destination_items[2];
5609
                                        $select_question_hotspot = $destination_items[3];
5610
                                        $url_hotspot = $destination_items[4];
5611
                                    }
5612
                                }
5613
                            } else {
5614
                                // the first delineation feedback
5615
                                if ($debug > 0) {
5616
                                    error_log(__LINE__.' first', 0);
5617
                                }
5618
                            }
5619
                        } elseif (in_array($answerType, [MATCHING, MATCHING_COMBINATION, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION])) {
5620
                            echo '<tr>';
5621
                            echo Display::tag('td', $answerMatching[$answerId]);
5622
                            echo Display::tag(
5623
                                'td',
5624
                                "$user_answer / ".Display::tag(
5625
                                    'strong',
5626
                                    $answerMatching[$answerCorrect],
5627
                                    ['style' => 'color: #008000; font-weight: bold;']
5628
                                )
5629
                            );
5630
                            echo '</tr>';
5631
                        } elseif ($answerType == ANNOTATION) {
5632
                            ExerciseShowFunctions::displayAnnotationAnswer(
5633
                                $feedback_type,
5634
                                $exeId,
5635
                                $questionId,
5636
                                $questionScore,
5637
                                $results_disabled
5638
                            );
5639
                        }
5640
                    }
5641
                } else {
5642
                    if ($debug) {
5643
                        error_log('Showing questions $from '.$from);
5644
                    }
5645
5646
                    switch ($answerType) {
5647
                        case UNIQUE_ANSWER:
5648
                        case UNIQUE_ANSWER_IMAGE:
5649
                        case UNIQUE_ANSWER_NO_OPTION:
5650
                        case MULTIPLE_ANSWER:
5651
                        case GLOBAL_MULTIPLE_ANSWER:
5652
                        case MULTIPLE_ANSWER_COMBINATION:
5653
                        case READING_COMPREHENSION:
5654
                            if ($answerId == 1) {
5655
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5656
                                    $this,
5657
                                    $feedback_type,
5658
                                    $answerType,
5659
                                    $studentChoice,
5660
                                    $answer,
5661
                                    $answerComment,
5662
                                    $answerCorrect,
5663
                                    $exeId,
5664
                                    $questionId,
5665
                                    $answerId,
5666
                                    $results_disabled,
5667
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5668
                                    $this->export
5669
                                );
5670
                            } else {
5671
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5672
                                    $this,
5673
                                    $feedback_type,
5674
                                    $answerType,
5675
                                    $studentChoice,
5676
                                    $answer,
5677
                                    $answerComment,
5678
                                    $answerCorrect,
5679
                                    $exeId,
5680
                                    $questionId,
5681
                                    '',
5682
                                    $results_disabled,
5683
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5684
                                    $this->export
5685
                                );
5686
                            }
5687
                            break;
5688
                        case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
5689
                            if ($answerId == 1) {
5690
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5691
                                    $this,
5692
                                    $feedback_type,
5693
                                    $answerType,
5694
                                    $studentChoice,
5695
                                    $answer,
5696
                                    $answerComment,
5697
                                    $answerCorrect,
5698
                                    $exeId,
5699
                                    $questionId,
5700
                                    $answerId,
5701
                                    $results_disabled,
5702
                                    $showTotalScoreAndUserChoicesInLastAttempt
5703
                                );
5704
                            } else {
5705
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5706
                                    $this,
5707
                                    $feedback_type,
5708
                                    $answerType,
5709
                                    $studentChoice,
5710
                                    $answer,
5711
                                    $answerComment,
5712
                                    $answerCorrect,
5713
                                    $exeId,
5714
                                    $questionId,
5715
                                    '',
5716
                                    $results_disabled,
5717
                                    $showTotalScoreAndUserChoicesInLastAttempt
5718
                                );
5719
                            }
5720
                            break;
5721
                        case MULTIPLE_ANSWER_TRUE_FALSE:
5722
                            if ($answerId == 1) {
5723
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5724
                                    $this,
5725
                                    $feedback_type,
5726
                                    $answerType,
5727
                                    $studentChoice,
5728
                                    $answer,
5729
                                    $answerComment,
5730
                                    $answerCorrect,
5731
                                    $exeId,
5732
                                    $questionId,
5733
                                    $answerId,
5734
                                    $results_disabled,
5735
                                    $showTotalScoreAndUserChoicesInLastAttempt
5736
                                );
5737
                            } else {
5738
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5739
                                    $this,
5740
                                    $feedback_type,
5741
                                    $answerType,
5742
                                    $studentChoice,
5743
                                    $answer,
5744
                                    $answerComment,
5745
                                    $answerCorrect,
5746
                                    $exeId,
5747
                                    $questionId,
5748
                                    '',
5749
                                    $results_disabled,
5750
                                    $showTotalScoreAndUserChoicesInLastAttempt
5751
                                );
5752
                            }
5753
                            break;
5754
                        case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
5755
                            if ($answerId == 1) {
5756
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5757
                                    $this,
5758
                                    $feedback_type,
5759
                                    $studentChoice,
5760
                                    $studentChoiceDegree,
5761
                                    $answer,
5762
                                    $answerComment,
5763
                                    $answerCorrect,
5764
                                    $questionId,
5765
                                    $results_disabled
5766
                                );
5767
                            } else {
5768
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5769
                                    $this,
5770
                                    $feedback_type,
5771
                                    $studentChoice,
5772
                                    $studentChoiceDegree,
5773
                                    $answer,
5774
                                    $answerComment,
5775
                                    $answerCorrect,
5776
                                    $questionId,
5777
                                    $results_disabled
5778
                                );
5779
                            }
5780
                            break;
5781
                        case FILL_IN_BLANKS:
5782
                        case FILL_IN_BLANKS_COMBINATION:
5783
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5784
                                $this,
5785
                                $feedback_type,
5786
                                $answer,
5787
                                $exeId,
5788
                                $questionId,
5789
                                $results_disabled,
5790
                                $str,
5791
                                $showTotalScoreAndUserChoicesInLastAttempt
5792
                            );
5793
                            break;
5794
                        case CALCULATED_ANSWER:
5795
                            ExerciseShowFunctions::display_calculated_answer(
5796
                                $this,
5797
                                $feedback_type,
5798
                                $answer,
5799
                                $exeId,
5800
                                $questionId,
5801
                                $results_disabled,
5802
                                '',
5803
                                $showTotalScoreAndUserChoicesInLastAttempt
5804
                            );
5805
                            break;
5806
                        case FREE_ANSWER:
5807
                            echo ExerciseShowFunctions::display_free_answer(
5808
                                $feedback_type,
5809
                                $choice,
5810
                                $exeId,
5811
                                $questionId,
5812
                                $questionScore,
5813
                                $results_disabled
5814
                            );
5815
                            break;
5816
                        case UPLOAD_ANSWER:
5817
                            echo ExerciseShowFunctions::displayUploadAnswer(
5818
                                $feedback_type,
5819
                                $choice,
5820
                                $exeId,
5821
                                $questionId,
5822
                                $questionScore,
5823
                                $results_disabled
5824
                            );
5825
                            break;
5826
                        case ANSWER_IN_OFFICE_DOC:
5827
                            $exe_info = Event::get_exercise_results_by_attempt($exeId);
5828
                            $exe_info = $exe_info[$exeId] ?? null;
5829
                            ExerciseShowFunctions::displayOnlyOfficeAnswer(
5830
                                $feedback_type,
5831
                                $exeId,
5832
                                $exe_info['exe_user_id'] ?? api_get_user_id(),
5833
                                $this->iid,
5834
                                $questionId,
5835
                                $questionScore
5836
                            );
5837
                            break;
5838
                        case ORAL_EXPRESSION:
5839
                            echo '<tr>
5840
                                <td>'.
5841
                                ExerciseShowFunctions::display_oral_expression_answer(
5842
                                    $feedback_type,
5843
                                    $choice,
5844
                                    $exeId,
5845
                                    $questionId,
5846
                                    $objQuestionTmp->getFileUrl(),
5847
                                    $results_disabled,
5848
                                    $questionScore
5849
                                ).'</td>
5850
                                </tr>
5851
                                </table>';
5852
                            break;
5853
                        case HOT_SPOT:
5854
                        case HOT_SPOT_COMBINATION:
5855
                            $correctAnswerId = 0;
5856
                            /** @var TrackEHotspot $hotspot */
5857
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5858
                                if ($hotspot->getHotspotAnswerId() == $foundAnswerId) {
5859
                                    break;
5860
                                }
5861
                            }
5862
5863
                            ExerciseShowFunctions::display_hotspot_answer(
5864
                                $this,
5865
                                $feedback_type,
5866
                                $answerId,
5867
                                $answer,
5868
                                $studentChoice,
5869
                                $answerComment,
5870
                                $results_disabled,
5871
                                $answerId,
5872
                                $showTotalScoreAndUserChoicesInLastAttempt
5873
                            );
5874
                            break;
5875
                        case HOT_SPOT_DELINEATION:
5876
                            $user_answer = $user_array;
5877
                            if ($next) {
5878
                                $user_answer = $user_array;
5879
                                // we compare only the delineation not the other points
5880
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5881
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5882
5883
                                // calculating the area
5884
                                $poly_user = convert_coordinates($user_answer, '/');
5885
                                $poly_answer = convert_coordinates($answer_question, '|');
5886
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5887
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5888
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5889
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5890
5891
                                $overlap = $poly_results['both'];
5892
                                $poly_answer_area = $poly_results['s1'];
5893
                                $poly_user_area = $poly_results['s2'];
5894
                                $missing = $poly_results['s1Only'];
5895
                                $excess = $poly_results['s2Only'];
5896
                                if ($debug > 0) {
5897
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5898
                                }
5899
                                if ($overlap < 1) {
5900
                                    //shortcut to avoid complicated calculations
5901
                                    $final_overlap = 0;
5902
                                    $final_missing = 100;
5903
                                    $final_excess = 100;
5904
                                } else {
5905
                                    // the final overlap is the percentage of the initial polygon
5906
                                    // that is overlapped by the user's polygon
5907
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5908
5909
                                    // the final missing area is the percentage of the initial polygon that
5910
                                    // is not overlapped by the user's polygon
5911
                                    $final_missing = 100 - $final_overlap;
5912
                                    // the final excess area is the percentage of the initial polygon's size that is
5913
                                    // covered by the user's polygon outside of the initial polygon
5914
                                    $final_excess = round((((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100);
5915
5916
                                    if ($debug > 1) {
5917
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap);
5918
                                        error_log(__LINE__.' - Final excess is '.$final_excess);
5919
                                        error_log(__LINE__.' - Final missing is '.$final_missing);
5920
                                    }
5921
                                }
5922
5923
                                // Checking the destination parameters parsing the "@@"
5924
                                $destination_items = explode('@@', $answerDestination);
5925
                                $threadhold_total = $destination_items[0];
5926
                                $threadhold_items = explode(';', $threadhold_total);
5927
                                $threadhold1 = $threadhold_items[0]; // overlap
5928
                                $threadhold2 = $threadhold_items[1]; // excess
5929
                                $threadhold3 = $threadhold_items[2]; //missing
5930
                                // if is delineation
5931
                                if ($answerId === 1) {
5932
                                    //setting colors
5933
                                    if ($final_overlap >= $threadhold1) {
5934
                                        $overlap_color = true;
5935
                                    }
5936
                                    if ($final_excess <= $threadhold2) {
5937
                                        $excess_color = true;
5938
                                    }
5939
                                    if ($final_missing <= $threadhold3) {
5940
                                        $missing_color = true;
5941
                                    }
5942
5943
                                    // if pass
5944
                                    if ($final_overlap >= $threadhold1 &&
5945
                                        $final_missing <= $threadhold3 &&
5946
                                        $final_excess <= $threadhold2
5947
                                    ) {
5948
                                        $next = 1; //go to the oars
5949
                                        $result_comment = get_lang('Acceptable');
5950
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5951
                                    } else {
5952
                                        $next = 0;
5953
                                        $result_comment = get_lang('Unacceptable');
5954
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5955
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5956
                                        //checking the destination parameters parsing the "@@"
5957
                                        $destination_items = explode('@@', $answerDestination);
5958
                                    }
5959
                                } elseif ($answerId > 1) {
5960
                                    if ($objAnswerTmp->selectHotspotType($answerId) === 'noerror') {
5961
                                        if ($debug > 0) {
5962
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5963
                                        }
5964
                                        //type no error shouldn't be treated
5965
                                        $next = 1;
5966
                                        break;
5967
                                    }
5968
                                    if ($debug > 0) {
5969
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5970
                                    }
5971
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5972
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5973
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5974
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5975
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5976
5977
                                    if ($overlap == false) {
5978
                                        //all good, no overlap
5979
                                        $next = 1;
5980
                                        break;
5981
                                    } else {
5982
                                        if ($debug > 0) {
5983
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5984
                                        }
5985
                                        $organs_at_risk_hit++;
5986
                                        //show the feedback
5987
                                        $next = 0;
5988
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5989
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5990
5991
                                        $destination_items = explode('@@', $answerDestination);
5992
                                        $try_hotspot = $destination_items[1];
5993
                                        $lp_hotspot = $destination_items[2];
5994
                                        $select_question_hotspot = $destination_items[3];
5995
                                        $url_hotspot = $destination_items[4];
5996
                                    }
5997
                                }
5998
                            }
5999
                            break;
6000
                        case HOT_SPOT_ORDER:
6001
                            ExerciseShowFunctions::display_hotspot_order_answer(
6002
                                $feedback_type,
6003
                                $answerId,
6004
                                $answer,
6005
                                $studentChoice,
6006
                                $answerComment
6007
                            );
6008
                            break;
6009
                        case DRAGGABLE:
6010
                        case MATCHING_DRAGGABLE:
6011
                        case MATCHING_DRAGGABLE_COMBINATION:
6012
                        case MATCHING_COMBINATION:
6013
                        case MATCHING:
6014
                            echo '<tr>';
6015
                            echo Display::tag('td', $answerMatching[$answerId]);
6016
                            echo Display::tag(
6017
                                'td',
6018
                                "$user_answer / ".Display::tag(
6019
                                    'strong',
6020
                                    $answerMatching[$answerCorrect],
6021
                                    ['style' => 'color: #008000; font-weight: bold;']
6022
                                )
6023
                            );
6024
                            echo '</tr>';
6025
                            break;
6026
                        case ANNOTATION:
6027
                            ExerciseShowFunctions::displayAnnotationAnswer(
6028
                                $feedback_type,
6029
                                $exeId,
6030
                                $questionId,
6031
                                $questionScore,
6032
                                $results_disabled
6033
                            );
6034
                            break;
6035
                    }
6036
                }
6037
            }
6038
        } // end for that loops over all answers of the current question
6039
6040
        // It validates unique score when all answers are correct for global questions
6041
        if (FILL_IN_BLANKS_COMBINATION === $answerType) {
6042
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
6043
                $answerType,
6044
                $listCorrectAnswers,
6045
                $exeId,
6046
                $questionId,
6047
                $questionWeighting
6048
            );
6049
        }
6050
        if (HOT_SPOT_COMBINATION === $answerType) {
6051
            $listCorrectAnswers = isset($exerciseResultCoordinates[$questionId]) ? $exerciseResultCoordinates[$questionId] : [];
6052
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
6053
                $answerType,
6054
                $listCorrectAnswers,
6055
                $exeId,
6056
                $questionId,
6057
                $questionWeighting,
6058
                $choice,
6059
                $nbrAnswers
6060
            );
6061
        }
6062
        if (in_array($answerType, [MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
6063
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
6064
                $answerType,
6065
                $matchingCorrectAnswers[$questionId],
6066
                $exeId,
6067
                $questionId,
6068
                $questionWeighting
6069
            );
6070
        }
6071
6072
        if ($debug) {
6073
            error_log('-- End answer loop --');
6074
        }
6075
6076
        $final_answer = true;
6077
6078
        foreach ($real_answers as $my_answer) {
6079
            if (!$my_answer) {
6080
                $final_answer = false;
6081
            }
6082
        }
6083
6084
        // We add the total score after dealing with the answers.
6085
        if ($answerType == MULTIPLE_ANSWER_COMBINATION ||
6086
            $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE
6087
        ) {
6088
            if ($final_answer) {
6089
                //getting only the first score where we save the weight of all the question
6090
                $answerWeighting = $objAnswerTmp->selectWeighting(1);
6091
                if (empty($answerWeighting) && !empty($firstAnswer) && isset($firstAnswer['ponderation'])) {
6092
                    $answerWeighting = $firstAnswer['ponderation'];
6093
                }
6094
                $questionScore += $answerWeighting;
6095
            }
6096
        }
6097
6098
        $extra_data = [
6099
            'final_overlap' => $final_overlap,
6100
            'final_missing' => $final_missing,
6101
            'final_excess' => $final_excess,
6102
            'overlap_color' => $overlap_color,
6103
            'missing_color' => $missing_color,
6104
            'excess_color' => $excess_color,
6105
            'threadhold1' => $threadhold1,
6106
            'threadhold2' => $threadhold2,
6107
            'threadhold3' => $threadhold3,
6108
        ];
6109
6110
        if ($from === 'exercise_result') {
6111
            // if answer is hotspot. To the difference of exercise_show.php,
6112
            //  we use the results from the session (from_db=0)
6113
            // TODO Change this, because it is wrong to show the user
6114
            //  some results that haven't been stored in the database yet
6115
            if (in_array($answerType, [HOT_SPOT, HOT_SPOT_ORDER, HOT_SPOT_DELINEATION, HOT_SPOT_COMBINATION])) {
6116
                if ($debug) {
6117
                    error_log('$from AND this is a hotspot kind of question ');
6118
                }
6119
                if ($answerType === HOT_SPOT_DELINEATION) {
6120
                    if ($showHotSpotDelineationTable) {
6121
                        if (!is_numeric($final_overlap)) {
6122
                            $final_overlap = 0;
6123
                        }
6124
                        if (!is_numeric($final_missing)) {
6125
                            $final_missing = 0;
6126
                        }
6127
                        if (!is_numeric($final_excess)) {
6128
                            $final_excess = 0;
6129
                        }
6130
6131
                        if ($final_overlap > 100) {
6132
                            $final_overlap = 100;
6133
                        }
6134
6135
                        $table_resume = '<table class="table table-hover table-striped data_table">
6136
                                <tr class="row_odd" >
6137
                                    <td></td>
6138
                                    <td ><b>'.get_lang('Requirements').'</b></td>
6139
                                    <td><b>'.get_lang('YourAnswer').'</b></td>
6140
                                </tr>
6141
                                <tr class="row_even">
6142
                                    <td><b>'.get_lang('Overlap').'</b></td>
6143
                                    <td>'.get_lang('Min').' '.$threadhold1.'</td>
6144
                                    <td class="text-right '.($overlap_color ? 'text-success' : 'text-danger').'">'
6145
                            .(($final_overlap < 0) ? 0 : intval($final_overlap)).'</td>
6146
                                </tr>
6147
                                <tr>
6148
                                    <td><b>'.get_lang('Excess').'</b></td>
6149
                                    <td>'.get_lang('Max').' '.$threadhold2.'</td>
6150
                                    <td class="text-right '.($excess_color ? 'text-success' : 'text-danger').'">'
6151
                            .(($final_excess < 0) ? 0 : intval($final_excess)).'</td>
6152
                                </tr>
6153
                                <tr class="row_even">
6154
                                    <td><b>'.get_lang('Missing').'</b></td>
6155
                                    <td>'.get_lang('Max').' '.$threadhold3.'</td>
6156
                                    <td class="text-right '.($missing_color ? 'text-success' : 'text-danger').'">'
6157
                            .(($final_missing < 0) ? 0 : intval($final_missing)).'</td>
6158
                                </tr>
6159
                            </table>';
6160
                        if ($next == 0) {
6161
                        } else {
6162
                            $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
6163
                            $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
6164
                        }
6165
6166
                        $message = '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>
6167
                                    <p style="text-align:center">';
6168
                        $message .= '<p>'.get_lang('YourDelineation').'</p>';
6169
                        $message .= $table_resume;
6170
                        $message .= '<br />'.get_lang('ResultIs').' '.$result_comment.'<br />';
6171
                        if ($organs_at_risk_hit > 0) {
6172
                            $message .= '<p><b>'.get_lang('OARHit').'</b></p>';
6173
                        }
6174
                        $message .= '<p>'.$comment.'</p>';
6175
                        echo $message;
6176
6177
                        $_SESSION['hotspot_delineation_result'][$this->selectId()][$questionId][0] = $message;
6178
                        $_SESSION['hotspot_delineation_result'][$this->selectId()][$questionId][1] = $_SESSION['exerciseResultCoordinates'][$questionId];
6179
                    } else {
6180
                        echo $hotspot_delineation_result[0];
6181
                    }
6182
6183
                    // Save the score attempts
6184
                    if (1) {
6185
                        //getting the answer 1 or 0 comes from exercise_submit_modal.php
6186
                        $final_answer = isset($hotspot_delineation_result[1]) ? $hotspot_delineation_result[1] : '';
6187
                        if ($final_answer == 0) {
6188
                            $questionScore = 0;
6189
                        }
6190
                        // we always insert the answer_id 1 = delineation
6191
                        Event::saveQuestionAttempt($questionScore, 1, $quesId, $exeId, 0);
6192
                        //in delineation mode, get the answer from $hotspot_delineation_result[1]
6193
                        $hotspotValue = isset($hotspot_delineation_result[1]) ? (int) $hotspot_delineation_result[1] === 1 ? 1 : 0 : 0;
6194
                        Event::saveExerciseAttemptHotspot(
6195
                            $exeId,
6196
                            $quesId,
6197
                            1,
6198
                            $hotspotValue,
6199
                            $exerciseResultCoordinates[$quesId],
6200
                            false,
6201
                            0,
6202
                            $learnpath_id,
6203
                            $learnpath_item_id
6204
                        );
6205
                    } else {
6206
                        if ($final_answer == 0) {
6207
                            $questionScore = 0;
6208
                            $answer = 0;
6209
                            Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0);
6210
                            if (is_array($exerciseResultCoordinates[$quesId])) {
6211
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
6212
                                    Event::saveExerciseAttemptHotspot(
6213
                                        $exeId,
6214
                                        $quesId,
6215
                                        $idx,
6216
                                        0,
6217
                                        $val,
6218
                                        false,
6219
                                        0,
6220
                                        $learnpath_id,
6221
                                        $learnpath_item_id
6222
                                    );
6223
                                }
6224
                            }
6225
                        } else {
6226
                            Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0);
6227
                            if (is_array($exerciseResultCoordinates[$quesId])) {
6228
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
6229
                                    $hotspotValue = (int) $choice[$idx] === 1 ? 1 : 0;
6230
                                    Event::saveExerciseAttemptHotspot(
6231
                                        $exeId,
6232
                                        $quesId,
6233
                                        $idx,
6234
                                        $hotspotValue,
6235
                                        $val,
6236
                                        false,
6237
                                        0,
6238
                                        $learnpath_id,
6239
                                        $learnpath_item_id
6240
                                    );
6241
                                }
6242
                            }
6243
                        }
6244
                    }
6245
                }
6246
            }
6247
6248
            $relPath = api_get_path(WEB_CODE_PATH);
6249
6250
            if (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION, HOT_SPOT_ORDER])) {
6251
                // We made an extra table for the answers
6252
                if ($show_result) {
6253
                    echo '</table></td></tr>';
6254
                    echo "
6255
                        <tr>
6256
                            <td>
6257
                                <p><em>".get_lang('HotSpot')."</em></p>
6258
                                <div id=\"hotspot-solution-$questionId\"></div>
6259
                                <script>
6260
                                    $(function() {
6261
                                        new HotspotQuestion({
6262
                                            questionId: $questionId,
6263
                                            exerciseId: {$this->iid},
6264
                                            exeId: $exeId,
6265
                                            selector: '#hotspot-solution-$questionId',
6266
                                            for: 'solution',
6267
                                            relPath: '$relPath'
6268
                                        });
6269
                                    });
6270
                                </script>
6271
                            </td>
6272
                        </tr>
6273
                    ";
6274
                }
6275
            } elseif ($answerType == ANNOTATION) {
6276
                if ($show_result) {
6277
                    echo '
6278
                        <p><em>'.get_lang('Annotation').'</em></p>
6279
                        <div id="annotation-canvas-'.$questionId.'"></div>
6280
                        <script>
6281
                            AnnotationQuestion({
6282
                                questionId: parseInt('.$questionId.'),
6283
                                exerciseId: parseInt('.$exeId.'),
6284
                                relPath: \''.$relPath.'\',
6285
                                courseId: parseInt('.$course_id.')
6286
                            });
6287
                        </script>
6288
                    ';
6289
                }
6290
            }
6291
6292
            if ($show_result && $answerType != ANNOTATION) {
6293
                echo '</table>';
6294
            }
6295
        }
6296
        unset($objAnswerTmp);
6297
6298
        $totalWeighting += $questionWeighting;
6299
        // Store results directly in the database
6300
        // For all in one page exercises, the results will be
6301
        // stored by exercise_results.php (using the session)
6302
        if ($save_results) {
6303
            if ($debug) {
6304
                error_log("Save question results $save_results");
6305
                error_log("Question score: $questionScore");
6306
                error_log('choice: ');
6307
                error_log(print_r($choice, 1));
6308
            }
6309
6310
            if (empty($choice)) {
6311
                $choice = 0;
6312
            }
6313
            // with certainty degree
6314
            if (empty($choiceDegreeCertainty)) {
6315
                $choiceDegreeCertainty = 0;
6316
            }
6317
            if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE ||
6318
                $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE ||
6319
                $answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY
6320
            ) {
6321
                if ($choice != 0) {
6322
                    $reply = array_keys($choice);
6323
                    $countReply = count($reply);
6324
                    for ($i = 0; $i < $countReply; $i++) {
6325
                        $chosenAnswer = $reply[$i];
6326
                        if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
6327
                            if ($choiceDegreeCertainty != 0) {
6328
                                $replyDegreeCertainty = array_keys($choiceDegreeCertainty);
6329
                                $answerDegreeCertainty = isset($replyDegreeCertainty[$i]) ? $replyDegreeCertainty[$i] : '';
6330
                                $answerValue = isset($choiceDegreeCertainty[$answerDegreeCertainty]) ? $choiceDegreeCertainty[$answerDegreeCertainty] : '';
6331
                                Event::saveQuestionAttempt(
6332
                                    $questionScore,
6333
                                    $chosenAnswer.':'.$choice[$chosenAnswer].':'.$answerValue,
6334
                                    $quesId,
6335
                                    $exeId,
6336
                                    $i,
6337
                                    $this->iid,
6338
                                    $updateResults,
6339
                                    $questionDuration
6340
                                );
6341
                            }
6342
                        } else {
6343
                            Event::saveQuestionAttempt(
6344
                                $questionScore,
6345
                                $chosenAnswer.':'.$choice[$chosenAnswer],
6346
                                $quesId,
6347
                                $exeId,
6348
                                $i,
6349
                                $this->iid,
6350
                                $updateResults,
6351
                                $questionDuration
6352
                            );
6353
                        }
6354
                        if ($debug) {
6355
                            error_log('result =>'.$questionScore.' '.$chosenAnswer.':'.$choice[$chosenAnswer]);
6356
                        }
6357
                    }
6358
                } else {
6359
                    Event::saveQuestionAttempt(
6360
                        $questionScore,
6361
                        0,
6362
                        $quesId,
6363
                        $exeId,
6364
                        0,
6365
                        $this->iid,
6366
                        false,
6367
                        $questionDuration
6368
                    );
6369
                }
6370
            } elseif (
6371
                in_array(
6372
                    $answerType,
6373
                    [
6374
                        MULTIPLE_ANSWER,
6375
                        GLOBAL_MULTIPLE_ANSWER,
6376
                        MULTIPLE_ANSWER_DROPDOWN,
6377
                        MULTIPLE_ANSWER_DROPDOWN_COMBINATION,
6378
                    ]
6379
                )
6380
            ) {
6381
                if ($choice != 0) {
6382
                    if (in_array($answerType, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION])) {
6383
                        $reply = array_values($choice);
6384
                    } else {
6385
                        $reply = array_keys($choice);
6386
                    }
6387
6388
                    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...
6389
                        $ans = $reply[$i];
6390
                        Event::saveQuestionAttempt(
6391
                            $questionScore,
6392
                            $ans,
6393
                            $quesId,
6394
                            $exeId,
6395
                            $i,
6396
                            $this->iid,
6397
                            false,
6398
                            $questionDuration
6399
                        );
6400
                    }
6401
                } else {
6402
                    Event::saveQuestionAttempt(
6403
                        $questionScore,
6404
                        0,
6405
                        $quesId,
6406
                        $exeId,
6407
                        0,
6408
                        $this->iid,
6409
                        false,
6410
                        $questionDuration
6411
                    );
6412
                }
6413
            } elseif ($answerType == MULTIPLE_ANSWER_COMBINATION) {
6414
                if ($choice != 0) {
6415
                    $reply = array_keys($choice);
6416
                    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...
6417
                        $ans = $reply[$i];
6418
                        Event::saveQuestionAttempt(
6419
                            $questionScore,
6420
                            $ans,
6421
                            $quesId,
6422
                            $exeId,
6423
                            $i,
6424
                            $this->iid,
6425
                            false,
6426
                            $questionDuration
6427
                        );
6428
                    }
6429
                } else {
6430
                    Event::saveQuestionAttempt(
6431
                        $questionScore,
6432
                        0,
6433
                        $quesId,
6434
                        $exeId,
6435
                        0,
6436
                        $this->iid,
6437
                        false,
6438
                        $questionDuration
6439
                    );
6440
                }
6441
            } elseif (in_array($answerType, [MATCHING, MATCHING_COMBINATION, DRAGGABLE, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION])) {
6442
                if (isset($matching)) {
6443
                    foreach ($matching as $j => $val) {
6444
                        Event::saveQuestionAttempt(
6445
                            $questionScore,
6446
                            $val,
6447
                            $quesId,
6448
                            $exeId,
6449
                            $j,
6450
                            $this->iid,
6451
                            false,
6452
                            $questionDuration
6453
                        );
6454
                    }
6455
                }
6456
            } elseif ($answerType == FREE_ANSWER) {
6457
                $answer = $choice;
6458
                Event::saveQuestionAttempt(
6459
                    $questionScore,
6460
                    $answer,
6461
                    $quesId,
6462
                    $exeId,
6463
                    0,
6464
                    $this->iid,
6465
                    false,
6466
                    $questionDuration
6467
                );
6468
            } elseif ($answerType == UPLOAD_ANSWER) {
6469
                $answer = $choice;
6470
                Event::saveQuestionAttempt(
6471
                    $questionScore,
6472
                    $answer,
6473
                    $quesId,
6474
                    $exeId,
6475
                    0,
6476
                    $this->iid,
6477
                    false,
6478
                    $questionDuration
6479
                );
6480
            } elseif ($answerType == ANSWER_IN_OFFICE_DOC) {
6481
                $answer = $choice;
6482
                $exerciseId = $this->iid;
6483
                $questionId = $quesId;
6484
                $originalFilePath = $objQuestionTmp->getFileUrl();
6485
                $originalExtension = !empty($originalFilePath) ? pathinfo($originalFilePath, PATHINFO_EXTENSION) : 'docx';
6486
                $fileName = "response_{$exeId}.{$originalExtension}";
6487
                Event::saveQuestionAttempt(
6488
                    $questionScore,
6489
                    $answer,
6490
                    $questionId,
6491
                    $exeId,
6492
                    0,
6493
                    $exerciseId,
6494
                    false,
6495
                    $questionDuration,
6496
                    $fileName
6497
                );
6498
            } elseif ($answerType == ORAL_EXPRESSION) {
6499
                $answer = $choice;
6500
                $absFilePath = $objQuestionTmp->getAbsoluteFilePath();
6501
                if (empty($answer) && !empty($absFilePath)) {
6502
                    // it takes the filename as answer to recognise has been saved
6503
                    $answer = basename($absFilePath);
6504
                }
6505
                Event::saveQuestionAttempt(
6506
                    $questionScore,
6507
                    $answer,
6508
                    $quesId,
6509
                    $exeId,
6510
                    0,
6511
                    $this->iid,
6512
                    false,
6513
                    $questionDuration,
6514
                    $absFilePath
6515
                );
6516
            } elseif (
6517
                in_array(
6518
                    $answerType,
6519
                    [UNIQUE_ANSWER, UNIQUE_ANSWER_IMAGE, UNIQUE_ANSWER_NO_OPTION, READING_COMPREHENSION]
6520
                )
6521
            ) {
6522
                $answer = $choice;
6523
                Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0, $this->iid, false, $questionDuration);
6524
            } elseif (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION, ANNOTATION])) {
6525
                $answer = [];
6526
                if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) {
6527
                    if ($debug) {
6528
                        error_log('Checking result coordinates');
6529
                    }
6530
                    Database::delete(
6531
                        Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
6532
                        [
6533
                            'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
6534
                                $exeId,
6535
                                $questionId,
6536
                                api_get_course_int_id(),
6537
                            ],
6538
                        ]
6539
                    );
6540
6541
                    foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
6542
                        $answer[] = $val;
6543
                        $hotspotValue = (int) $choice[$idx] === 1 ? 1 : 0;
6544
                        if ($debug) {
6545
                            error_log('Hotspot value: '.$hotspotValue);
6546
                        }
6547
                        Event::saveExerciseAttemptHotspot(
6548
                            $exeId,
6549
                            $quesId,
6550
                            $idx,
6551
                            $hotspotValue,
6552
                            $val,
6553
                            false,
6554
                            $this->iid,
6555
                            $learnpath_id,
6556
                            $learnpath_item_id
6557
                        );
6558
                    }
6559
                } else {
6560
                    if ($debug) {
6561
                        error_log('Empty: exerciseResultCoordinates');
6562
                    }
6563
                }
6564
                Event::saveQuestionAttempt(
6565
                    $questionScore,
6566
                    implode('|', $answer),
6567
                    $quesId,
6568
                    $exeId,
6569
                    0,
6570
                    $this->iid,
6571
                    false,
6572
                    $questionDuration
6573
                );
6574
            } else {
6575
                if ($answerType === CALCULATED_ANSWER && !empty($calculatedAnswerId)) {
6576
                    $answer .= ':::'.$calculatedAnswerId;
6577
                }
6578
6579
                Event::saveQuestionAttempt(
6580
                    $questionScore,
6581
                    $answer,
6582
                    $quesId,
6583
                    $exeId,
6584
                    0,
6585
                    $this->iid,
6586
                    false,
6587
                    $questionDuration
6588
                );
6589
            }
6590
        }
6591
6592
        if ($propagate_neg == 0 && $questionScore < 0) {
6593
            $questionScore = 0;
6594
        }
6595
6596
        if ($save_results) {
6597
            $statsTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6598
            $sql = "UPDATE $statsTable SET
6599
                        exe_result = exe_result + ".floatval($questionScore)."
6600
                    WHERE exe_id = $exeId";
6601
            Database::query($sql);
6602
        }
6603
6604
        return [
6605
            'score' => $questionScore,
6606
            'weight' => $questionWeighting,
6607
            'extra' => $extra_data,
6608
            'open_question' => $arrques,
6609
            'open_answer' => $arrans,
6610
            'answer_type' => $answerType,
6611
            'generated_oral_file' => $generatedFile,
6612
            'user_answered' => $userAnsweredQuestion,
6613
            'correct_answer_id' => $correctAnswerId,
6614
            'answer_destination' => $answerDestination,
6615
        ];
6616
    }
6617
6618
    /**
6619
     * Sends a notification when a user ends an examn.
6620
     *
6621
     * @param string $type                  'start' or 'end' of an exercise
6622
     * @param array  $question_list_answers
6623
     * @param string $origin
6624
     * @param int    $exe_id
6625
     * @param float  $score
6626
     * @param float  $weight
6627
     *
6628
     * @return bool
6629
     */
6630
    public function send_mail_notification_for_exam(
6631
        $type,
6632
        $question_list_answers,
6633
        $origin,
6634
        $exe_id,
6635
        $score = null,
6636
        $weight = null
6637
    ) {
6638
        $setting = api_get_course_setting('email_alert_manager_on_new_quiz');
6639
6640
        if (empty($setting) && empty($this->getNotifications())) {
6641
            return false;
6642
        }
6643
6644
        $settingFromExercise = $this->getNotifications();
6645
        if (!empty($settingFromExercise)) {
6646
            $setting = $settingFromExercise;
6647
        }
6648
6649
        // Email configuration settings
6650
        $courseCode = api_get_course_id();
6651
        $courseInfo = api_get_course_info($courseCode);
6652
6653
        if (empty($courseInfo)) {
6654
            return false;
6655
        }
6656
6657
        $sessionId = api_get_session_id();
6658
6659
        $sessionData = '';
6660
        if (!empty($sessionId)) {
6661
            $sessionInfo = api_get_session_info($sessionId);
6662
            if (!empty($sessionInfo)) {
6663
                $sessionData = '<tr>'
6664
                    .'<td>'.get_lang('SessionName').'</td>'
6665
                    .'<td>'.$sessionInfo['name'].'</td>'
6666
                    .'</tr>';
6667
            }
6668
        }
6669
6670
        $sendStart = false;
6671
        $sendEnd = false;
6672
        $sendEndOpenQuestion = false;
6673
        $sendEndOralQuestion = false;
6674
6675
        foreach ($setting as $option) {
6676
            switch ($option) {
6677
                case 0:
6678
                    return false;
6679
                    break;
0 ignored issues
show
break is not strictly necessary here and could be removed.

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

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

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

Loading history...
6680
                case 1: // End
6681
                    if ($type === 'end') {
6682
                        $sendEnd = true;
6683
                    }
6684
                    break;
6685
                case 2: // start
6686
                    if ($type === 'start') {
6687
                        $sendStart = true;
6688
                    }
6689
                    break;
6690
                case 3: // end + open
6691
                    if ($type === 'end') {
6692
                        $sendEndOpenQuestion = true;
6693
                    }
6694
                    break;
6695
                case 4: // end + oral
6696
                    if ($type === 'end') {
6697
                        $sendEndOralQuestion = true;
6698
                    }
6699
                    break;
6700
            }
6701
        }
6702
6703
        $user_info = api_get_user_info(api_get_user_id());
6704
        $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_show.php?'.
6705
            api_get_cidreq(true, true, 'qualify').'&id='.$exe_id.'&action=qualify';
6706
6707
        if (!empty($sessionId)) {
6708
            $addGeneralCoach = true;
6709
            $setting = api_get_configuration_value('block_quiz_mail_notification_general_coach');
6710
            if ($setting === true) {
6711
                $addGeneralCoach = false;
6712
            }
6713
            $teachers = CourseManager::get_coach_list_from_course_code(
6714
                $courseCode,
6715
                $sessionId,
6716
                $addGeneralCoach
6717
            );
6718
        } else {
6719
            $teachers = CourseManager::get_teacher_list_from_course_code($courseCode);
6720
        }
6721
6722
        if ($sendEndOpenQuestion) {
6723
            $this->sendNotificationForOpenQuestions(
6724
                $question_list_answers,
6725
                $origin,
6726
                $user_info,
6727
                $url,
6728
                $teachers
6729
            );
6730
        }
6731
6732
        if ($sendEndOralQuestion) {
6733
            $this->sendNotificationForOralQuestions(
6734
                $question_list_answers,
6735
                $origin,
6736
                $exe_id,
6737
                $user_info,
6738
                $url,
6739
                $teachers
6740
            );
6741
        }
6742
6743
        if (!$sendEnd && !$sendStart) {
6744
            return false;
6745
        }
6746
6747
        $scoreLabel = '';
6748
        if ($sendEnd &&
6749
            api_get_configuration_value('send_score_in_exam_notification_mail_to_manager') == true
6750
        ) {
6751
            $notificationPercentage = api_get_configuration_value('send_notification_score_in_percentage');
6752
            $scoreLabel = ExerciseLib::show_score($score, $weight, $notificationPercentage, true);
6753
            $scoreLabel = "<tr>
6754
                            <td>".get_lang('Score')."</td>
6755
                            <td>&nbsp;$scoreLabel</td>
6756
                        </tr>";
6757
        }
6758
6759
        if ($sendEnd) {
6760
            $msg = get_lang('ExerciseAttempted').'<br /><br />';
6761
        } else {
6762
            $msg = get_lang('StudentStartExercise').'<br /><br />';
6763
        }
6764
6765
        $msg .= get_lang('AttemptDetails').' : <br /><br />
6766
                    <table>
6767
                        <tr>
6768
                            <td>'.get_lang('CourseName').'</td>
6769
                            <td>#course#</td>
6770
                        </tr>
6771
                        '.$sessionData.'
6772
                        <tr>
6773
                            <td>'.get_lang('Exercise').'</td>
6774
                            <td>&nbsp;#exercise#</td>
6775
                        </tr>
6776
                        <tr>
6777
                            <td>'.get_lang('StudentName').'</td>
6778
                            <td>&nbsp;#student_complete_name#</td>
6779
                        </tr>
6780
                        <tr>
6781
                            <td>'.get_lang('StudentEmail').'</td>
6782
                            <td>&nbsp;#email#</td>
6783
                        </tr>
6784
                        '.$scoreLabel.'
6785
                    </table>';
6786
6787
        $variables = [
6788
            '#email#' => $user_info['email'],
6789
            '#exercise#' => $this->exercise,
6790
            '#student_complete_name#' => $user_info['complete_name'],
6791
            '#course#' => Display::url(
6792
                $courseInfo['title'],
6793
                $courseInfo['course_public_url'].'?id_session='.$sessionId
6794
            ),
6795
        ];
6796
6797
        if ($sendEnd) {
6798
            $msg .= '<br /><a href="#url#">'.get_lang('ClickToCommentAndGiveFeedback').'</a>';
6799
            $variables['#url#'] = $url;
6800
        }
6801
6802
        $content = str_replace(array_keys($variables), array_values($variables), $msg);
6803
6804
        if ($sendEnd) {
6805
            $subject = get_lang('ExerciseAttempted');
6806
        } else {
6807
            $subject = get_lang('StudentStartExercise');
6808
        }
6809
6810
        if (!empty($teachers)) {
6811
            foreach ($teachers as $user_id => $teacher_data) {
6812
                MessageManager::send_message_simple(
6813
                    $user_id,
6814
                    $subject,
6815
                    $content
6816
                );
6817
            }
6818
        }
6819
    }
6820
6821
    /**
6822
     * @param array $user_data         result of api_get_user_info()
6823
     * @param array $trackExerciseInfo result of get_stat_track_exercise_info
6824
     * @param bool  $saveUserResult
6825
     * @param bool  $allowSignature
6826
     * @param bool  $allowExportPdf
6827
     *
6828
     * @return string
6829
     */
6830
    public function showExerciseResultHeader(
6831
        $user_data,
6832
        $trackExerciseInfo,
6833
        $saveUserResult,
6834
        $allowSignature = false,
6835
        $allowExportPdf = false
6836
    ) {
6837
        if (api_get_configuration_value('hide_user_info_in_quiz_result')) {
6838
            return '';
6839
        }
6840
6841
        $start_date = null;
6842
        if (isset($trackExerciseInfo['start_date'])) {
6843
            $start_date = api_convert_and_format_date($trackExerciseInfo['start_date']);
6844
        }
6845
        $duration = isset($trackExerciseInfo['duration_formatted']) ? $trackExerciseInfo['duration_formatted'] : null;
6846
        $ip = isset($trackExerciseInfo['user_ip']) ? $trackExerciseInfo['user_ip'] : null;
6847
6848
        if (!empty($user_data)) {
6849
            $userFullName = $user_data['complete_name'];
6850
            if (api_is_teacher() || api_is_platform_admin(true, true)) {
6851
                $userFullName = '<a href="'.$user_data['profile_url'].'" title="'.get_lang('GoToStudentDetails').'">'.
6852
                    $user_data['complete_name'].'</a>';
6853
            }
6854
6855
            $data = [
6856
                'name_url' => $userFullName,
6857
                'complete_name' => $user_data['complete_name'],
6858
                'username' => $user_data['username'],
6859
                'avatar' => $user_data['avatar_medium'],
6860
                'url' => $user_data['profile_url'],
6861
            ];
6862
6863
            if (!empty($user_data['official_code'])) {
6864
                $data['code'] = $user_data['official_code'];
6865
            }
6866
        }
6867
        // Description can be very long and is generally meant to explain
6868
        //   rules *before* the exam. Leaving here to make display easier if
6869
        //   necessary
6870
        /*
6871
        if (!empty($this->description)) {
6872
            $array[] = array('title' => get_lang("Description"), 'content' => $this->description);
6873
        }
6874
        */
6875
        if (!empty($start_date)) {
6876
            $data['start_date'] = $start_date;
6877
        }
6878
6879
        if (!empty($duration)) {
6880
            $data['duration'] = $duration;
6881
        }
6882
6883
        if (!empty($ip)) {
6884
            $data['ip'] = $ip;
6885
        }
6886
6887
        if (true === api_get_configuration_value('exercise_hide_ip')) {
6888
            unset($data['ip']);
6889
        }
6890
6891
        if (api_get_configuration_value('save_titles_as_html')) {
6892
            $data['title'] = Security::remove_XSS($this->get_formated_title()).get_lang('Result');
6893
        } else {
6894
            $data['title'] = PHP_EOL.Security::remove_XSS($this->exercise).' : '.get_lang('Result');
6895
        }
6896
6897
        $questionsCount = count(explode(',', $trackExerciseInfo['data_tracking']));
6898
        $savedAnswersCount = $this->countUserAnswersSavedInExercise($trackExerciseInfo['exe_id']);
6899
6900
        $data['number_of_answers'] = $questionsCount;
6901
        $data['number_of_answers_saved'] = $savedAnswersCount;
6902
        $exeId = $trackExerciseInfo['exe_id'];
6903
6904
        if (false !== api_get_configuration_value('quiz_confirm_saved_answers')) {
6905
            $em = Database::getManager();
6906
6907
            if ($saveUserResult) {
6908
                $trackConfirmation = new TrackEExerciseConfirmation();
6909
                $trackConfirmation
6910
                    ->setUserId($trackExerciseInfo['exe_user_id'])
6911
                    ->setQuizId($trackExerciseInfo['exe_exo_id'])
6912
                    ->setAttemptId($trackExerciseInfo['exe_id'])
6913
                    ->setQuestionsCount($questionsCount)
6914
                    ->setSavedAnswersCount($savedAnswersCount)
6915
                    ->setCourseId($trackExerciseInfo['c_id'])
6916
                    ->setSessionId($trackExerciseInfo['session_id'])
6917
                    ->setCreatedAt(api_get_utc_datetime(null, false, true));
6918
6919
                $em->persist($trackConfirmation);
6920
                $em->flush();
6921
            } else {
6922
                $trackConfirmation = $em
6923
                    ->getRepository('ChamiloCoreBundle:TrackEExerciseConfirmation')
6924
                    ->findOneBy(
6925
                        [
6926
                            'attemptId' => $trackExerciseInfo['exe_id'],
6927
                            'quizId' => $trackExerciseInfo['exe_exo_id'],
6928
                            'courseId' => $trackExerciseInfo['c_id'],
6929
                            'sessionId' => $trackExerciseInfo['session_id'],
6930
                        ]
6931
                    );
6932
            }
6933
6934
            $data['track_confirmation'] = $trackConfirmation;
6935
        }
6936
6937
        $signature = '';
6938
        if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($this)) {
6939
            $signature = ExerciseSignaturePlugin::getSignature($trackExerciseInfo['exe_user_id'], $trackExerciseInfo);
6940
        }
6941
6942
        $tpl = new Template(null, false, false, false, false, false, false);
6943
        $tpl->assign('data', $data);
6944
        $tpl->assign('allow_signature', $allowSignature);
6945
        $tpl->assign('signature', $signature);
6946
        $tpl->assign('allow_export_pdf', $allowExportPdf);
6947
        $tpl->assign('export_url', api_get_path(WEB_CODE_PATH).'exercise/result.php?action=export&id='.$exeId.'&'.api_get_cidreq());
6948
6949
        $layoutTemplate = $tpl->get_template('exercise/partials/result_exercise.tpl');
6950
6951
        return $tpl->fetch($layoutTemplate);
6952
    }
6953
6954
    /**
6955
     * Returns the exercise result.
6956
     *
6957
     * @param 	int		attempt id
6958
     *
6959
     * @return array
6960
     */
6961
    public function get_exercise_result($exe_id)
6962
    {
6963
        $result = [];
6964
        $track_exercise_info = ExerciseLib::get_exercise_track_exercise_info($exe_id);
6965
6966
        if (!empty($track_exercise_info)) {
6967
            $totalScore = 0;
6968
            $objExercise = new self();
6969
            $objExercise->read($track_exercise_info['exe_exo_id']);
6970
            if (!empty($track_exercise_info['data_tracking'])) {
6971
                $question_list = explode(',', $track_exercise_info['data_tracking']);
6972
            }
6973
            foreach ($question_list as $questionId) {
6974
                $question_result = $objExercise->manage_answer(
6975
                    $exe_id,
6976
                    $questionId,
6977
                    '',
6978
                    'exercise_show',
6979
                    [],
6980
                    false,
6981
                    true,
6982
                    false,
6983
                    $objExercise->selectPropagateNeg()
6984
                );
6985
                $totalScore += $question_result['score'];
6986
            }
6987
6988
            if ($objExercise->selectPropagateNeg() == 0 && $totalScore < 0) {
6989
                $totalScore = 0;
6990
            }
6991
            $result = [
6992
                'score' => $totalScore,
6993
                'weight' => $track_exercise_info['exe_weighting'],
6994
            ];
6995
        }
6996
6997
        return $result;
6998
    }
6999
7000
    /**
7001
     * Checks if the exercise is visible due a lot of conditions
7002
     * visibility, time limits, student attempts
7003
     * Return associative array
7004
     * value : true if exercise visible
7005
     * message : HTML formatted message
7006
     * rawMessage : text message.
7007
     *
7008
     * @param int  $lpId
7009
     * @param int  $lpItemId
7010
     * @param int  $lpItemViewId
7011
     * @param bool $filterByAdmin
7012
     *
7013
     * @return array
7014
     */
7015
    public function is_visible(
7016
        $lpId = 0,
7017
        $lpItemId = 0,
7018
        $lpItemViewId = 0,
7019
        $filterByAdmin = true,
7020
        $sessionId = 0
7021
    ) {
7022
        $sessionId = (int) $sessionId;
7023
        if ($sessionId == 0) {
7024
            $sessionId = $this->sessionId;
7025
        }
7026
        // 1. By default the exercise is visible
7027
        $isVisible = true;
7028
        $message = null;
7029
7030
        // 1.1 Admins, teachers and tutors can access to the exercise
7031
        if ($filterByAdmin) {
7032
            if (api_is_platform_admin() || api_is_course_admin() || api_is_course_tutor() || api_is_session_general_coach()) {
7033
                return ['value' => true, 'message' => ''];
7034
            }
7035
        }
7036
7037
        // Deleted exercise.
7038
        if ($this->active == -1) {
7039
            return [
7040
                'value' => false,
7041
                'message' => Display::return_message(
7042
                    get_lang('ExerciseNotFound'),
7043
                    'warning',
7044
                    false
7045
                ),
7046
                'rawMessage' => get_lang('ExerciseNotFound'),
7047
            ];
7048
        }
7049
7050
        // Checking visibility in the item_property table.
7051
        $visibility = api_get_item_visibility(
7052
            api_get_course_info(),
7053
            TOOL_QUIZ,
7054
            $this->iid,
7055
            api_get_session_id()
7056
        );
7057
7058
        if ($visibility == 0 || $visibility == 2) {
7059
            $this->active = 0;
7060
        }
7061
7062
        // 2. If the exercise is not active.
7063
        if (empty($lpId)) {
7064
            // 2.1 LP is OFF
7065
            if ($this->active == 0) {
7066
                return [
7067
                    'value' => false,
7068
                    'message' => Display::return_message(
7069
                        get_lang('ExerciseNotFound'),
7070
                        'warning',
7071
                        false
7072
                    ),
7073
                    'rawMessage' => get_lang('ExerciseNotFound'),
7074
                ];
7075
            }
7076
        } else {
7077
            // 2.1 LP is loaded
7078
            if ($this->active == 0 &&
7079
                !learnpath::is_lp_visible_for_student($lpId, api_get_user_id())
7080
            ) {
7081
                return [
7082
                    'value' => false,
7083
                    'message' => Display::return_message(
7084
                        get_lang('ExerciseNotFound'),
7085
                        'warning',
7086
                        false
7087
                    ),
7088
                    'rawMessage' => get_lang('ExerciseNotFound'),
7089
                ];
7090
            }
7091
        }
7092
7093
        // 3. We check if the time limits are on
7094
        $limitTimeExists = false;
7095
        if (!empty($this->start_time) || !empty($this->end_time)) {
7096
            $limitTimeExists = true;
7097
        }
7098
7099
        if ($limitTimeExists) {
7100
            $timeNow = time();
7101
            $existsStartDate = false;
7102
            $nowIsAfterStartDate = true;
7103
            $existsEndDate = false;
7104
            $nowIsBeforeEndDate = true;
7105
7106
            if (!empty($this->start_time)) {
7107
                $existsStartDate = true;
7108
            }
7109
7110
            if (!empty($this->end_time)) {
7111
                $existsEndDate = true;
7112
            }
7113
7114
            // check if we are before-or-after end-or-start date
7115
            if ($existsStartDate && $timeNow < api_strtotime($this->start_time, 'UTC')) {
7116
                $nowIsAfterStartDate = false;
7117
            }
7118
7119
            if ($existsEndDate & $timeNow >= api_strtotime($this->end_time, 'UTC')) {
7120
                $nowIsBeforeEndDate = false;
7121
            }
7122
7123
            // lets check all cases
7124
            if ($existsStartDate && !$existsEndDate) {
7125
                // exists start date and dont exists end date
7126
                if ($nowIsAfterStartDate) {
7127
                    // after start date, no end date
7128
                    $isVisible = true;
7129
                    $message = sprintf(
7130
                        get_lang('ExerciseAvailableSinceX'),
7131
                        api_convert_and_format_date($this->start_time)
7132
                    );
7133
                } else {
7134
                    // before start date, no end date
7135
                    $isVisible = false;
7136
                    $message = sprintf(
7137
                        get_lang('ExerciseAvailableFromX'),
7138
                        api_convert_and_format_date($this->start_time)
7139
                    );
7140
                }
7141
            } elseif (!$existsStartDate && $existsEndDate) {
7142
                // doesnt exist start date, exists end date
7143
                if ($nowIsBeforeEndDate) {
7144
                    // before end date, no start date
7145
                    $isVisible = true;
7146
                    $message = sprintf(
7147
                        get_lang('ExerciseAvailableUntilX'),
7148
                        api_convert_and_format_date($this->end_time)
7149
                    );
7150
                } else {
7151
                    // after end date, no start date
7152
                    $isVisible = false;
7153
                    $message = sprintf(
7154
                        get_lang('ExerciseAvailableUntilX'),
7155
                        api_convert_and_format_date($this->end_time)
7156
                    );
7157
                }
7158
            } elseif ($existsStartDate && $existsEndDate) {
7159
                // exists start date and end date
7160
                if ($nowIsAfterStartDate) {
7161
                    if ($nowIsBeforeEndDate) {
7162
                        // after start date and before end date
7163
                        $isVisible = true;
7164
                        $message = sprintf(
7165
                            get_lang('ExerciseIsActivatedFromXToY'),
7166
                            api_convert_and_format_date($this->start_time),
7167
                            api_convert_and_format_date($this->end_time)
7168
                        );
7169
                    } else {
7170
                        // after start date and after end date
7171
                        $isVisible = false;
7172
                        $message = sprintf(
7173
                            get_lang('ExerciseWasActivatedFromXToY'),
7174
                            api_convert_and_format_date($this->start_time),
7175
                            api_convert_and_format_date($this->end_time)
7176
                        );
7177
                    }
7178
                } else {
7179
                    if ($nowIsBeforeEndDate) {
7180
                        // before start date and before end date
7181
                        $isVisible = false;
7182
                        $message = sprintf(
7183
                            get_lang('ExerciseWillBeActivatedFromXToY'),
7184
                            api_convert_and_format_date($this->start_time),
7185
                            api_convert_and_format_date($this->end_time)
7186
                        );
7187
                    }
7188
                    // case before start date and after end date is impossible
7189
                }
7190
            } elseif (!$existsStartDate && !$existsEndDate) {
7191
                // doesnt exist start date nor end date
7192
                $isVisible = true;
7193
                $message = '';
7194
            }
7195
        }
7196
7197
        $remedialCoursePlugin = RemedialCoursePlugin::create();
7198
7199
        // BT#18165
7200
        $exerciseAttempts = $this->selectAttempts();
7201
        if ($exerciseAttempts > 0) {
7202
            $userId = api_get_user_id();
7203
            $attemptCount = Event::get_attempt_count_not_finished(
7204
                $userId,
7205
                $this->iid,
7206
                $lpId,
7207
                $lpItemId,
7208
                $lpItemViewId
7209
            );
7210
            $message .= $remedialCoursePlugin->getAdvancedCourseList(
7211
                $this,
7212
                $userId,
7213
                api_get_session_id(),
7214
                $lpId ?: 0,
7215
                $lpItemId ?: 0
7216
            );
7217
            if ($attemptCount >= $exerciseAttempts) {
7218
                $message .= $remedialCoursePlugin->getRemedialCourseList(
7219
                    $this,
7220
                    $userId,
7221
                    api_get_session_id(),
7222
                    false,
7223
                    $lpId ?: 0,
7224
                    $lpItemId ?: 0
7225
                );
7226
            }
7227
        }
7228
        // 4. We check if the student have attempts
7229
        if ($isVisible) {
7230
            $exerciseAttempts = $this->selectAttempts();
7231
7232
            if ($exerciseAttempts > 0) {
7233
                $attemptCount = Event::get_attempt_count_not_finished(
7234
                    api_get_user_id(),
7235
                    $this->iid,
7236
                    $lpId,
7237
                    $lpItemId,
7238
                    $lpItemViewId
7239
                );
7240
7241
                if ($attemptCount >= $exerciseAttempts) {
7242
                    $message = sprintf(
7243
                        get_lang('ReachedMaxAttempts'),
7244
                        $this->name,
7245
                        $exerciseAttempts
7246
                    );
7247
                    $isVisible = false;
7248
                } else {
7249
                    // Check blocking exercise.
7250
                    $extraFieldValue = new ExtraFieldValue('exercise');
7251
                    $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
7252
                        $this->iid,
7253
                        'blocking_percentage'
7254
                    );
7255
                    if ($blockExercise && isset($blockExercise['value']) && !empty($blockExercise['value'])) {
7256
                        $blockPercentage = (int) $blockExercise['value'];
7257
                        $userAttempts = Event::getExerciseResultsByUser(
7258
                            api_get_user_id(),
7259
                            $this->iid,
7260
                            $this->course_id,
7261
                            $sessionId,
7262
                            $lpId,
7263
                            $lpItemId
7264
                        );
7265
7266
                        if (!empty($userAttempts)) {
7267
                            $currentAttempt = current($userAttempts);
7268
                            if ($currentAttempt['total_percentage'] <= $blockPercentage) {
7269
                                $message = sprintf(
7270
                                    get_lang('ExerciseBlockBecausePercentageX'),
7271
                                    $blockPercentage
7272
                                );
7273
                                $isVisible = false; // See BT#18165
7274
                                $message .= $remedialCoursePlugin->getRemedialCourseList(
7275
                                    $this,
7276
                                    api_get_user_id(),
7277
                                    api_get_session_id(),
7278
                                    false,
7279
                                    $lpId,
7280
                                    $lpItemId
7281
                                );
7282
                            }
7283
                        }
7284
                    }
7285
                }
7286
            }
7287
        }
7288
7289
        $rawMessage = '';
7290
        if (!empty($message)) {
7291
            $rawMessage = $message;
7292
            $message = Display::return_message($message, 'warning', false);
7293
        }
7294
7295
        return [
7296
            'value' => $isVisible,
7297
            'message' => $message,
7298
            'rawMessage' => $rawMessage,
7299
        ];
7300
    }
7301
7302
    /**
7303
     * @return bool
7304
     */
7305
    public function added_in_lp()
7306
    {
7307
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
7308
        $sql = "SELECT max_score FROM $TBL_LP_ITEM
7309
                WHERE
7310
                    c_id = {$this->course_id} AND
7311
                    item_type = '".TOOL_QUIZ."' AND
7312
                    path = '{$this->iid}'";
7313
        $result = Database::query($sql);
7314
        if (Database::num_rows($result) > 0) {
7315
            return true;
7316
        }
7317
7318
        return false;
7319
    }
7320
7321
    /**
7322
     * Returns an array with this form.
7323
     *
7324
     * @example
7325
     * <code>
7326
     * array (size=3)
7327
     * 999 =>
7328
     * array (size=3)
7329
     * 0 => int 3422
7330
     * 1 => int 3423
7331
     * 2 => int 3424
7332
     * 100 =>
7333
     * array (size=2)
7334
     * 0 => int 3469
7335
     * 1 => int 3470
7336
     * 101 =>
7337
     * array (size=1)
7338
     * 0 => int 3482
7339
     * </code>
7340
     * The array inside the key 999 means the question list that belongs to the media id = 999,
7341
     * this case is special because 999 means "no media".
7342
     *
7343
     * @return array
7344
     */
7345
    public function getMediaList()
7346
    {
7347
        return $this->mediaList;
7348
    }
7349
7350
    /**
7351
     * Is media question activated?
7352
     *
7353
     * @return bool
7354
     */
7355
    public function mediaIsActivated()
7356
    {
7357
        $mediaQuestions = $this->getMediaList();
7358
        $active = false;
7359
        if (isset($mediaQuestions) && !empty($mediaQuestions)) {
7360
            $media_count = count($mediaQuestions);
7361
            if ($media_count > 1) {
7362
                return true;
7363
            } elseif ($media_count == 1) {
7364
                if (isset($mediaQuestions[999])) {
7365
                    return false;
7366
                } else {
7367
                    return true;
7368
                }
7369
            }
7370
        }
7371
7372
        return $active;
7373
    }
7374
7375
    /**
7376
     * Gets question list from the exercise.
7377
     *
7378
     * @return array
7379
     */
7380
    public function getQuestionList()
7381
    {
7382
        return $this->questionList;
7383
    }
7384
7385
    /**
7386
     * Question list with medias compressed like this.
7387
     *
7388
     * @example
7389
     * <code>
7390
     * array(
7391
     *      question_id_1,
7392
     *      question_id_2,
7393
     *      media_id, <- this media id contains question ids
7394
     *      question_id_3,
7395
     * )
7396
     * </code>
7397
     *
7398
     * @return array
7399
     */
7400
    public function getQuestionListWithMediasCompressed()
7401
    {
7402
        return $this->questionList;
7403
    }
7404
7405
    /**
7406
     * Question list with medias uncompressed like this.
7407
     *
7408
     * @example
7409
     * <code>
7410
     * array(
7411
     *      question_id,
7412
     *      question_id,
7413
     *      question_id, <- belongs to a media id
7414
     *      question_id, <- belongs to a media id
7415
     *      question_id,
7416
     * )
7417
     * </code>
7418
     *
7419
     * @return array
7420
     */
7421
    public function getQuestionListWithMediasUncompressed()
7422
    {
7423
        return $this->questionListUncompressed;
7424
    }
7425
7426
    /**
7427
     * Sets the question list when the exercise->read() is executed.
7428
     *
7429
     * @param bool $adminView Whether to view the set the list of *all* questions or just the normal student view
7430
     */
7431
    public function setQuestionList($adminView = false)
7432
    {
7433
        // Getting question list.
7434
        $questionList = $this->selectQuestionList(true, $adminView);
7435
        $this->setMediaList($questionList);
7436
        $this->questionList = $this->transformQuestionListWithMedias($questionList, false);
7437
        $this->questionListUncompressed = $this->transformQuestionListWithMedias(
7438
            $questionList,
7439
            true
7440
        );
7441
    }
7442
7443
    /**
7444
     * @params array question list
7445
     * @params bool expand or not question list (true show all questions,
7446
     * false show media question id instead of the question ids)
7447
     */
7448
    public function transformQuestionListWithMedias(
7449
        $question_list,
7450
        $expand_media_questions = false
7451
    ) {
7452
        $new_question_list = [];
7453
        if (!empty($question_list)) {
7454
            $media_questions = $this->getMediaList();
7455
            $media_active = $this->mediaIsActivated($media_questions);
7456
7457
            if ($media_active) {
7458
                $counter = 1;
7459
                foreach ($question_list as $question_id) {
7460
                    $add_question = true;
7461
                    foreach ($media_questions as $media_id => $question_list_in_media) {
7462
                        if ($media_id != 999 && in_array($question_id, $question_list_in_media)) {
7463
                            $add_question = false;
7464
                            if (!in_array($media_id, $new_question_list)) {
7465
                                $new_question_list[$counter] = $media_id;
7466
                                $counter++;
7467
                            }
7468
                            break;
7469
                        }
7470
                    }
7471
                    if ($add_question) {
7472
                        $new_question_list[$counter] = $question_id;
7473
                        $counter++;
7474
                    }
7475
                }
7476
                if ($expand_media_questions) {
7477
                    $media_key_list = array_keys($media_questions);
7478
                    foreach ($new_question_list as &$question_id) {
7479
                        if (in_array($question_id, $media_key_list)) {
7480
                            $question_id = $media_questions[$question_id];
7481
                        }
7482
                    }
7483
                    $new_question_list = array_flatten($new_question_list);
7484
                }
7485
            } else {
7486
                $new_question_list = $question_list;
7487
            }
7488
        }
7489
7490
        return $new_question_list;
7491
    }
7492
7493
    /**
7494
     * Get sorted question list based on the random order settings.
7495
     *
7496
     * @return array
7497
     */
7498
    public function get_validated_question_list()
7499
    {
7500
        $isRandomByCategory = $this->isRandomByCat();
7501
        if ($isRandomByCategory == 0) {
7502
            if ($this->isRandom()) {
7503
                return $this->getRandomList();
7504
            }
7505
7506
            return $this->selectQuestionList();
7507
        }
7508
7509
        if ($this->isRandom()) {
7510
            // USE question categories
7511
            // get questions by category for this exercise
7512
            // we have to choice $objExercise->random question in each array values of $categoryQuestions
7513
            // key of $categoryQuestions are the categopy id (0 for not in a category)
7514
            // value is the array of question id of this category
7515
            $questionList = [];
7516
            $categoryQuestions = TestCategory::getQuestionsByCat($this->iid);
7517
            $isRandomByCategory = $this->getRandomByCategory();
7518
            // We sort categories based on the term between [] in the head
7519
            // of the category's description
7520
            /* examples of categories :
7521
             * [biologie] Maitriser les mecanismes de base de la genetique
7522
             * [biologie] Relier les moyens de depenses et les agents infectieux
7523
             * [biologie] Savoir ou est produite l'enrgie dans les cellules et sous quelle forme
7524
             * [chimie] Classer les molles suivant leur pouvoir oxydant ou reacteur
7525
             * [chimie] Connaître la denition de la theoie acide/base selon Brönsted
7526
             * [chimie] Connaître les charges des particules
7527
             * We want that in the order of the groups defined by the term
7528
             * between brackets at the beginning of the category title
7529
            */
7530
            // If test option is Grouped By Categories
7531
            if ($isRandomByCategory == 2) {
7532
                $categoryQuestions = TestCategory::sortTabByBracketLabel($categoryQuestions);
7533
            }
7534
            foreach ($categoryQuestions as $question) {
7535
                $number_of_random_question = $this->random;
7536
                if ($this->random == -1) {
7537
                    $number_of_random_question = count($this->questionList);
7538
                }
7539
                $questionList = array_merge(
7540
                    $questionList,
7541
                    TestCategory::getNElementsFromArray(
7542
                        $question,
7543
                        $number_of_random_question
7544
                    )
7545
                );
7546
            }
7547
            // shuffle the question list if test is not grouped by categories
7548
            if ($isRandomByCategory == 1) {
7549
                shuffle($questionList); // or not
7550
            }
7551
7552
            return $questionList;
7553
        }
7554
7555
        // Problem, random by category has been selected and
7556
        // we have no $this->isRandom number of question selected
7557
        // Should not happened
7558
7559
        return [];
7560
    }
7561
7562
    public function get_question_list($expand_media_questions = false)
7563
    {
7564
        $question_list = $this->get_validated_question_list();
7565
        $question_list = $this->transform_question_list_with_medias($question_list, $expand_media_questions);
7566
7567
        return $question_list;
7568
    }
7569
7570
    public function transform_question_list_with_medias($question_list, $expand_media_questions = false)
7571
    {
7572
        $new_question_list = [];
7573
        if (!empty($question_list)) {
7574
            $media_questions = $this->getMediaList();
7575
            $media_active = $this->mediaIsActivated($media_questions);
7576
7577
            if ($media_active) {
7578
                $counter = 1;
7579
                foreach ($question_list as $question_id) {
7580
                    $add_question = true;
7581
                    foreach ($media_questions as $media_id => $question_list_in_media) {
7582
                        if ($media_id != 999 && in_array($question_id, $question_list_in_media)) {
7583
                            $add_question = false;
7584
                            if (!in_array($media_id, $new_question_list)) {
7585
                                $new_question_list[$counter] = $media_id;
7586
                                $counter++;
7587
                            }
7588
                            break;
7589
                        }
7590
                    }
7591
                    if ($add_question) {
7592
                        $new_question_list[$counter] = $question_id;
7593
                        $counter++;
7594
                    }
7595
                }
7596
                if ($expand_media_questions) {
7597
                    $media_key_list = array_keys($media_questions);
7598
                    foreach ($new_question_list as &$question_id) {
7599
                        if (in_array($question_id, $media_key_list)) {
7600
                            $question_id = $media_questions[$question_id];
7601
                        }
7602
                    }
7603
                    $new_question_list = array_flatten($new_question_list);
7604
                }
7605
            } else {
7606
                $new_question_list = $question_list;
7607
            }
7608
        }
7609
7610
        return $new_question_list;
7611
    }
7612
7613
    /**
7614
     * @param int $exe_id
7615
     *
7616
     * @return array
7617
     */
7618
    public function get_stat_track_exercise_info_by_exe_id($exe_id)
7619
    {
7620
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7621
        $exe_id = (int) $exe_id;
7622
        $sql_track = "SELECT * FROM $table WHERE exe_id = $exe_id ";
7623
        $result = Database::query($sql_track);
7624
        $new_array = [];
7625
        if (Database::num_rows($result) > 0) {
7626
            $new_array = Database::fetch_array($result, 'ASSOC');
7627
            $start_date = api_get_utc_datetime($new_array['start_date'], true);
7628
            $end_date = api_get_utc_datetime($new_array['exe_date'], true);
7629
            $new_array['duration_formatted'] = '';
7630
            if (!empty($new_array['exe_duration']) && !empty($start_date) && !empty($end_date)) {
7631
                $time = api_format_time($new_array['exe_duration'], 'js');
7632
                $new_array['duration_formatted'] = $time;
7633
            }
7634
        }
7635
7636
        return $new_array;
7637
    }
7638
7639
    /**
7640
     * @param int $exeId
7641
     *
7642
     * @return bool
7643
     */
7644
    public function removeAllQuestionToRemind($exeId)
7645
    {
7646
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7647
        $exeId = (int) $exeId;
7648
        if (empty($exeId)) {
7649
            return false;
7650
        }
7651
        $sql = "UPDATE $table
7652
                SET questions_to_check = ''
7653
                WHERE exe_id = $exeId ";
7654
        Database::query($sql);
7655
7656
        return true;
7657
    }
7658
7659
    /**
7660
     * @param int   $exeId
7661
     * @param array $questionList
7662
     *
7663
     * @return bool
7664
     */
7665
    public function addAllQuestionToRemind($exeId, $questionList = [])
7666
    {
7667
        $exeId = (int) $exeId;
7668
        if (empty($questionList)) {
7669
            return false;
7670
        }
7671
7672
        $questionListToString = implode(',', $questionList);
7673
        $questionListToString = Database::escape_string($questionListToString);
7674
7675
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7676
        $sql = "UPDATE $table
7677
                SET questions_to_check = '$questionListToString'
7678
                WHERE exe_id = $exeId";
7679
        Database::query($sql);
7680
7681
        return true;
7682
    }
7683
7684
    /**
7685
     * @param int    $exeId
7686
     * @param int    $questionId
7687
     * @param string $action
7688
     */
7689
    public function editQuestionToRemind($exeId, $questionId, $action = 'add')
7690
    {
7691
        $exercise_info = self::get_stat_track_exercise_info_by_exe_id($exeId);
7692
        $questionId = (int) $questionId;
7693
        $exeId = (int) $exeId;
7694
7695
        if ($exercise_info) {
7696
            $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7697
            if (empty($exercise_info['questions_to_check'])) {
7698
                if ($action === 'add') {
7699
                    $sql = "UPDATE $track_exercises
7700
                            SET questions_to_check = '$questionId'
7701
                            WHERE exe_id = $exeId ";
7702
                    Database::query($sql);
7703
                }
7704
            } else {
7705
                $remind_list = explode(',', $exercise_info['questions_to_check']);
7706
                $remind_list_string = '';
7707
                if ($action === 'add') {
7708
                    if (!in_array($questionId, $remind_list)) {
7709
                        $newRemindList = [];
7710
                        $remind_list[] = $questionId;
7711
                        $questionListInSession = Session::read('questionList');
7712
                        if (!empty($questionListInSession)) {
7713
                            foreach ($questionListInSession as $originalQuestionId) {
7714
                                if (in_array($originalQuestionId, $remind_list)) {
7715
                                    $newRemindList[] = $originalQuestionId;
7716
                                }
7717
                            }
7718
                        }
7719
                        $remind_list_string = implode(',', $newRemindList);
7720
                    }
7721
                } elseif ($action === 'delete') {
7722
                    if (!empty($remind_list)) {
7723
                        if (in_array($questionId, $remind_list)) {
7724
                            $remind_list = array_flip($remind_list);
7725
                            unset($remind_list[$questionId]);
7726
                            $remind_list = array_flip($remind_list);
7727
7728
                            if (!empty($remind_list)) {
7729
                                sort($remind_list);
7730
                                array_filter($remind_list);
7731
                                $remind_list_string = implode(',', $remind_list);
7732
                            }
7733
                        }
7734
                    }
7735
                }
7736
                $value = Database::escape_string($remind_list_string);
7737
                $sql = "UPDATE $track_exercises
7738
                        SET questions_to_check = '$value'
7739
                        WHERE exe_id = $exeId ";
7740
                Database::query($sql);
7741
            }
7742
        }
7743
    }
7744
7745
    /**
7746
     * @param string $answer
7747
     *
7748
     * @return mixed
7749
     */
7750
    public function fill_in_blank_answer_to_array($answer)
7751
    {
7752
        $listStudentResults = FillBlanks::getAnswerInfo(
7753
            $answer,
7754
            true
7755
        );
7756
        $teacherAnswerList = $listStudentResults['student_answer'];
7757
7758
        return $teacherAnswerList;
7759
    }
7760
7761
    /**
7762
     * @param string $answer
7763
     *
7764
     * @return string
7765
     */
7766
    public function fill_in_blank_answer_to_string($answer)
7767
    {
7768
        $teacher_answer_list = $this->fill_in_blank_answer_to_array($answer);
7769
        $result = '';
7770
        if (!empty($teacher_answer_list)) {
7771
            foreach ($teacher_answer_list as $teacher_item) {
7772
                // Cleaning student answer list
7773
                $value = strip_tags($teacher_item);
7774
                if (strlen($value) > 2 && false !== strpos($value, '/')) {
7775
                    $value = api_substr($value, 1, api_strlen($value) - 2);
7776
                    $value = explode('/', $value);
7777
                    if (!empty($value[0])) {
7778
                        $value = trim($value[0]);
7779
                        $value = str_replace('&nbsp;', '', $value);
7780
                        $result .= $value;
7781
                    }
7782
                } else {
7783
                    $result .= $value;
7784
                }
7785
            }
7786
        }
7787
7788
        return $result;
7789
    }
7790
7791
    /**
7792
     * @return string
7793
     */
7794
    public function returnTimeLeftDiv()
7795
    {
7796
        $html = '<div id="clock_warning" style="display:none">';
7797
        $html .= Display::return_message(
7798
            get_lang('ReachedTimeLimit'),
7799
            'warning'
7800
        );
7801
        $html .= ' ';
7802
        $html .= sprintf(
7803
            get_lang('YouWillBeRedirectedInXSeconds'),
7804
            '<span id="counter_to_redirect" class="red_alert"></span>'
7805
        );
7806
        $html .= '</div>';
7807
7808
        $icon = Display::returnFontAwesomeIcon('clock-o');
7809
        $html .= '<div class="count_down">
7810
                    '.get_lang('RemainingTimeToFinishExercise').'
7811
                    '.$icon.'<span id="exercise_clock_warning"></span>
7812
                </div>';
7813
7814
        return $html;
7815
    }
7816
7817
    /**
7818
     * Get categories added in the exercise--category matrix.
7819
     *
7820
     * @return array
7821
     */
7822
    public function getCategoriesInExercise()
7823
    {
7824
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7825
        if (!empty($this->iid)) {
7826
            $sql = "SELECT * FROM $table
7827
                    WHERE exercise_id = {$this->iid} AND c_id = {$this->course_id} ";
7828
            $result = Database::query($sql);
7829
            $list = [];
7830
            if (Database::num_rows($result)) {
7831
                while ($row = Database::fetch_array($result, 'ASSOC')) {
7832
                    $list[$row['category_id']] = $row;
7833
                }
7834
7835
                return $list;
7836
            }
7837
        }
7838
7839
        return [];
7840
    }
7841
7842
    /**
7843
     * Get total number of question that will be parsed when using the category/exercise.
7844
     *
7845
     * @return int
7846
     */
7847
    public function getNumberQuestionExerciseCategory()
7848
    {
7849
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7850
        if (!empty($this->iid)) {
7851
            $sql = "SELECT SUM(count_questions) count_questions
7852
                    FROM $table
7853
                    WHERE exercise_id = {$this->iid} AND c_id = {$this->course_id}";
7854
            $result = Database::query($sql);
7855
            if (Database::num_rows($result)) {
7856
                $row = Database::fetch_array($result);
7857
7858
                return (int) $row['count_questions'];
7859
            }
7860
        }
7861
7862
        return 0;
7863
    }
7864
7865
    /**
7866
     * Save categories in the TABLE_QUIZ_REL_CATEGORY table.
7867
     *
7868
     * @param array $categories
7869
     */
7870
    public function save_categories_in_exercise($categories)
7871
    {
7872
        if (!empty($categories) && !empty($this->iid)) {
7873
            $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7874
            $sql = "DELETE FROM $table
7875
                    WHERE exercise_id = {$this->iid} AND c_id = {$this->course_id}";
7876
            Database::query($sql);
7877
            if (!empty($categories)) {
7878
                foreach ($categories as $categoryId => $countQuestions) {
7879
                    $params = [
7880
                        'c_id' => $this->course_id,
7881
                        'exercise_id' => $this->iid,
7882
                        'category_id' => $categoryId,
7883
                        'count_questions' => $countQuestions,
7884
                    ];
7885
                    Database::insert($table, $params);
7886
                }
7887
            }
7888
        }
7889
    }
7890
7891
    /**
7892
     * @param array  $questionList
7893
     * @param int    $currentQuestion
7894
     * @param array  $conditions
7895
     * @param string $link
7896
     *
7897
     * @return string
7898
     */
7899
    public function progressExercisePaginationBar(
7900
        $questionList,
7901
        $currentQuestion,
7902
        $conditions,
7903
        $link
7904
    ) {
7905
        $mediaQuestions = $this->getMediaList();
7906
7907
        $html = '<div class="exercise_pagination pagination pagination-mini"><ul>';
7908
        $counter = 0;
7909
        $nextValue = 0;
7910
        $wasMedia = false;
7911
        $before = 0;
7912
        $counterNoMedias = 0;
7913
        foreach ($questionList as $questionId) {
7914
            $isCurrent = $currentQuestion == ($counterNoMedias + 1) ? true : false;
7915
7916
            if (!empty($nextValue)) {
7917
                if ($wasMedia) {
7918
                    $nextValue = $nextValue - $before + 1;
7919
                }
7920
            }
7921
7922
            if (isset($mediaQuestions) && isset($mediaQuestions[$questionId])) {
7923
                $fixedValue = $counterNoMedias;
7924
7925
                $html .= Display::progressPaginationBar(
7926
                    $nextValue,
7927
                    $mediaQuestions[$questionId],
7928
                    $currentQuestion,
7929
                    $fixedValue,
7930
                    $conditions,
7931
                    $link,
7932
                    true,
7933
                    true
7934
                );
7935
7936
                $counter += count($mediaQuestions[$questionId]) - 1;
7937
                $before = count($questionList);
7938
                $wasMedia = true;
7939
                $nextValue += count($questionList);
7940
            } else {
7941
                $html .= Display::parsePaginationItem(
7942
                    $questionId,
7943
                    $isCurrent,
7944
                    $conditions,
7945
                    $link,
7946
                    $counter
7947
                );
7948
                $counter++;
7949
                $nextValue++;
7950
                $wasMedia = false;
7951
            }
7952
            $counterNoMedias++;
7953
        }
7954
        $html .= '</ul></div>';
7955
7956
        return $html;
7957
    }
7958
7959
    /**
7960
     *  Shows a list of numbers that represents the question to answer in a exercise.
7961
     *
7962
     * @param array  $categories
7963
     * @param int    $current
7964
     * @param array  $conditions
7965
     * @param string $link
7966
     *
7967
     * @return string
7968
     */
7969
    public function progressExercisePaginationBarWithCategories(
7970
        $categories,
7971
        $current,
7972
        $conditions = [],
7973
        $link = null
7974
    ) {
7975
        $html = null;
7976
        $counterNoMedias = 0;
7977
        $nextValue = 0;
7978
        $wasMedia = false;
7979
        $before = 0;
7980
7981
        if (!empty($categories)) {
7982
            $selectionType = $this->getQuestionSelectionType();
7983
            $useRootAsCategoryTitle = false;
7984
7985
            // Grouping questions per parent category see BT#6540
7986
            if (in_array(
7987
                $selectionType,
7988
                [
7989
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED,
7990
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM,
7991
                ]
7992
            )) {
7993
                $useRootAsCategoryTitle = true;
7994
            }
7995
7996
            // If the exercise is set to only show the titles of the categories
7997
            // at the root of the tree, then pre-order the categories tree by
7998
            // removing children and summing their questions into the parent
7999
            // categories
8000
            if ($useRootAsCategoryTitle) {
8001
                // The new categories list starts empty
8002
                $newCategoryList = [];
8003
                foreach ($categories as $category) {
8004
                    $rootElement = $category['root'];
8005
8006
                    if (isset($category['parent_info'])) {
8007
                        $rootElement = $category['parent_info']['iid'];
8008
                    }
8009
8010
                    //$rootElement = $category['iid'];
8011
                    // If the current category's ancestor was never seen
8012
                    // before, then declare it and assign the current
8013
                    // category to it.
8014
                    if (!isset($newCategoryList[$rootElement])) {
8015
                        $newCategoryList[$rootElement] = $category;
8016
                    } else {
8017
                        // If it was already seen, then merge the previous with
8018
                        // the current category
8019
                        $oldQuestionList = $newCategoryList[$rootElement]['question_list'];
8020
                        $category['question_list'] = array_merge($oldQuestionList, $category['question_list']);
8021
                        $newCategoryList[$rootElement] = $category;
8022
                    }
8023
                }
8024
                // Now use the newly built categories list, with only parents
8025
                $categories = $newCategoryList;
8026
            }
8027
8028
            foreach ($categories as $category) {
8029
                $questionList = $category['question_list'];
8030
                // Check if in this category there questions added in a media
8031
                $mediaQuestionId = $category['media_question'];
8032
                $isMedia = false;
8033
                $fixedValue = null;
8034
8035
                // Media exists!
8036
                if ($mediaQuestionId != 999) {
8037
                    $isMedia = true;
8038
                    $fixedValue = $counterNoMedias;
8039
                }
8040
8041
                //$categoryName = $category['path']; << show the path
8042
                $categoryName = $category['name'];
8043
8044
                if ($useRootAsCategoryTitle) {
8045
                    if (isset($category['parent_info'])) {
8046
                        $categoryName = $category['parent_info']['title'];
8047
                    }
8048
                }
8049
                $html .= '<div class="row">';
8050
                $html .= '<div class="span2">'.$categoryName.'</div>';
8051
                $html .= '<div class="span8">';
8052
8053
                if (!empty($nextValue)) {
8054
                    if ($wasMedia) {
8055
                        $nextValue = $nextValue - $before + 1;
8056
                    }
8057
                }
8058
                $html .= Display::progressPaginationBar(
8059
                    $nextValue,
8060
                    $questionList,
8061
                    $current,
8062
                    $fixedValue,
8063
                    $conditions,
8064
                    $link,
8065
                    $isMedia,
8066
                    true
8067
                );
8068
                $html .= '</div>';
8069
                $html .= '</div>';
8070
8071
                if ($mediaQuestionId == 999) {
8072
                    $counterNoMedias += count($questionList);
8073
                } else {
8074
                    $counterNoMedias++;
8075
                }
8076
8077
                $nextValue += count($questionList);
8078
                $before = count($questionList);
8079
8080
                if ($mediaQuestionId != 999) {
8081
                    $wasMedia = true;
8082
                } else {
8083
                    $wasMedia = false;
8084
                }
8085
            }
8086
        }
8087
8088
        return $html;
8089
    }
8090
8091
    /**
8092
     * Renders a question list.
8093
     *
8094
     * @param array $questionList    (with media questions compressed)
8095
     * @param int   $currentQuestion
8096
     * @param array $exerciseResult
8097
     * @param array $attemptList
8098
     * @param array $remindList
8099
     */
8100
    public function renderQuestionList(
8101
        $questionList,
8102
        $currentQuestion,
8103
        $exerciseResult,
8104
        $attemptList,
8105
        $remindList
8106
    ) {
8107
        $mediaQuestions = $this->getMediaList();
8108
        $i = 0;
8109
8110
        // Normal question list render (medias compressed)
8111
        foreach ($questionList as $questionId) {
8112
            $i++;
8113
            // For sequential exercises
8114
8115
            if ($this->type == ONE_PER_PAGE) {
8116
                // If it is not the right question, goes to the next loop iteration
8117
                if ($currentQuestion != $i) {
8118
                    continue;
8119
                } else {
8120
                    if (!in_array(
8121
                        $this->getFeedbackType(),
8122
                        [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
8123
                    )) {
8124
                        // if the user has already answered this question
8125
                        if (isset($exerciseResult[$questionId])) {
8126
                            echo Display::return_message(
8127
                                get_lang('AlreadyAnswered'),
8128
                                'normal'
8129
                            );
8130
                            break;
8131
                        }
8132
                    }
8133
                }
8134
            }
8135
8136
            // The $questionList contains the media id we check
8137
            // if this questionId is a media question type
8138
            if (isset($mediaQuestions[$questionId]) &&
8139
                $mediaQuestions[$questionId] != 999
8140
            ) {
8141
                // The question belongs to a media
8142
                $mediaQuestionList = $mediaQuestions[$questionId];
8143
                $objQuestionTmp = Question::read($questionId);
8144
8145
                $counter = 1;
8146
                if ($objQuestionTmp->type == MEDIA_QUESTION) {
8147
                    echo $objQuestionTmp->show_media_content();
8148
8149
                    $countQuestionsInsideMedia = count($mediaQuestionList);
8150
8151
                    // Show questions that belongs to a media
8152
                    if (!empty($mediaQuestionList)) {
8153
                        // In order to parse media questions we use letters a, b, c, etc.
8154
                        $letterCounter = 97;
8155
                        foreach ($mediaQuestionList as $questionIdInsideMedia) {
8156
                            $isLastQuestionInMedia = false;
8157
                            if ($counter == $countQuestionsInsideMedia) {
8158
                                $isLastQuestionInMedia = true;
8159
                            }
8160
                            $this->renderQuestion(
8161
                                $questionIdInsideMedia,
8162
                                $attemptList,
8163
                                $remindList,
8164
                                chr($letterCounter),
8165
                                $currentQuestion,
8166
                                $mediaQuestionList,
8167
                                $isLastQuestionInMedia,
8168
                                $questionList
8169
                            );
8170
                            $letterCounter++;
8171
                            $counter++;
8172
                        }
8173
                    }
8174
                } else {
8175
                    $this->renderQuestion(
8176
                        $questionId,
8177
                        $attemptList,
8178
                        $remindList,
8179
                        $i,
8180
                        $currentQuestion,
8181
                        null,
8182
                        null,
8183
                        $questionList
8184
                    );
8185
                    $i++;
8186
                }
8187
            } else {
8188
                // Normal question render.
8189
                $this->renderQuestion(
8190
                    $questionId,
8191
                    $attemptList,
8192
                    $remindList,
8193
                    $i,
8194
                    $currentQuestion,
8195
                    null,
8196
                    null,
8197
                    $questionList
8198
                );
8199
            }
8200
8201
            // For sequential exercises.
8202
            if ($this->type == ONE_PER_PAGE) {
8203
                // quits the loop
8204
                break;
8205
            }
8206
        }
8207
        // end foreach()
8208
8209
        if ($this->type == ALL_ON_ONE_PAGE) {
8210
            $exercise_actions = $this->show_button($questionId, $currentQuestion);
8211
            echo Display::div($exercise_actions, ['class' => 'exercise_actions']);
8212
        }
8213
    }
8214
8215
    /**
8216
     * @param int   $questionId
8217
     * @param array $attemptList
8218
     * @param array $remindList
8219
     * @param int   $i
8220
     * @param int   $current_question
8221
     * @param array $questions_in_media
8222
     * @param bool  $last_question_in_media
8223
     * @param array $realQuestionList
8224
     * @param bool  $generateJS
8225
     */
8226
    public function renderQuestion(
8227
        $questionId,
8228
        $attemptList,
8229
        $remindList,
8230
        $i,
8231
        $current_question,
8232
        $questions_in_media = [],
8233
        $last_question_in_media = false,
8234
        $realQuestionList = [],
8235
        $generateJS = true
8236
    ) {
8237
        // With this option on the question is loaded via AJAX
8238
        //$generateJS = true;
8239
        //$this->loadQuestionAJAX = true;
8240
8241
        if ($generateJS && $this->loadQuestionAJAX) {
8242
            $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?a=get_question&id='.$questionId.'&'.api_get_cidreq();
8243
            $params = [
8244
                'questionId' => $questionId,
8245
                'attemptList' => $attemptList,
8246
                'remindList' => $remindList,
8247
                'i' => $i,
8248
                'current_question' => $current_question,
8249
                'questions_in_media' => $questions_in_media,
8250
                'last_question_in_media' => $last_question_in_media,
8251
            ];
8252
            $params = json_encode($params);
8253
8254
            $script = '<script>
8255
            $(function(){
8256
                var params = '.$params.';
8257
                $.ajax({
8258
                    type: "GET",
8259
                    data: params,
8260
                    url: "'.$url.'",
8261
                    success: function(return_value) {
8262
                        $("#ajaxquestiondiv'.$questionId.'").html(return_value);
8263
                    }
8264
                });
8265
            });
8266
            </script>
8267
            <div id="ajaxquestiondiv'.$questionId.'"></div>';
8268
            echo $script;
8269
        } else {
8270
            global $origin;
8271
            $question_obj = Question::read($questionId);
8272
            $user_choice = isset($attemptList[$questionId]) ? $attemptList[$questionId] : null;
8273
            $remind_highlight = null;
8274
8275
            // Hides questions when reviewing a ALL_ON_ONE_PAGE exercise
8276
            // see #4542 no_remind_highlight class hide with jquery
8277
            if ($this->type == ALL_ON_ONE_PAGE && isset($_GET['reminder']) && $_GET['reminder'] == 2) {
8278
                $remind_highlight = 'no_remind_highlight';
8279
                if (in_array($question_obj->type, Question::question_type_no_review())) {
8280
                    return null;
8281
                }
8282
            }
8283
8284
            $attributes = ['id' => 'remind_list['.$questionId.']', 'data-question-id' => $questionId];
8285
8286
            // Showing the question
8287
            $exercise_actions = null;
8288
            echo '<a id="questionanchor'.$questionId.'"></a><br />';
8289
            echo '<div id="question_div_'.$questionId.'" class="main_question '.$remind_highlight.'" >';
8290
8291
            // Shows the question + possible answers
8292
            $showTitle = $this->getHideQuestionTitle() == 1 ? false : true;
8293
            echo ExerciseLib::showQuestion(
8294
                $this,
8295
                $question_obj,
8296
                false,
8297
                $origin,
8298
                $i,
8299
                $showTitle,
8300
                false,
8301
                $user_choice
8302
            );
8303
8304
            // Button save and continue
8305
            switch ($this->type) {
8306
                case ONE_PER_PAGE:
8307
                    $exercise_actions .= $this->show_button(
8308
                        $questionId,
8309
                        $current_question,
8310
                        null,
8311
                        $remindList
8312
                    );
8313
                    break;
8314
                case ALL_ON_ONE_PAGE:
8315
                    if (api_is_allowed_to_session_edit()) {
8316
                        $button = [
8317
                            Display::button(
8318
                                'save_now',
8319
                                get_lang('SaveForNow'),
8320
                                ['type' => 'button', 'class' => 'btn btn-primary', 'data-question' => $questionId]
8321
                            ),
8322
                            '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>',
8323
                        ];
8324
                        $exercise_actions .= Display::div(
8325
                            implode(PHP_EOL, $button),
8326
                            ['class' => 'exercise_save_now_button']
8327
                        );
8328
                    }
8329
                    break;
8330
            }
8331
8332
            if (!empty($questions_in_media)) {
8333
                $count_of_questions_inside_media = count($questions_in_media);
8334
                if ($count_of_questions_inside_media > 1 && api_is_allowed_to_session_edit()) {
8335
                    $button = [
8336
                        Display::button(
8337
                            'save_now',
8338
                            get_lang('SaveForNow'),
8339
                            ['type' => 'button', 'class' => 'btn btn-primary', 'data-question' => $questionId]
8340
                        ),
8341
                        '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>&nbsp;',
8342
                    ];
8343
                    $exercise_actions = Display::div(
8344
                        implode(PHP_EOL, $button),
8345
                        ['class' => 'exercise_save_now_button']
8346
                    );
8347
                }
8348
8349
                if ($last_question_in_media && $this->type == ONE_PER_PAGE) {
8350
                    $exercise_actions = $this->show_button($questionId, $current_question, $questions_in_media);
8351
                }
8352
            }
8353
8354
            // Checkbox review answers
8355
            if ($this->review_answers &&
8356
                !in_array($question_obj->type, Question::question_type_no_review())
8357
            ) {
8358
                $remind_question_div = Display::tag(
8359
                    'label',
8360
                    Display::input(
8361
                        'checkbox',
8362
                        'remind_list['.$questionId.']',
8363
                        '',
8364
                        $attributes
8365
                    ).get_lang('ReviewQuestionLater'),
8366
                    [
8367
                        'class' => 'checkbox',
8368
                        'for' => 'remind_list['.$questionId.']',
8369
                    ]
8370
                );
8371
                $exercise_actions .= Display::div(
8372
                    $remind_question_div,
8373
                    ['class' => 'exercise_save_now_button']
8374
                );
8375
            }
8376
8377
            echo Display::div(' ', ['class' => 'clear']);
8378
8379
            $paginationCounter = null;
8380
            if ($this->type == ONE_PER_PAGE) {
8381
                if (empty($questions_in_media)) {
8382
                    $paginationCounter = Display::paginationIndicator(
8383
                        $current_question,
8384
                        count($realQuestionList)
8385
                    );
8386
                } else {
8387
                    if ($last_question_in_media) {
8388
                        $paginationCounter = Display::paginationIndicator(
8389
                            $current_question,
8390
                            count($realQuestionList)
8391
                        );
8392
                    }
8393
                }
8394
            }
8395
8396
            echo '<div class="row"><div class="pull-right">'.$paginationCounter.'</div></div>';
8397
            echo Display::div($exercise_actions, ['class' => 'form-actions']);
8398
            echo '</div>';
8399
        }
8400
    }
8401
8402
    /**
8403
     * Returns an array of categories details for the questions of the current
8404
     * exercise.
8405
     *
8406
     * @return array
8407
     */
8408
    public function getQuestionWithCategories()
8409
    {
8410
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
8411
        $categoryRelTable = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
8412
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8413
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
8414
        $sql = "SELECT DISTINCT cat.*
8415
                FROM $TBL_EXERCICE_QUESTION e
8416
                INNER JOIN $TBL_QUESTIONS q
8417
                ON e.question_id = q.iid
8418
                INNER JOIN $categoryRelTable catRel
8419
                ON (catRel.question_id = e.question_id AND catRel.c_id = e.c_id)
8420
                INNER JOIN $categoryTable cat
8421
                ON (cat.iid = catRel.category_id)
8422
                WHERE
8423
                  e.c_id = {$this->course_id} AND
8424
                  e.exercice_id	= ".intval($this->iid);
8425
8426
        $result = Database::query($sql);
8427
        $categoriesInExercise = [];
8428
        if (Database::num_rows($result)) {
8429
            $categoriesInExercise = Database::store_result($result, 'ASSOC');
8430
        }
8431
8432
        return $categoriesInExercise;
8433
    }
8434
8435
    /**
8436
     * Calculate the max_score of the quiz, depending of question inside, and quiz advanced option.
8437
     */
8438
    public function get_max_score()
8439
    {
8440
        $outMaxScore = 0;
8441
        // list of question's id !!! the array key start at 1 !!!
8442
        $questionList = $this->selectQuestionList(true);
8443
8444
        if ($this->random > 0 && $this->randomByCat > 0) {
8445
            // test is random by category
8446
            // get the $numberRandomQuestions best score question of each category
8447
            $numberRandomQuestions = $this->random;
8448
            $tab_categories_scores = [];
8449
            foreach ($questionList as $questionId) {
8450
                $question_category_id = TestCategory::getCategoryForQuestion($questionId);
8451
                if (!is_array($tab_categories_scores[$question_category_id])) {
8452
                    $tab_categories_scores[$question_category_id] = [];
8453
                }
8454
                $tmpobj_question = Question::read($questionId);
8455
                if (is_object($tmpobj_question)) {
8456
                    $tab_categories_scores[$question_category_id][] = $tmpobj_question->weighting;
8457
                }
8458
            }
8459
8460
            // here we've got an array with first key, the category_id, second key, score of question for this cat
8461
            foreach ($tab_categories_scores as $tab_scores) {
8462
                rsort($tab_scores);
8463
                for ($i = 0; $i < min($numberRandomQuestions, count($tab_scores)); $i++) {
8464
                    $outMaxScore += $tab_scores[$i];
8465
                }
8466
            }
8467
        } else {
8468
            // standard test, just add each question score
8469
            foreach ($questionList as $questionId) {
8470
                $question = Question::read($questionId, $this->course);
8471
                $outMaxScore += $question->weighting;
8472
            }
8473
        }
8474
8475
        return $outMaxScore;
8476
    }
8477
8478
    /**
8479
     * @return string
8480
     */
8481
    public function get_formated_title()
8482
    {
8483
        if (api_get_configuration_value('save_titles_as_html')) {
8484
        }
8485
8486
        return api_html_entity_decode($this->selectTitle());
8487
    }
8488
8489
    /**
8490
     * @param string $title
8491
     *
8492
     * @return string
8493
     */
8494
    public static function get_formated_title_variable($title)
8495
    {
8496
        return api_html_entity_decode($title);
8497
    }
8498
8499
    /**
8500
     * @return string
8501
     */
8502
    public function format_title()
8503
    {
8504
        return api_htmlentities($this->title);
8505
    }
8506
8507
    /**
8508
     * @param string $title
8509
     *
8510
     * @return string
8511
     */
8512
    public static function format_title_variable($title)
8513
    {
8514
        return api_htmlentities($title);
8515
    }
8516
8517
    /**
8518
     * @param int $courseId
8519
     * @param int $sessionId
8520
     *
8521
     * @return array exercises
8522
     */
8523
    public function getExercisesByCourseSession($courseId, $sessionId)
8524
    {
8525
        $courseId = (int) $courseId;
8526
        $sessionId = (int) $sessionId;
8527
8528
        $tbl_quiz = Database::get_course_table(TABLE_QUIZ_TEST);
8529
        $sql = "SELECT * FROM $tbl_quiz cq
8530
                WHERE
8531
                    cq.c_id = %s AND
8532
                    (cq.session_id = %s OR cq.session_id = 0) AND
8533
                    cq.active = 0
8534
                ORDER BY cq.iid";
8535
        $sql = sprintf($sql, $courseId, $sessionId);
8536
8537
        $result = Database::query($sql);
8538
8539
        $rows = [];
8540
        while ($row = Database::fetch_array($result, 'ASSOC')) {
8541
            $rows[] = $row;
8542
        }
8543
8544
        return $rows;
8545
    }
8546
8547
    /**
8548
     * Get array of exercise details and user results.
8549
     *
8550
     * @param int   $courseId
8551
     * @param int   $sessionId
8552
     * @param array $quizId
8553
     * @param bool  $checkOnlyActiveUsers
8554
     * @param array $filterDates          Limit the results exported to those within this range ('start_date' to 'end_date')
8555
     *
8556
     * @return array exercises
8557
     */
8558
    public function getExerciseAndResult($courseId, $sessionId, $quizId = [], $checkOnlyActiveUsers = false, $filterDates = [])
8559
    {
8560
        if (empty($quizId)) {
8561
            return [];
8562
        }
8563
8564
        $sessionId = (int) $sessionId;
8565
        $courseId = (int) $courseId;
8566
8567
        $ids = is_array($quizId) ? $quizId : [$quizId];
8568
        $ids = array_map('intval', $ids);
8569
        $ids = implode(',', $ids);
8570
        $trackExcercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8571
        $tblQuiz = Database::get_course_table(TABLE_QUIZ_TEST);
8572
        $tblUser = Database::get_main_table(TABLE_MAIN_USER);
8573
8574
        $condition = '';
8575
        $innerJoinUser = '';
8576
        if ($checkOnlyActiveUsers) {
8577
            $condition .= " AND te.status = '' ";
8578
            $innerJoinUser .= " INNER JOIN $tblUser u ON u.user_id = te. exe_user_id";
8579
        }
8580
8581
        if (!empty($filterDates)) {
8582
            if (!empty($filterDates['start_date'])) {
8583
                $condition .= " AND te.exe_date >= '".Database::escape_string($filterDates['start_date'])."' ";
8584
            }
8585
            if (!empty($filterDates['end_date'])) {
8586
                $condition .= " AND te.exe_date <= '".Database::escape_string($filterDates['end_date'])."' ";
8587
            }
8588
        }
8589
8590
        if (0 != $sessionId) {
8591
            $sql = "SELECT * FROM $trackExcercises te
8592
                    INNER JOIN $tblQuiz cq ON cq.iid = te.exe_exo_id
8593
                    $innerJoinUser
8594
                    WHERE
8595
                    te.c_id = %s AND
8596
                    te.session_id = %s AND
8597
                    cq.iid IN (%s)
8598
                    $condition
8599
              ORDER BY cq.iid";
8600
8601
            $sql = sprintf($sql, $courseId, $sessionId, $ids);
8602
        } else {
8603
            $sql = "SELECT * FROM $trackExcercises te
8604
                INNER JOIN $tblQuiz cq ON cq.iid = te.exe_exo_id
8605
                $innerJoinUser
8606
                WHERE
8607
                te.c_id = %s AND
8608
                cq.iid IN (%s)
8609
                $condition
8610
              ORDER BY cq.iid";
8611
            $sql = sprintf($sql, $courseId, $ids);
8612
        }
8613
        $result = Database::query($sql);
8614
        $rows = [];
8615
        while ($row = Database::fetch_array($result, 'ASSOC')) {
8616
            $rows[] = $row;
8617
        }
8618
8619
        return $rows;
8620
    }
8621
8622
    /**
8623
     * @param $exeId
8624
     * @param $exercise_stat_info
8625
     * @param $remindList
8626
     * @param $currentQuestion
8627
     *
8628
     * @return int|null
8629
     */
8630
    public static function getNextQuestionId(
8631
        $exeId,
8632
        $exercise_stat_info,
8633
        $remindList,
8634
        $currentQuestion
8635
    ) {
8636
        $result = Event::get_exercise_results_by_attempt($exeId, 'incomplete');
8637
8638
        if (isset($result[$exeId])) {
8639
            $result = $result[$exeId];
8640
        } else {
8641
            return null;
8642
        }
8643
8644
        $data_tracking = $exercise_stat_info['data_tracking'];
8645
        $data_tracking = explode(',', $data_tracking);
8646
8647
        // if this is the final question do nothing.
8648
        if ($currentQuestion == count($data_tracking)) {
8649
            return null;
8650
        }
8651
8652
        $currentQuestion--;
8653
8654
        if (!empty($result['question_list'])) {
8655
            $answeredQuestions = [];
8656
            foreach ($result['question_list'] as $question) {
8657
                if (!empty($question['answer'])) {
8658
                    $answeredQuestions[] = $question['question_id'];
8659
                }
8660
            }
8661
8662
            // Checking answered questions
8663
            $counterAnsweredQuestions = 0;
8664
            foreach ($data_tracking as $questionId) {
8665
                if (!in_array($questionId, $answeredQuestions)) {
8666
                    if ($currentQuestion != $counterAnsweredQuestions) {
8667
                        break;
8668
                    }
8669
                }
8670
                $counterAnsweredQuestions++;
8671
            }
8672
8673
            $counterRemindListQuestions = 0;
8674
            // Checking questions saved in the reminder list
8675
            if (!empty($remindList)) {
8676
                foreach ($data_tracking as $questionId) {
8677
                    if (in_array($questionId, $remindList)) {
8678
                        // Skip the current question
8679
                        if ($currentQuestion != $counterRemindListQuestions) {
8680
                            break;
8681
                        }
8682
                    }
8683
                    $counterRemindListQuestions++;
8684
                }
8685
8686
                if ($counterRemindListQuestions < $currentQuestion) {
8687
                    return null;
8688
                }
8689
8690
                if (!empty($counterRemindListQuestions)) {
8691
                    if ($counterRemindListQuestions > $counterAnsweredQuestions) {
8692
                        return $counterAnsweredQuestions;
8693
                    } else {
8694
                        return $counterRemindListQuestions;
8695
                    }
8696
                }
8697
            }
8698
8699
            return $counterAnsweredQuestions;
8700
        }
8701
    }
8702
8703
    /**
8704
     * Gets the position of a questionId in the question list.
8705
     *
8706
     * @param $questionId
8707
     *
8708
     * @return int
8709
     */
8710
    public function getPositionInCompressedQuestionList($questionId)
8711
    {
8712
        $questionList = $this->getQuestionListWithMediasCompressed();
8713
        $mediaQuestions = $this->getMediaList();
8714
        $position = 1;
8715
        foreach ($questionList as $id) {
8716
            if (isset($mediaQuestions[$id]) && in_array($questionId, $mediaQuestions[$id])) {
8717
                $mediaQuestionList = $mediaQuestions[$id];
8718
                if (in_array($questionId, $mediaQuestionList)) {
8719
                    return $position;
8720
                } else {
8721
                    $position++;
8722
                }
8723
            } else {
8724
                if ($id == $questionId) {
8725
                    return $position;
8726
                } else {
8727
                    $position++;
8728
                }
8729
            }
8730
        }
8731
8732
        return 1;
8733
    }
8734
8735
    /**
8736
     * Get the correct answers in all attempts.
8737
     *
8738
     * @param int  $learnPathId
8739
     * @param int  $learnPathItemId
8740
     * @param bool $onlyCorrect
8741
     *
8742
     * @return array
8743
     */
8744
    public function getAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0, $onlyCorrect = true)
8745
    {
8746
        $attempts = Event::getExerciseResultsByUser(
8747
            api_get_user_id(),
8748
            $this->iid,
8749
            api_get_course_int_id(),
8750
            api_get_session_id(),
8751
            $learnPathId,
8752
            $learnPathItemId,
8753
            'DESC'
8754
        );
8755
8756
        $list = [];
8757
        foreach ($attempts as $attempt) {
8758
            foreach ($attempt['question_list'] as $answers) {
8759
                foreach ($answers as $answer) {
8760
                    $objAnswer = new Answer($answer['question_id']);
8761
                    if ($onlyCorrect) {
8762
                        switch ($objAnswer->getQuestionType()) {
8763
                            case FILL_IN_BLANKS:
8764
                            case FILL_IN_BLANKS_COMBINATION:
8765
                                $isCorrect = FillBlanks::isCorrect($answer['answer']);
8766
                                break;
8767
                            case MATCHING:
8768
                            case MATCHING_COMBINATION:
8769
                            case DRAGGABLE:
8770
                            case MATCHING_DRAGGABLE_COMBINATION:
8771
                            case MATCHING_DRAGGABLE:
8772
                                $isCorrect = Matching::isCorrect(
8773
                                    $answer['position'],
8774
                                    $answer['answer'],
8775
                                    $answer['question_id']
8776
                                );
8777
                                break;
8778
                            case ORAL_EXPRESSION:
8779
                                $isCorrect = false;
8780
                                break;
8781
                            default:
8782
                                $isCorrect = $objAnswer->isCorrectByAutoId($answer['answer']);
8783
                        }
8784
                        if ($isCorrect) {
8785
                            $list[$answer['question_id']][] = $answer;
8786
                        }
8787
                    } else {
8788
                        $list[$answer['question_id']][] = $answer;
8789
                    }
8790
                }
8791
            }
8792
8793
            if (false === $onlyCorrect) {
8794
                // Only take latest attempt
8795
                break;
8796
            }
8797
        }
8798
8799
        return $list;
8800
    }
8801
8802
    /**
8803
     * Get the correct answers in all attempts.
8804
     *
8805
     * @param int $learnPathId
8806
     * @param int $learnPathItemId
8807
     *
8808
     * @return array
8809
     */
8810
    public function getCorrectAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0)
8811
    {
8812
        return $this->getAnswersInAllAttempts($learnPathId, $learnPathItemId);
8813
    }
8814
8815
    /**
8816
     * @return bool
8817
     */
8818
    public function showPreviousButton()
8819
    {
8820
        $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
8821
        if (false === $allow) {
8822
            return true;
8823
        }
8824
8825
        return $this->showPreviousButton;
8826
    }
8827
8828
    public function getPreventBackwards()
8829
    {
8830
        $allow = api_get_configuration_value('quiz_prevent_backwards_move');
8831
        if (false === $allow) {
8832
            return 0;
8833
        }
8834
8835
        return (int) $this->preventBackwards;
8836
    }
8837
8838
    /**
8839
     * @return int
8840
     */
8841
    public function getExerciseCategoryId()
8842
    {
8843
        if (empty($this->exerciseCategoryId)) {
8844
            return null;
8845
        }
8846
8847
        return (int) $this->exerciseCategoryId;
8848
    }
8849
8850
    /**
8851
     * @param int $value
8852
     */
8853
    public function setExerciseCategoryId($value)
8854
    {
8855
        if (!empty($value)) {
8856
            $this->exerciseCategoryId = (int) $value;
8857
        }
8858
    }
8859
8860
    /**
8861
     * Set the value to 1 to hide the question number.
8862
     *
8863
     * @param int $value
8864
     */
8865
    public function setHideQuestionNumber($value = 0)
8866
    {
8867
        $showHideConfiguration = api_get_configuration_value('quiz_hide_question_number');
8868
        if ($showHideConfiguration) {
8869
            $this->hideQuestionNumber = (int) $value;
8870
        }
8871
    }
8872
8873
    /**
8874
     * Gets the value to hide or show the question number. If it does not exist, it is set to 0.
8875
     *
8876
     * @return int 1 if the question number must be hidden
8877
     */
8878
    public function getHideQuestionNumber()
8879
    {
8880
        $showHideConfiguration = api_get_configuration_value('quiz_hide_question_number');
8881
        if ($showHideConfiguration) {
8882
            return (int) $this->hideQuestionNumber;
8883
        }
8884
8885
        return 0;
8886
    }
8887
8888
    /**
8889
     * Set the value to 1 to hide the attempts table on start page.
8890
     *
8891
     * @param int $value
8892
     */
8893
    public function setHideAttemptsTableOnStartPage($value = 0)
8894
    {
8895
        $showHideAttemptsTableOnStartPage = api_get_configuration_value('quiz_hide_attempts_table_on_start_page');
8896
        if ($showHideAttemptsTableOnStartPage) {
8897
            $this->hideAttemptsTableOnStartPage = (int) $value;
8898
        }
8899
    }
8900
8901
    /**
8902
     * Gets the value to hide or show the attempts table on start page. If it does not exist, it is set to 0.
8903
     *
8904
     * @return int 1 if the attempts table must be hidden
8905
     */
8906
    public function getHideAttemptsTableOnStartPage()
8907
    {
8908
        $showHideAttemptsTableOnStartPage = api_get_configuration_value('quiz_hide_attempts_table_on_start_page');
8909
        if ($showHideAttemptsTableOnStartPage) {
8910
            return (int) $this->hideAttemptsTableOnStartPage;
8911
        }
8912
8913
        return 0;
8914
    }
8915
8916
    /**
8917
     * @param array $values
8918
     */
8919
    public function setPageResultConfiguration($values)
8920
    {
8921
        $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
8922
        if ($pageConfig) {
8923
            $params = [
8924
                'hide_expected_answer' => $values['hide_expected_answer'] ?? '',
8925
                'hide_question_score' => $values['hide_question_score'] ?? '',
8926
                'hide_total_score' => $values['hide_total_score'] ?? '',
8927
                'hide_category_table' => $values['hide_category_table'] ?? '',
8928
                'hide_correct_answered_questions' => $values['hide_correct_answered_questions'] ?? '',
8929
                'hide_comment' => $values['hide_comment'] ?? '',
8930
            ];
8931
            $type = Type::getType('array');
8932
            $platform = Database::getManager()->getConnection()->getDatabasePlatform();
8933
            $this->pageResultConfiguration = $type->convertToDatabaseValue($params, $platform);
8934
        }
8935
    }
8936
8937
    /**
8938
     * @param array $defaults
8939
     */
8940
    public function setPageResultConfigurationDefaults(&$defaults)
8941
    {
8942
        $configuration = $this->getPageResultConfiguration();
8943
        if (!empty($configuration) && !empty($defaults)) {
8944
            $defaults = array_merge($defaults, $configuration);
8945
        }
8946
    }
8947
8948
    /**
8949
     * Sets the value to show or hide the question number in the default settings of the forms.
8950
     *
8951
     * @param array $defaults
8952
     */
8953
    public function setHideQuestionNumberDefaults(&$defaults)
8954
    {
8955
        $configuration = $this->getHideQuestionNumberConfiguration();
8956
        if (!empty($configuration) && !empty($defaults)) {
8957
            $defaults = array_merge($defaults, $configuration);
8958
        }
8959
    }
8960
8961
    /**
8962
     * Sets the value to show or hide the attempts table on start page in the default settings of the forms.
8963
     *
8964
     * @param array $defaults
8965
     */
8966
    public function setHideAttemptsTableOnStartPageDefaults(&$defaults)
8967
    {
8968
        $configuration = $this->getHideAttemptsTableOnStartPageConfiguration();
8969
        if (!empty($configuration) && !empty($defaults)) {
8970
            $defaults = array_merge($defaults, $configuration);
8971
        }
8972
    }
8973
8974
    /**
8975
     * @return array
8976
     */
8977
    public function getPageResultConfiguration()
8978
    {
8979
        $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
8980
        if ($pageConfig) {
8981
            $type = Type::getType('array');
8982
            $platform = Database::getManager()->getConnection()->getDatabasePlatform();
8983
8984
            return $type->convertToPHPValue($this->pageResultConfiguration, $platform);
8985
        }
8986
8987
        return [];
8988
    }
8989
8990
    /**
8991
     * Get the value to show or hide the question number in the default settings of the forms.
8992
     *
8993
     * @return array
8994
     */
8995
    public function getHideQuestionNumberConfiguration()
8996
    {
8997
        $pageConfig = api_get_configuration_value('quiz_hide_question_number');
8998
        if ($pageConfig) {
8999
            return ['hide_question_number' => $this->hideQuestionNumber];
9000
        }
9001
9002
        return [];
9003
    }
9004
9005
    /**
9006
     * Get the value to show or hide the attempts table on start page in the default settings of the forms.
9007
     *
9008
     * @return array
9009
     */
9010
    public function getHideAttemptsTableOnStartPageConfiguration()
9011
    {
9012
        $pageConfig = api_get_configuration_value('quiz_hide_attempts_table_on_start_page');
9013
        if ($pageConfig) {
9014
            return ['hide_attempts_table' => $this->hideAttemptsTableOnStartPage];
9015
        }
9016
9017
        return [];
9018
    }
9019
9020
    /**
9021
     * @param string $attribute
9022
     *
9023
     * @return mixed|null
9024
     */
9025
    public function getPageConfigurationAttribute($attribute)
9026
    {
9027
        $result = $this->getPageResultConfiguration();
9028
9029
        if (!empty($result)) {
9030
            return isset($result[$attribute]) ? $result[$attribute] : null;
9031
        }
9032
9033
        return null;
9034
    }
9035
9036
    /**
9037
     * @param bool $showPreviousButton
9038
     *
9039
     * @return Exercise
9040
     */
9041
    public function setShowPreviousButton($showPreviousButton)
9042
    {
9043
        $this->showPreviousButton = $showPreviousButton;
9044
9045
        return $this;
9046
    }
9047
9048
    /**
9049
     * @param array $notifications
9050
     */
9051
    public function setNotifications($notifications)
9052
    {
9053
        $this->notifications = $notifications;
9054
    }
9055
9056
    /**
9057
     * @return array
9058
     */
9059
    public function getNotifications()
9060
    {
9061
        return $this->notifications;
9062
    }
9063
9064
    /**
9065
     * @return bool
9066
     */
9067
    public function showExpectedChoice()
9068
    {
9069
        return api_get_configuration_value('show_exercise_expected_choice');
9070
    }
9071
9072
    /**
9073
     * @return bool
9074
     */
9075
    public function showExpectedChoiceColumn()
9076
    {
9077
        if (true === $this->forceShowExpectedChoiceColumn) {
9078
            return true;
9079
        }
9080
9081
        if ($this->hideExpectedAnswer) {
9082
            return false;
9083
        }
9084
9085
        if (!in_array($this->results_disabled, [
9086
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
9087
        ])
9088
        ) {
9089
            $hide = (int) $this->getPageConfigurationAttribute('hide_expected_answer');
9090
            if (1 === $hide) {
9091
                return false;
9092
            }
9093
9094
            return true;
9095
        }
9096
9097
        return false;
9098
    }
9099
9100
    /**
9101
     * @param string $class
9102
     * @param string $scoreLabel
9103
     * @param string $result
9104
     * @param array
9105
     *
9106
     * @return string
9107
     */
9108
    public function getQuestionRibbon($class, $scoreLabel, $result, $array)
9109
    {
9110
        $hide = (int) $this->getPageConfigurationAttribute('hide_question_score');
9111
        if (1 === $hide) {
9112
            return '';
9113
        }
9114
9115
        if ($this->showExpectedChoice()) {
9116
            $html = null;
9117
            $hideLabel = api_get_configuration_value('exercise_hide_label');
9118
            $label = '<div class="rib rib-'.$class.'">
9119
                        <h3>'.$scoreLabel.'</h3>
9120
                      </div>';
9121
            if (!empty($result)) {
9122
                $label .= '<h4>'.get_lang('Score').': '.$result.'</h4>';
9123
            }
9124
            if (true === $hideLabel) {
9125
                $answerUsed = (int) $array['used'];
9126
                $answerMissing = (int) $array['missing'] - $answerUsed;
9127
                for ($i = 1; $i <= $answerUsed; $i++) {
9128
                    $html .= '<span class="score-img">'.
9129
                        Display::return_icon('attempt-check.png', null, null, ICON_SIZE_SMALL).
9130
                        '</span>';
9131
                }
9132
                for ($i = 1; $i <= $answerMissing; $i++) {
9133
                    $html .= '<span class="score-img">'.
9134
                        Display::return_icon('attempt-nocheck.png', null, null, ICON_SIZE_SMALL).
9135
                        '</span>';
9136
                }
9137
                $label = '<div class="score-title">'.get_lang('CorrectAnswers').': '.$result.'</div>';
9138
                $label .= '<div class="score-limits">';
9139
                $label .= $html;
9140
                $label .= '</div>';
9141
            }
9142
9143
            return '<div class="ribbon">
9144
                '.$label.'
9145
                </div>'
9146
                ;
9147
        } else {
9148
            $html = '<div class="ribbon">
9149
                        <div class="rib rib-'.$class.'">
9150
                            <h3>'.$scoreLabel.'</h3>
9151
                        </div>';
9152
            if (!empty($result)) {
9153
                $html .= '<h4>'.get_lang('Score').': '.$result.'</h4>';
9154
            }
9155
            $html .= '</div>';
9156
9157
            return $html;
9158
        }
9159
    }
9160
9161
    /**
9162
     * @return int
9163
     */
9164
    public function getAutoLaunch()
9165
    {
9166
        return $this->autolaunch;
9167
    }
9168
9169
    /**
9170
     * Clean auto launch settings for all exercise in course/course-session.
9171
     */
9172
    public function enableAutoLaunch()
9173
    {
9174
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
9175
        $sql = "UPDATE $table SET autolaunch = 1
9176
                WHERE iid = ".$this->iid;
9177
        Database::query($sql);
9178
    }
9179
9180
    /**
9181
     * Clean auto launch settings for all exercise in course/course-session.
9182
     */
9183
    public function cleanCourseLaunchSettings()
9184
    {
9185
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
9186
        $sql = "UPDATE $table SET autolaunch = 0
9187
                WHERE c_id = ".$this->course_id." AND session_id = ".$this->sessionId;
9188
        Database::query($sql);
9189
    }
9190
9191
    /**
9192
     * Get the title without HTML tags.
9193
     *
9194
     * @return string
9195
     */
9196
    public function getUnformattedTitle()
9197
    {
9198
        return strip_tags(api_html_entity_decode($this->title));
9199
    }
9200
9201
    /**
9202
     * Get the question IDs from quiz_rel_question for the current quiz,
9203
     * using the parameters as the arguments to the SQL's LIMIT clause.
9204
     * Because the exercise_id is known, it also comes with a filter on
9205
     * the session, so sessions are not specified here.
9206
     *
9207
     * @param int $start  At which question do we want to start the list
9208
     * @param int $length Up to how many results we want
9209
     *
9210
     * @return array A list of question IDs
9211
     */
9212
    public function getQuestionForTeacher($start = 0, $length = 10)
9213
    {
9214
        $start = (int) $start;
9215
        if ($start < 0) {
9216
            $start = 0;
9217
        }
9218
9219
        $length = (int) $length;
9220
9221
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
9222
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
9223
        $sql = "SELECT DISTINCT e.question_id
9224
                FROM $quizRelQuestion e
9225
                INNER JOIN $question q
9226
                ON e.question_id = q.iid
9227
                WHERE
9228
                    e.c_id = {$this->course_id} AND
9229
                    e.exercice_id = {$this->iid}
9230
                ORDER BY question_order
9231
                LIMIT $start, $length
9232
            ";
9233
        $result = Database::query($sql);
9234
        $questionList = [];
9235
        while ($object = Database::fetch_object($result)) {
9236
            $questionList[] = $object->question_id;
9237
        }
9238
9239
        return $questionList;
9240
    }
9241
9242
    /**
9243
     * @param int   $exerciseId
9244
     * @param array $courseInfo
9245
     * @param int   $sessionId
9246
     *
9247
     * @return bool
9248
     */
9249
    public function generateStats($exerciseId, $courseInfo, $sessionId)
9250
    {
9251
        $allowStats = api_get_configuration_value('allow_gradebook_stats');
9252
        if (!$allowStats) {
9253
            return false;
9254
        }
9255
9256
        if (empty($courseInfo)) {
9257
            return false;
9258
        }
9259
9260
        $courseId = $courseInfo['real_id'];
9261
9262
        $sessionId = (int) $sessionId;
9263
        $exerciseId = (int) $exerciseId;
9264
9265
        $result = $this->read($exerciseId);
9266
9267
        if (empty($result)) {
9268
            api_not_allowed(true);
9269
        }
9270
9271
        $statusToFilter = empty($sessionId) ? STUDENT : 0;
9272
9273
        $studentList = CourseManager::get_user_list_from_course_code(
9274
            $courseInfo['code'],
9275
            $sessionId,
9276
            null,
9277
            null,
9278
            $statusToFilter
9279
        );
9280
9281
        if (empty($studentList)) {
9282
            Display::addFlash(Display::return_message(get_lang('NoUsersInCourse')));
9283
            header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
9284
            exit;
9285
        }
9286
9287
        $tblStats = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
9288
9289
        $studentIdList = [];
9290
        if (!empty($studentList)) {
9291
            $studentIdList = array_column($studentList, 'user_id');
9292
        }
9293
9294
        if (false == $this->exercise_was_added_in_lp) {
9295
            $sql = "SELECT * FROM $tblStats
9296
                        WHERE
9297
                            exe_exo_id = $exerciseId AND
9298
                            orig_lp_id = 0 AND
9299
                            orig_lp_item_id = 0 AND
9300
                            status <> 'incomplete' AND
9301
                            session_id = $sessionId AND
9302
                            c_id = $courseId
9303
                        ";
9304
        } else {
9305
            $lpId = null;
9306
            if (!empty($this->lpList)) {
9307
                // Taking only the first LP
9308
                $lpId = $this->getLpBySession($sessionId);
9309
                $lpId = $lpId['lp_id'];
9310
            }
9311
9312
            $sql = "SELECT *
9313
                        FROM $tblStats
9314
                        WHERE
9315
                            exe_exo_id = $exerciseId AND
9316
                            orig_lp_id = $lpId AND
9317
                            status <> 'incomplete' AND
9318
                            session_id = $sessionId AND
9319
                            c_id = $courseId ";
9320
        }
9321
9322
        $sql .= ' ORDER BY exe_id DESC';
9323
9324
        $studentCount = 0;
9325
        $sum = 0;
9326
        $bestResult = 0;
9327
        $sumResult = 0;
9328
        $result = Database::query($sql);
9329
        $students = [];
9330
        while ($data = Database::fetch_array($result, 'ASSOC')) {
9331
            // Only take into account users in the current student list.
9332
            if (!empty($studentIdList)) {
9333
                if (!in_array($data['exe_user_id'], $studentIdList)) {
9334
                    continue;
9335
                }
9336
            }
9337
9338
            if (!isset($students[$data['exe_user_id']])) {
9339
                if ($data['exe_weighting'] != 0) {
9340
                    $students[$data['exe_user_id']] = $data['exe_result'];
9341
                    if ($data['exe_result'] > $bestResult) {
9342
                        $bestResult = $data['exe_result'];
9343
                    }
9344
                    $sumResult += $data['exe_result'];
9345
                }
9346
            }
9347
        }
9348
9349
        $count = count($studentList);
9350
        $average = $sumResult / $count;
9351
        $em = Database::getManager();
9352
9353
        $links = AbstractLink::getGradebookLinksFromItem(
9354
            $this->iid,
9355
            LINK_EXERCISE,
9356
            $courseInfo['code'],
9357
            $sessionId
9358
        );
9359
9360
        if (empty($links)) {
9361
            $links = AbstractLink::getGradebookLinksFromItem(
9362
                $this->iid,
9363
                LINK_EXERCISE,
9364
                $courseInfo['code'],
9365
                $sessionId
9366
            );
9367
        }
9368
9369
        if (!empty($links)) {
9370
            $repo = $em->getRepository('ChamiloCoreBundle:GradebookLink');
9371
9372
            foreach ($links as $link) {
9373
                $linkId = $link['id'];
9374
                /** @var GradebookLink $exerciseLink */
9375
                $exerciseLink = $repo->find($linkId);
9376
                if ($exerciseLink) {
9377
                    $exerciseLink
9378
                        ->setUserScoreList($students)
9379
                        ->setBestScore($bestResult)
9380
                        ->setAverageScore($average)
9381
                        ->setScoreWeight($this->get_max_score());
9382
                    $em->persist($exerciseLink);
9383
                    $em->flush();
9384
                }
9385
            }
9386
        }
9387
    }
9388
9389
    /**
9390
     * Return an HTML table of exercises for on-screen printing, including
9391
     * action icons. If no exercise is present and the user can edit the
9392
     * course, show a "create test" button.
9393
     *
9394
     * @param int    $categoryId
9395
     * @param string $keyword
9396
     * @param int    $userId
9397
     * @param int    $courseId
9398
     * @param int    $sessionId
9399
     * @param bool   $returnData
9400
     * @param int    $minCategoriesInExercise
9401
     * @param int    $filterByResultDisabled
9402
     * @param int    $filterByAttempt
9403
     *
9404
     * @return string|SortableTableFromArrayConfig
9405
     */
9406
    public static function exerciseGrid(
9407
        $categoryId,
9408
        $keyword = '',
9409
        $userId = 0,
9410
        $courseId = 0,
9411
        $sessionId = 0,
9412
        $returnData = false,
9413
        $minCategoriesInExercise = 0,
9414
        $filterByResultDisabled = 0,
9415
        $filterByAttempt = 0,
9416
        $myActions = null,
9417
        $returnTable = false,
9418
        $isAllowedToEdit = null
9419
    ) {
9420
        //$allowDelete = Exercise::allowAction('delete');
9421
        $allowClean = self::allowAction('clean_results');
9422
9423
        $TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
9424
        $TBL_ITEM_PROPERTY = Database::get_course_table(TABLE_ITEM_PROPERTY);
9425
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
9426
        $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
9427
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
9428
9429
        $categoryId = (int) $categoryId;
9430
        $keyword = Database::escape_string($keyword);
9431
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
9432
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
9433
9434
        $autoLaunchAvailable = false;
9435
        if (api_get_course_setting('enable_exercise_auto_launch') == 1 &&
9436
            api_get_configuration_value('allow_exercise_auto_launch')
9437
        ) {
9438
            $autoLaunchAvailable = true;
9439
        }
9440
9441
        if (!isset($isAllowedToEdit)) {
9442
            $isAllowedToEdit = api_is_allowed_to_edit(null, true);
9443
        }
9444
        $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : api_get_course_info();
9445
        $sessionId = $sessionId ? (int) $sessionId : api_get_session_id();
9446
        $courseId = $courseInfo['real_id'];
9447
        $tableRows = [];
9448
        $uploadPath = DIR_HOTPOTATOES; //defined in main_api
9449
        $exercisePath = api_get_self();
9450
        $origin = api_get_origin();
9451
        $userInfo = $userId ? api_get_user_info($userId) : api_get_user_info();
9452
        $charset = 'utf-8';
9453
        $token = Security::get_token();
9454
        $userId = $userId ? (int) $userId : api_get_user_id();
9455
        $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh($userId, $courseInfo);
9456
        $documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
9457
        $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
9458
9459
        // Condition for the session
9460
        $condition_session = api_get_session_condition($sessionId, true, true, 'e.session_id');
9461
        $content = '';
9462
        $column = 0;
9463
        if ($isAllowedToEdit) {
9464
            $column = 1;
9465
        }
9466
9467
        $table = new SortableTableFromArrayConfig(
9468
            [],
9469
            $column,
9470
            self::PAGINATION_ITEMS_PER_PAGE,
9471
            'exercises_cat_'.$categoryId
9472
        );
9473
9474
        $limit = $table->per_page;
9475
        $page = $table->page_nr;
9476
        $from = $limit * ($page - 1);
9477
9478
        $categoryCondition = '';
9479
        if (api_get_configuration_value('allow_exercise_categories')) {
9480
            if (!empty($categoryId)) {
9481
                $categoryCondition = " AND exercise_category_id = $categoryId ";
9482
            } else {
9483
                $categoryCondition = ' AND exercise_category_id IS NULL ';
9484
            }
9485
        }
9486
9487
        $keywordCondition = '';
9488
        if (!empty($keyword)) {
9489
            $keywordCondition = " AND title LIKE '%$keyword%' ";
9490
        }
9491
9492
        $filterByResultDisabledCondition = '';
9493
        $filterByResultDisabled = (int) $filterByResultDisabled;
9494
        if (!empty($filterByResultDisabled)) {
9495
            $filterByResultDisabledCondition = ' AND e.results_disabled = '.$filterByResultDisabled;
9496
        }
9497
        $filterByAttemptCondition = '';
9498
        $filterByAttempt = (int) $filterByAttempt;
9499
        if (!empty($filterByAttempt)) {
9500
            $filterByAttemptCondition = ' AND e.max_attempt = '.$filterByAttempt;
9501
        }
9502
9503
        // Only for administrators
9504
        if ($isAllowedToEdit) {
9505
            $total_sql = "SELECT count(iid) as count
9506
                          FROM $TBL_EXERCISES e
9507
                          WHERE
9508
                                c_id = $courseId AND
9509
                                active <> -1
9510
                                $condition_session
9511
                                $categoryCondition
9512
                                $keywordCondition
9513
                                $filterByResultDisabledCondition
9514
                                $filterByAttemptCondition
9515
                                ";
9516
            $sql = "SELECT * FROM $TBL_EXERCISES e
9517
                    WHERE
9518
                        c_id = $courseId AND
9519
                        active <> -1
9520
                        $condition_session
9521
                        $categoryCondition
9522
                        $keywordCondition
9523
                        $filterByResultDisabledCondition
9524
                        $filterByAttemptCondition
9525
                    ORDER BY title
9526
                    LIMIT $from , $limit";
9527
        } else {
9528
            // Only for students
9529
            if (empty($sessionId)) {
9530
                $condition_session = ' AND ( session_id = 0 OR session_id IS NULL) ';
9531
                $total_sql = "SELECT count(DISTINCT(e.iid)) as count
9532
                              FROM $TBL_EXERCISES e
9533
                              WHERE
9534
                                    e.c_id = $courseId AND
9535
                                    e.active = 1
9536
                                    $condition_session
9537
                                    $categoryCondition
9538
                                    $keywordCondition
9539
                              ";
9540
9541
                $sql = "SELECT DISTINCT e.* FROM $TBL_EXERCISES e
9542
                        WHERE
9543
                             e.c_id = $courseId AND
9544
                             e.active = 1
9545
                             $condition_session
9546
                             $categoryCondition
9547
                             $keywordCondition
9548
                        ORDER BY title
9549
                        LIMIT $from , $limit";
9550
            } else {
9551
                $invisibleSql = "SELECT e.iid
9552
                                  FROM $TBL_EXERCISES e
9553
                                  INNER JOIN $TBL_ITEM_PROPERTY ip
9554
                                  ON (e.iid = ip.ref AND e.c_id = ip.c_id)
9555
                                  WHERE
9556
                                        ip.tool = '".TOOL_QUIZ."' AND
9557
                                        e.c_id = $courseId AND
9558
                                        e.active = 1 AND
9559
                                        ip.visibility = 0 AND
9560
                                        ip.session_id = $sessionId
9561
                                        $categoryCondition
9562
                                        $keywordCondition
9563
                                  ";
9564
9565
                $result = Database::query($invisibleSql);
9566
                $result = Database::store_result($result);
9567
                $hiddenFromSessionCondition = ' 1=1 ';
9568
                if (!empty($result)) {
9569
                    $hiddenFromSession = implode("','", array_column($result, 'iid'));
9570
                    $hiddenFromSessionCondition = " e.iid not in ('$hiddenFromSession')";
9571
                }
9572
9573
                $condition_session = " AND (
9574
                        (e.session_id = $sessionId OR e.session_id = 0 OR e.session_id IS NULL) AND
9575
                        $hiddenFromSessionCondition
9576
                )
9577
                ";
9578
9579
                // Only for students
9580
                $total_sql = "SELECT count(DISTINCT(e.iid)) as count
9581
                              FROM $TBL_EXERCISES e
9582
                              WHERE
9583
                                    e.c_id = $courseId AND
9584
                                    e.active = 1
9585
                                    $condition_session
9586
                                    $categoryCondition
9587
                                    $keywordCondition
9588
                              ";
9589
                $sql = "SELECT DISTINCT e.* FROM $TBL_EXERCISES e
9590
                        WHERE
9591
                             e.c_id = $courseId AND
9592
                             e.active = 1
9593
                             $condition_session
9594
                             $categoryCondition
9595
                             $keywordCondition
9596
                        ORDER BY title
9597
                        LIMIT $from , $limit";
9598
            }
9599
        }
9600
9601
        $result = Database::query($sql);
9602
        $result_total = Database::query($total_sql);
9603
9604
        $total_exercises = 0;
9605
        if (Database::num_rows($result_total)) {
9606
            $result_total = Database::fetch_array($result_total);
9607
            $total_exercises = $result_total['count'];
9608
        }
9609
9610
        //get HotPotatoes files (active and inactive)
9611
        if ($isAllowedToEdit) {
9612
            $sql = "SELECT * FROM $TBL_DOCUMENT
9613
                    WHERE
9614
                        c_id = $courseId AND
9615
                        path LIKE '".Database::escape_string($uploadPath.'/%/%')."'";
9616
            $res = Database::query($sql);
9617
            $hp_count = Database::num_rows($res);
9618
        } else {
9619
            $sql = "SELECT * FROM $TBL_DOCUMENT d
9620
                    INNER JOIN $TBL_ITEM_PROPERTY ip
9621
                    ON (d.iid = ip.ref)
9622
                    WHERE
9623
                        ip.tool = '".TOOL_DOCUMENT."' AND
9624
                        d.path LIKE '".Database::escape_string($uploadPath.'/%/%')."' AND
9625
                        ip.visibility = 1 AND
9626
                        d.c_id = $courseId AND
9627
                        ip.c_id  = $courseId";
9628
            $res = Database::query($sql);
9629
            $hp_count = Database::num_rows($res);
9630
        }
9631
9632
        $total = $total_exercises + $hp_count;
9633
        $exerciseList = [];
9634
        while ($row = Database::fetch_array($result, 'ASSOC')) {
9635
            $exerciseList[] = $row;
9636
        }
9637
9638
        if (!empty($exerciseList) &&
9639
            api_get_setting('exercise_invisible_in_session') === 'true'
9640
        ) {
9641
            if (!empty($sessionId)) {
9642
                $changeDefaultVisibility = true;
9643
                if (api_get_setting('configure_exercise_visibility_in_course') === 'true') {
9644
                    $changeDefaultVisibility = false;
9645
                    if (api_get_course_setting('exercise_invisible_in_session') == 1) {
9646
                        $changeDefaultVisibility = true;
9647
                    }
9648
                }
9649
9650
                if ($changeDefaultVisibility) {
9651
                    // Check exercise
9652
                    foreach ($exerciseList as $exercise) {
9653
                        if ($exercise['session_id'] == 0) {
9654
                            $visibilityInfo = api_get_item_property_info(
9655
                                $courseId,
9656
                                TOOL_QUIZ,
9657
                                $exercise['iid'],
9658
                                $sessionId
9659
                            );
9660
9661
                            if (empty($visibilityInfo)) {
9662
                                // Create a record for this
9663
                                api_item_property_update(
9664
                                    $courseInfo,
9665
                                    TOOL_QUIZ,
9666
                                    $exercise['iid'],
9667
                                    'invisible',
9668
                                    api_get_user_id(),
9669
                                    0,
9670
                                    null,
9671
                                    '',
9672
                                    '',
9673
                                    $sessionId
9674
                                );
9675
                            }
9676
                        }
9677
                    }
9678
                }
9679
            }
9680
        }
9681
9682
        $webPath = api_get_path(WEB_CODE_PATH);
9683
        if (!empty($exerciseList)) {
9684
            if ($origin !== 'learnpath') {
9685
                $visibilitySetting = api_get_configuration_value('show_hidden_exercise_added_to_lp');
9686
                //avoid sending empty parameters
9687
                $mylpid = empty($learnpath_id) ? '' : '&learnpath_id='.$learnpath_id;
9688
                $mylpitemid = empty($learnpath_item_id) ? '' : '&learnpath_item_id='.$learnpath_item_id;
9689
                foreach ($exerciseList as $row) {
9690
                    $currentRow = [];
9691
                    $my_exercise_id = $row['iid'];
9692
                    $attempt_text = '';
9693
                    $actions = '';
9694
                    $exercise = new Exercise($returnData ? $courseId : 0);
9695
                    $exercise->read($my_exercise_id, false);
9696
9697
                    if (empty($exercise->iid)) {
9698
                        continue;
9699
                    }
9700
9701
                    $locked = $exercise->is_gradebook_locked;
9702
                    // Validation when belongs to a session
9703
                    $session_img = api_get_session_image($row['session_id'], $userInfo['status']);
9704
9705
                    $time_limits = false;
9706
                    if (!empty($row['start_time']) || !empty($row['end_time'])) {
9707
                        $time_limits = true;
9708
                    }
9709
9710
                    $is_actived_time = false;
9711
                    if ($time_limits) {
9712
                        // check if start time
9713
                        $start_time = false;
9714
                        if (!empty($row['start_time'])) {
9715
                            $start_time = api_strtotime($row['start_time'], 'UTC');
9716
                        }
9717
                        $end_time = false;
9718
                        if (!empty($row['end_time'])) {
9719
                            $end_time = api_strtotime($row['end_time'], 'UTC');
9720
                        }
9721
                        $now = time();
9722
9723
                        //If both "clocks" are enable
9724
                        if ($start_time && $end_time) {
9725
                            if ($now > $start_time && $end_time > $now) {
9726
                                $is_actived_time = true;
9727
                            }
9728
                        } else {
9729
                            //we check the start and end
9730
                            if ($start_time) {
9731
                                if ($now > $start_time) {
9732
                                    $is_actived_time = true;
9733
                                }
9734
                            }
9735
                            if ($end_time) {
9736
                                if ($end_time > $now) {
9737
                                    $is_actived_time = true;
9738
                                }
9739
                            }
9740
                        }
9741
                    }
9742
9743
                    // Blocking empty start times see BT#2800
9744
                    global $_custom;
9745
                    if (isset($_custom['exercises_hidden_when_no_start_date']) &&
9746
                        $_custom['exercises_hidden_when_no_start_date']
9747
                    ) {
9748
                        if (empty($row['start_time'])) {
9749
                            $time_limits = true;
9750
                            $is_actived_time = false;
9751
                        }
9752
                    }
9753
9754
                    $cut_title = $exercise->getCutTitle();
9755
                    $alt_title = '';
9756
                    if ($cut_title != $row['title']) {
9757
                        $alt_title = ' title = "'.$exercise->getUnformattedTitle().'" ';
9758
                    }
9759
9760
                    // Teacher only
9761
                    if ($isAllowedToEdit) {
9762
                        $lp_blocked = null;
9763
                        if ($exercise->exercise_was_added_in_lp == true) {
9764
                            $lp_blocked = Display::div(
9765
                                get_lang('AddedToLPCannotBeAccessed'),
9766
                                ['class' => 'lp_content_type_label']
9767
                            );
9768
                        }
9769
9770
                        // Get visibility in base course
9771
                        $visibility = api_get_item_visibility(
9772
                            $courseInfo,
9773
                            TOOL_QUIZ,
9774
                            $my_exercise_id,
9775
                            0
9776
                        );
9777
9778
                        if (!empty($sessionId)) {
9779
                            // If we are in a session, the test is invisible
9780
                            // in the base course, it is included in a LP
9781
                            // *and* the setting to show it is *not*
9782
                            // specifically set to true, then hide it.
9783
                            if ($visibility == 0) {
9784
                                if (!$visibilitySetting) {
9785
                                    if ($exercise->exercise_was_added_in_lp == true) {
9786
                                        continue;
9787
                                    }
9788
                                }
9789
                            }
9790
9791
                            $visibility = api_get_item_visibility(
9792
                                $courseInfo,
9793
                                TOOL_QUIZ,
9794
                                $my_exercise_id,
9795
                                $sessionId
9796
                            );
9797
                        }
9798
9799
                        if ($row['active'] == 0 || $visibility == 0) {
9800
                            $title = Display::tag('font', $cut_title, ['style' => 'color:grey']);
9801
                        } else {
9802
                            $title = $cut_title;
9803
                        }
9804
9805
                        $overviewUrl = api_get_path(WEB_CODE_PATH).'exercise/overview.php';
9806
                        $url = Security::remove_XSS(
9807
                            '<a
9808
                                '.$alt_title.'
9809
                                id="tooltip_'.$row['iid'].'"
9810
                                href="'.$overviewUrl.'?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$row['iid'].'"
9811
                            >
9812
                             '.Display::return_icon('quiz.png', $row['title']).'
9813
                             '.$title.'
9814
                             </a>');
9815
9816
                        if (ExerciseLib::isQuizEmbeddable($row)) {
9817
                            $embeddableIcon = Display::return_icon('om_integration.png', get_lang('ThisQuizCanBeEmbeddable'));
9818
                            $url .= Display::div($embeddableIcon, ['class' => 'pull-right']);
9819
                        }
9820
9821
                        $currentRow['title'] = $url.' '.$session_img.$lp_blocked;
9822
9823
                        if ($returnData) {
9824
                            $currentRow['title'] = $exercise->getUnformattedTitle();
9825
                        }
9826
9827
                        // Count number exercise - teacher
9828
                        $sql = "SELECT count(*) count FROM $TBL_EXERCISE_QUESTION
9829
                                WHERE c_id = $courseId AND exercice_id = $my_exercise_id";
9830
                        $sqlresult = Database::query($sql);
9831
                        $rowi = (int) Database::result($sqlresult, 0, 0);
9832
9833
                        if ($sessionId == $row['session_id']) {
9834
                            // Questions list
9835
                            $actions = Display::url(
9836
                                Display::return_icon('edit.png', get_lang('Edit'), '', ICON_SIZE_SMALL),
9837
                                'admin.php?'.api_get_cidreq().'&exerciseId='.$row['iid']
9838
                            );
9839
9840
                            // Test settings
9841
                            $settings = Display::url(
9842
                                Display::return_icon('settings.png', get_lang('Configure'), '', ICON_SIZE_SMALL),
9843
                                'exercise_admin.php?'.api_get_cidreq().'&exerciseId='.$row['iid']
9844
                            );
9845
9846
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
9847
                                $settings = '';
9848
                            }
9849
                            $actions .= $settings;
9850
9851
                            // Exercise results
9852
                            $resultsLink = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$row['iid'].'">'.
9853
                                Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
9854
9855
                            if ($limitTeacherAccess) {
9856
                                if (api_is_platform_admin()) {
9857
                                    $actions .= $resultsLink;
9858
                                }
9859
                            } else {
9860
                                // Exercise results
9861
                                $actions .= $resultsLink;
9862
                            }
9863
9864
                            // Auto launch
9865
                            if ($autoLaunchAvailable) {
9866
                                $autoLaunch = $exercise->getAutoLaunch();
9867
                                if (empty($autoLaunch)) {
9868
                                    $actions .= Display::url(
9869
                                        Display::return_icon(
9870
                                            'launch_na.png',
9871
                                            get_lang('Enable'),
9872
                                            '',
9873
                                            ICON_SIZE_SMALL
9874
                                        ),
9875
                                        'exercise.php?'.api_get_cidreq().'&choice=enable_launch&sec_token='.$token.'&exerciseId='.$row['iid']
9876
                                    );
9877
                                } else {
9878
                                    $actions .= Display::url(
9879
                                        Display::return_icon(
9880
                                            'launch.png',
9881
                                            get_lang('Disable'),
9882
                                            '',
9883
                                            ICON_SIZE_SMALL
9884
                                        ),
9885
                                        'exercise.php?'.api_get_cidreq().'&choice=disable_launch&sec_token='.$token.'&exerciseId='.$row['iid']
9886
                                    );
9887
                                }
9888
                            }
9889
9890
                            // Export
9891
                            $actions .= Display::url(
9892
                                Display::return_icon('cd.png', get_lang('CopyExercise')),
9893
                                '',
9894
                                [
9895
                                    'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('AreYouSureToCopy'), ENT_QUOTES, $charset))." ".addslashes($row['title'])."?"."')) return false;",
9896
                                    'href' => 'exercise.php?'.api_get_cidreq().'&choice=copy_exercise&sec_token='.$token.'&exerciseId='.$row['iid'],
9897
                                ]
9898
                            );
9899
9900
                            // Link to embed the quiz
9901
                            $urlEmbed = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&origin=iframe&exerciseId='.$row['iid'];
9902
                            $actions .= Display::url(
9903
                                Display::return_icon('new_link.png', get_lang('Embed')),
9904
                                '',
9905
                                [
9906
                                    'class' => 'ajax',
9907
                                    'data-title' => get_lang('EmbedExerciseLink'),
9908
                                    'title' => get_lang('EmbedExerciseLink'),
9909
                                    '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>',
9910
                                    'href' => 'javascript:void(0);',
9911
                                ]
9912
                            );
9913
9914
                            // Clean exercise
9915
                            $clean = '';
9916
                            if (true === $allowClean) {
9917
                                if (false == $locked) {
9918
                                    $clean = Display::url(
9919
                                        Display::return_icon(
9920
                                            'clean.png',
9921
                                            get_lang('CleanStudentResults'),
9922
                                            '',
9923
                                            ICON_SIZE_SMALL
9924
                                        ),
9925
                                        '',
9926
                                        [
9927
                                            'onclick' => "javascript:if(!confirm('".addslashes(
9928
                                                    api_htmlentities(
9929
                                                        get_lang('AreYouSureToDeleteResults'),
9930
                                                        ENT_QUOTES,
9931
                                                        $charset
9932
                                                    )
9933
                                                )." ".addslashes($row['title'])."?"."')) return false;",
9934
                                            'href' => 'exercise.php?'.api_get_cidreq(
9935
                                                ).'&choice=clean_results&sec_token='.$token.'&exerciseId='.$row['iid'],
9936
                                        ]
9937
                                    );
9938
                                } else {
9939
                                    $clean = Display::return_icon(
9940
                                        'clean_na.png',
9941
                                        get_lang('ResourceLockedByGradebook'),
9942
                                        '',
9943
                                        ICON_SIZE_SMALL
9944
                                    );
9945
                                }
9946
                            }
9947
9948
                            $actions .= $clean;
9949
9950
                            // Visible / invisible
9951
                            // Check if this exercise was added in a LP
9952
                            if ($exercise->exercise_was_added_in_lp == true) {
9953
                                $visibility = Display::return_icon(
9954
                                    'invisible.png',
9955
                                    get_lang('AddedToLPCannotBeAccessed'),
9956
                                    '',
9957
                                    ICON_SIZE_SMALL
9958
                                );
9959
                            } else {
9960
                                if ($row['active'] == 0 || $visibility == 0) {
9961
                                    $visibility = Display::url(
9962
                                        Display::return_icon(
9963
                                            'invisible.png',
9964
                                            get_lang('Activate'),
9965
                                            '',
9966
                                            ICON_SIZE_SMALL
9967
                                        ),
9968
                                        'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&exerciseId='.$row['iid']
9969
                                    );
9970
                                } else {
9971
                                    // else if not active
9972
                                    $visibility = Display::url(
9973
                                        Display::return_icon(
9974
                                            'visible.png',
9975
                                            get_lang('Deactivate'),
9976
                                            '',
9977
                                            ICON_SIZE_SMALL
9978
                                        ),
9979
                                        'exercise.php?'.api_get_cidreq().'&choice=disable&sec_token='.$token.'&exerciseId='.$row['iid']
9980
                                    );
9981
                                }
9982
                            }
9983
9984
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
9985
                                $visibility = '';
9986
                            }
9987
9988
                            $actions .= $visibility;
9989
9990
                            // Export qti ...
9991
                            $export = Display::url(
9992
                                Display::return_icon(
9993
                                    'export_qti2.png',
9994
                                    'IMS/QTI',
9995
                                    '',
9996
                                    ICON_SIZE_SMALL
9997
                                ),
9998
                                'exercise.php?action=exportqti2&exerciseId='.$row['iid'].'&'.api_get_cidreq()
9999
                            );
10000
10001
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
10002
                                $export = '';
10003
                            }
10004
10005
                            $actions .= $export;
10006
                        } else {
10007
                            // not session
10008
                            $actions = Display::return_icon(
10009
                                'edit_na.png',
10010
                                get_lang('ExerciseEditionNotAvailableInSession')
10011
                            );
10012
10013
                            // Check if this exercise was added in a LP
10014
                            if ($exercise->exercise_was_added_in_lp == true) {
10015
                                $visibility = Display::return_icon(
10016
                                    'invisible.png',
10017
                                    get_lang('AddedToLPCannotBeAccessed'),
10018
                                    '',
10019
                                    ICON_SIZE_SMALL
10020
                                );
10021
                            } else {
10022
                                if ($row['active'] == 0 || $visibility == 0) {
10023
                                    $visibility = Display::url(
10024
                                        Display::return_icon(
10025
                                            'invisible.png',
10026
                                            get_lang('Activate'),
10027
                                            '',
10028
                                            ICON_SIZE_SMALL
10029
                                        ),
10030
                                        'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&exerciseId='.$row['iid']
10031
                                    );
10032
                                } else {
10033
                                    // else if not active
10034
                                    $visibility = Display::url(
10035
                                        Display::return_icon(
10036
                                            'visible.png',
10037
                                            get_lang('Deactivate'),
10038
                                            '',
10039
                                            ICON_SIZE_SMALL
10040
                                        ),
10041
                                        'exercise.php?'.api_get_cidreq().'&choice=disable&sec_token='.$token.'&exerciseId='.$row['iid']
10042
                                    );
10043
                                }
10044
                            }
10045
10046
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
10047
                                $visibility = '';
10048
                            }
10049
10050
                            $actions .= $visibility;
10051
                            $actions .= '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$row['iid'].'">'.
10052
                                Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
10053
                            $actions .= Display::url(
10054
                                Display::return_icon('cd.gif', get_lang('CopyExercise')),
10055
                                '',
10056
                                [
10057
                                    'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('AreYouSureToCopy'), ENT_QUOTES, $charset))." ".addslashes($row['title'])."?"."')) return false;",
10058
                                    'href' => 'exercise.php?'.api_get_cidreq().'&choice=copy_exercise&sec_token='.$token.'&exerciseId='.$row['iid'],
10059
                                ]
10060
                            );
10061
                        }
10062
10063
                        // Delete
10064
                        $delete = '';
10065
                        if ($sessionId == $row['session_id']) {
10066
                            if ($locked == false) {
10067
                                $delete = Display::url(
10068
                                    Display::return_icon(
10069
                                        'delete.png',
10070
                                        get_lang('Delete'),
10071
                                        '',
10072
                                        ICON_SIZE_SMALL
10073
                                    ),
10074
                                    '',
10075
                                    [
10076
                                        'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('AreYouSureToDeleteJS'), ENT_QUOTES, $charset))." ".addslashes($exercise->getUnformattedTitle())."?"."')) return false;",
10077
                                        'href' => 'exercise.php?'.api_get_cidreq().'&choice=delete&sec_token='.$token.'&exerciseId='.$row['iid'],
10078
                                    ]
10079
                                );
10080
                            } else {
10081
                                $delete = Display::return_icon(
10082
                                    'delete_na.png',
10083
                                    get_lang('ResourceLockedByGradebook'),
10084
                                    '',
10085
                                    ICON_SIZE_SMALL
10086
                                );
10087
                            }
10088
                        }
10089
10090
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
10091
                            $delete = '';
10092
                        }
10093
10094
                        if (!empty($minCategoriesInExercise)) {
10095
                            $cats = TestCategory::getListOfCategoriesForTest($exercise);
10096
                            if (!(count($cats) >= $minCategoriesInExercise)) {
10097
                                continue;
10098
                            }
10099
                        }
10100
10101
                        $actions .= $delete;
10102
                        $usersToRemind = self::getUsersInExercise(
10103
                            $row['iid'],
10104
                            $row['c_id'],
10105
                            $row['session_id'],
10106
                            true
10107
                        );
10108
                        if ($usersToRemind > 0) {
10109
                            $actions .= Display::url(
10110
                                Display::return_icon('announce.png', get_lang('EmailNotifySubscription')),
10111
                                '',
10112
                                [
10113
                                    'href' => '#!',
10114
                                    'onclick' => 'showUserToSendNotificacion(this)',
10115
                                    'data-link' => 'exercise.php?'.api_get_cidreq()
10116
                                        .'&choice=send_reminder&sec_token='.$token.'&exerciseId='.$row['iid'],
10117
                                ]
10118
                            );
10119
                        }
10120
10121
                        // Number of questions
10122
                        $random_label = null;
10123
                        if ($row['random'] > 0 || $row['random'] == -1) {
10124
                            // if random == -1 means use random questions with all questions
10125
                            $random_number_of_question = $row['random'];
10126
                            if ($random_number_of_question == -1) {
10127
                                $random_number_of_question = $rowi;
10128
                            }
10129
                            if ($row['random_by_category'] > 0) {
10130
                                $nbQuestionsTotal = TestCategory::getNumberOfQuestionRandomByCategory(
10131
                                    $my_exercise_id,
10132
                                    $random_number_of_question
10133
                                );
10134
                                $number_of_questions = $nbQuestionsTotal.' ';
10135
                                $number_of_questions .= ($nbQuestionsTotal > 1) ? get_lang('QuestionsLowerCase') : get_lang('QuestionLowerCase');
10136
                                $number_of_questions .= ' - ';
10137
                                $number_of_questions .= min(
10138
                                        TestCategory::getNumberMaxQuestionByCat($my_exercise_id), $random_number_of_question
10139
                                    ).' '.get_lang('QuestionByCategory');
10140
                            } else {
10141
                                $random_label = ' ('.get_lang('Random').') ';
10142
                                $number_of_questions = $random_number_of_question.' '.$random_label.' / '.$rowi;
10143
                                // Bug if we set a random value bigger than the real number of questions
10144
                                if ($random_number_of_question > $rowi) {
10145
                                    $number_of_questions = $rowi.' '.$random_label;
10146
                                }
10147
                            }
10148
                        } else {
10149
                            $number_of_questions = $rowi;
10150
                        }
10151
10152
                        $currentRow['count_questions'] = $number_of_questions;
10153
                    } else {
10154
                        // Student only.
10155
                        $visibility = api_get_item_visibility(
10156
                            $courseInfo,
10157
                            TOOL_QUIZ,
10158
                            $my_exercise_id,
10159
                            $sessionId
10160
                        );
10161
10162
                        if ($visibility == 0) {
10163
                            continue;
10164
                        }
10165
10166
                        $url = '<a '.$alt_title.'  href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$row['iid'].'">'.
10167
                            $cut_title.'</a>';
10168
10169
                        // Link of the exercise.
10170
                        $currentRow['title'] = $url.' '.$session_img;
10171
10172
                        if ($returnData) {
10173
                            $currentRow['title'] = $exercise->getUnformattedTitle();
10174
                        }
10175
10176
                        // This query might be improved later on by ordering by the new "tms" field rather than by exe_id
10177
                        // Don't remove this marker: note-query-exe-results
10178
                        $sql = "SELECT * FROM $TBL_TRACK_EXERCISES
10179
                                WHERE
10180
                                    exe_exo_id = ".$row['iid']." AND
10181
                                    exe_user_id = $userId AND
10182
                                    c_id = ".api_get_course_int_id()." AND
10183
                                    status <> 'incomplete' AND
10184
                                    orig_lp_id = 0 AND
10185
                                    orig_lp_item_id = 0 AND
10186
                                    session_id =  '".api_get_session_id()."'
10187
                                ORDER BY exe_id DESC";
10188
10189
                        $qryres = Database::query($sql);
10190
                        $num = Database::num_rows($qryres);
10191
10192
                        // Hide the results.
10193
                        $my_result_disabled = $row['results_disabled'];
10194
10195
                        $attempt_text = '-';
10196
                        // Time limits are on
10197
                        if ($time_limits) {
10198
                            // Exam is ready to be taken
10199
                            if ($is_actived_time) {
10200
                                // Show results
10201
                                if (
10202
                                in_array(
10203
                                    $my_result_disabled,
10204
                                    [
10205
                                        RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
10206
                                        RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
10207
                                        RESULT_DISABLE_SHOW_SCORE_ONLY,
10208
                                        RESULT_DISABLE_RANKING,
10209
                                    ]
10210
                                )
10211
                                ) {
10212
                                    // More than one attempt
10213
                                    if ($num > 0) {
10214
                                        $row_track = Database::fetch_array($qryres);
10215
                                        $attempt_text = get_lang('LatestAttempt').' : ';
10216
                                        $attempt_text .= ExerciseLib::show_score(
10217
                                            $row_track['exe_result'],
10218
                                            $row_track['exe_weighting']
10219
                                        );
10220
                                    } else {
10221
                                        //No attempts
10222
                                        $attempt_text = get_lang('NotAttempted');
10223
                                    }
10224
                                } else {
10225
                                    $attempt_text = '-';
10226
                                }
10227
                            } else {
10228
                                // Quiz not ready due to time limits
10229
                                //@todo use the is_visible function
10230
                                if (!empty($row['start_time']) && !empty($row['end_time'])) {
10231
                                    $today = time();
10232
                                    $start_time = api_strtotime($row['start_time'], 'UTC');
10233
                                    $end_time = api_strtotime($row['end_time'], 'UTC');
10234
                                    if ($today < $start_time) {
10235
                                        $attempt_text = sprintf(
10236
                                            get_lang('ExerciseWillBeActivatedFromXToY'),
10237
                                            api_convert_and_format_date($row['start_time']),
10238
                                            api_convert_and_format_date($row['end_time'])
10239
                                        );
10240
                                    } else {
10241
                                        if ($today > $end_time) {
10242
                                            $attempt_text = sprintf(
10243
                                                get_lang('ExerciseWasActivatedFromXToY'),
10244
                                                api_convert_and_format_date($row['start_time']),
10245
                                                api_convert_and_format_date($row['end_time'])
10246
                                            );
10247
                                        }
10248
                                    }
10249
                                } else {
10250
                                    //$attempt_text = get_lang('ExamNotAvailableAtThisTime');
10251
                                    if (!empty($row['start_time'])) {
10252
                                        $attempt_text = sprintf(
10253
                                            get_lang('ExerciseAvailableFromX'),
10254
                                            api_convert_and_format_date($row['start_time'])
10255
                                        );
10256
                                    }
10257
                                    if (!empty($row['end_time'])) {
10258
                                        $attempt_text = sprintf(
10259
                                            get_lang('ExerciseAvailableUntilX'),
10260
                                            api_convert_and_format_date($row['end_time'])
10261
                                        );
10262
                                    }
10263
                                }
10264
                            }
10265
                        } else {
10266
                            // Normal behaviour.
10267
                            // Show results.
10268
                            if (
10269
                            in_array(
10270
                                $my_result_disabled,
10271
                                [
10272
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
10273
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
10274
                                    RESULT_DISABLE_SHOW_SCORE_ONLY,
10275
                                    RESULT_DISABLE_RANKING,
10276
                                    RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
10277
                                ]
10278
                            )
10279
                            ) {
10280
                                if ($num > 0) {
10281
                                    $row_track = Database::fetch_array($qryres);
10282
                                    $attempt_text = get_lang('LatestAttempt').' : ';
10283
                                    $attempt_text .= ExerciseLib::show_score(
10284
                                        $row_track['exe_result'],
10285
                                        $row_track['exe_weighting']
10286
                                    );
10287
                                } else {
10288
                                    $attempt_text = get_lang('NotAttempted');
10289
                                }
10290
                            }
10291
                        }
10292
10293
                        if ($returnData) {
10294
                            $attempt_text = $num;
10295
                        }
10296
                    }
10297
10298
                    $currentRow['attempt'] = $attempt_text;
10299
10300
                    if ($isAllowedToEdit) {
10301
                        $additionalActions = ExerciseLib::getAdditionalTeacherActions($row['iid']);
10302
10303
                        if (!empty($additionalActions)) {
10304
                            $actions .= $additionalActions.PHP_EOL;
10305
                        }
10306
10307
                        // Replace with custom actions.
10308
                        if (!empty($myActions) && is_callable($myActions)) {
10309
                            $actions = $myActions($row);
10310
                        }
10311
10312
                        $rowTitle = $currentRow['title'];
10313
                        $currentRow = [
10314
                            $row['iid'],
10315
                            $rowTitle,
10316
                            $currentRow['count_questions'],
10317
                            $actions,
10318
                        ];
10319
10320
                        if ($returnData) {
10321
                            $currentRow['id'] = $exercise->iid;
10322
                            $currentRow['url'] = $webPath.'exercise/overview.php?'
10323
                                .api_get_cidreq_params($courseInfo['code'], $sessionId).'&'
10324
                                ."$mylpid$mylpitemid&exerciseId={$row['iid']}";
10325
                            $currentRow['name'] = $rowTitle;
10326
                        }
10327
                    } else {
10328
                        $rowTitle = $currentRow['title'];
10329
                        $currentRow = [
10330
                            $rowTitle,
10331
                            $currentRow['attempt'],
10332
                        ];
10333
10334
                        if ($isDrhOfCourse) {
10335
                            $currentRow[] = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$row['iid'].'">'.
10336
                                Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
10337
                        }
10338
10339
                        if ($returnData) {
10340
                            $currentRow['id'] = $exercise->iid;
10341
                            $currentRow['url'] = $webPath.'exercise/overview.php?'
10342
                                .api_get_cidreq_params($courseInfo['code'], $sessionId).'&'
10343
                                ."$mylpid$mylpitemid&exerciseId={$row['iid']}";
10344
                            $currentRow['name'] = $rowTitle;
10345
                        }
10346
                    }
10347
10348
                    $tableRows[] = $currentRow;
10349
                }
10350
            }
10351
        }
10352
10353
        // end exercise list
10354
        // Hotpotatoes results
10355
        if ($isAllowedToEdit) {
10356
            $sql = "SELECT d.iid, d.path as path, d.comment as comment
10357
                    FROM $TBL_DOCUMENT d
10358
                    WHERE
10359
                        d.c_id = $courseId AND
10360
                        (d.path LIKE '%htm%') AND
10361
                        d.path  LIKE '".Database::escape_string($uploadPath.'/%/%')."'
10362
                    LIMIT $from , $limit"; // only .htm or .html files listed
10363
        } else {
10364
            $sql = "SELECT d.iid, d.path as path, d.comment as comment
10365
                    FROM $TBL_DOCUMENT d
10366
                    WHERE
10367
                        d.c_id = $courseId AND
10368
                        (d.path LIKE '%htm%') AND
10369
                        d.path  LIKE '".Database::escape_string($uploadPath.'/%/%')."'
10370
                    LIMIT $from , $limit";
10371
        }
10372
10373
        $result = Database::query($sql);
10374
        $attributes = [];
10375
        while ($row = Database::fetch_array($result, 'ASSOC')) {
10376
            $attributes[$row['iid']] = $row;
10377
        }
10378
10379
        $nbrActiveTests = 0;
10380
        if (!empty($attributes)) {
10381
            foreach ($attributes as $item) {
10382
                $id = $item['iid'];
10383
                $path = $item['path'];
10384
10385
                $title = GetQuizName($path, $documentPath);
10386
                if ($title == '') {
10387
                    $title = basename($path);
10388
                }
10389
10390
                // prof only
10391
                if ($isAllowedToEdit) {
10392
                    $visibility = api_get_item_visibility(
10393
                        ['real_id' => $courseId],
10394
                        TOOL_DOCUMENT,
10395
                        $id,
10396
                        0
10397
                    );
10398
10399
                    if (!empty($sessionId)) {
10400
                        if (0 == $visibility) {
10401
                            continue;
10402
                        }
10403
10404
                        $visibility = api_get_item_visibility(
10405
                            ['real_id' => $courseId],
10406
                            TOOL_DOCUMENT,
10407
                            $id,
10408
                            $sessionId
10409
                        );
10410
                    }
10411
10412
                    $title =
10413
                        implode(PHP_EOL, [
10414
                            Display::return_icon('hotpotatoes_s.png', 'HotPotatoes'),
10415
                            Display::url(
10416
                                $title,
10417
                                'showinframes.php?'.api_get_cidreq().'&'.http_build_query([
10418
                                    'file' => $path,
10419
                                    'uid' => $userId,
10420
                                ]),
10421
                                ['class' => $visibility == 0 ? 'text-muted' : null]
10422
                            ),
10423
                        ]);
10424
10425
                    $actions = Display::url(
10426
                        Display::return_icon(
10427
                            'edit.png',
10428
                            get_lang('Edit'),
10429
                            '',
10430
                            ICON_SIZE_SMALL
10431
                        ),
10432
                        'adminhp.php?'.api_get_cidreq().'&hotpotatoesName='.$path
10433
                    );
10434
10435
                    $actions .= '<a href="hotpotatoes_exercise_report.php?'.api_get_cidreq().'&path='.$path.'">'.
10436
                        Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).
10437
                        '</a>';
10438
10439
                    // if active
10440
                    if ($visibility != 0) {
10441
                        $nbrActiveTests = $nbrActiveTests + 1;
10442
                        $actions .= '      <a href="'.$exercisePath.'?'.api_get_cidreq().'&hpchoice=disable&file='.$path.'">'.
10443
                            Display::return_icon('visible.png', get_lang('Deactivate'), '', ICON_SIZE_SMALL).'</a>';
10444
                    } else { // else if not active
10445
                        $actions .= '    <a href="'.$exercisePath.'?'.api_get_cidreq().'&hpchoice=enable&file='.$path.'">'.
10446
                            Display::return_icon('invisible.png', get_lang('Activate'), '', ICON_SIZE_SMALL).'</a>';
10447
                    }
10448
                    $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;">'.
10449
                        Display::return_icon('delete.png', get_lang('Delete'), '', ICON_SIZE_SMALL).'</a>';
10450
10451
                    $currentRow = [
10452
                        '',
10453
                        $title,
10454
                        '',
10455
                        $actions,
10456
                    ];
10457
                } else {
10458
                    $visibility = api_get_item_visibility(
10459
                        ['real_id' => $courseId],
10460
                        TOOL_DOCUMENT,
10461
                        $id,
10462
                        $sessionId
10463
                    );
10464
10465
                    if (0 == $visibility) {
10466
                        continue;
10467
                    }
10468
10469
                    // Student only
10470
                    $attempt = ExerciseLib::getLatestHotPotatoResult(
10471
                        $path,
10472
                        $userId,
10473
                        api_get_course_int_id(),
10474
                        api_get_session_id()
10475
                    );
10476
10477
                    $nbrActiveTests = $nbrActiveTests + 1;
10478
                    $title = Display::url(
10479
                        $title,
10480
                        'showinframes.php?'.api_get_cidreq().'&'.http_build_query(
10481
                            [
10482
                                'file' => $path,
10483
                                'cid' => api_get_course_id(),
10484
                                'uid' => $userId,
10485
                            ]
10486
                        )
10487
                    );
10488
10489
                    if (!empty($attempt)) {
10490
                        $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>';
10491
                        $attemptText = get_lang('LatestAttempt').' : ';
10492
                        $attemptText .= ExerciseLib::show_score(
10493
                                $attempt['exe_result'],
10494
                                $attempt['exe_weighting']
10495
                            ).' ';
10496
                        $attemptText .= $actions;
10497
                    } else {
10498
                        // No attempts.
10499
                        $attemptText = get_lang('NotAttempted').' ';
10500
                    }
10501
10502
                    $currentRow = [
10503
                        $title,
10504
                        $attemptText,
10505
                    ];
10506
10507
                    if ($isDrhOfCourse) {
10508
                        $currentRow[] = '<a href="hotpotatoes_exercise_report.php?'.api_get_cidreq().'&path='.$path.'">'.
10509
                            Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
10510
                    }
10511
                }
10512
10513
                $tableRows[] = $currentRow;
10514
            }
10515
        }
10516
10517
        if ($returnData) {
10518
            return $tableRows;
10519
        }
10520
10521
        if (empty($tableRows) && empty($categoryId)) {
10522
            if ($isAllowedToEdit && $origin !== 'learnpath') {
10523
                $content .= '<div id="no-data-view">';
10524
                $content .= '<h3>'.get_lang('Quiz').'</h3>';
10525
                $content .= Display::return_icon('quiz.png', '', [], 64);
10526
                $content .= '<div class="controls">';
10527
                $content .= Display::url(
10528
                    '<em class="fa fa-plus"></em> '.get_lang('NewEx'),
10529
                    'exercise_admin.php?'.api_get_cidreq(),
10530
                    ['class' => 'btn btn-primary']
10531
                );
10532
                $content .= '</div>';
10533
                $content .= '</div>';
10534
            }
10535
        } else {
10536
            if (empty($tableRows)) {
10537
                return '';
10538
            }
10539
10540
            if (true === api_get_configuration_value('allow_exercise_categories') && empty($categoryId)) {
10541
                echo Display::page_subheader(get_lang('NoCategory'));
10542
            }
10543
10544
            $table->setTableData($tableRows);
10545
            $table->setTotalNumberOfItems($total);
10546
            $table->set_additional_parameters([
10547
                'cidReq' => api_get_course_id(),
10548
                'id_session' => api_get_session_id(),
10549
                'category_id' => $categoryId,
10550
            ]);
10551
10552
            if ($isAllowedToEdit) {
10553
                $formActions = [];
10554
                $formActions['visible'] = get_lang('Activate');
10555
                $formActions['invisible'] = get_lang('Deactivate');
10556
                $formActions['delete'] = get_lang('Delete');
10557
                $table->set_form_actions($formActions);
10558
            }
10559
10560
            $i = 0;
10561
            if ($isAllowedToEdit) {
10562
                $table->set_header($i++, '', false, 'width="18px"');
10563
            }
10564
            $table->set_header($i++, get_lang('ExerciseName'), false);
10565
10566
            if ($isAllowedToEdit) {
10567
                $table->set_header($i++, get_lang('QuantityQuestions'), false);
10568
                $table->set_header($i++, get_lang('Actions'), false);
10569
            } else {
10570
                $table->set_header($i++, get_lang('Status'), false);
10571
                if ($isDrhOfCourse) {
10572
                    $table->set_header($i++, get_lang('Actions'), false);
10573
                }
10574
            }
10575
10576
            if ($returnTable) {
10577
                return $table;
10578
            }
10579
10580
            $content .= $table->return_table();
10581
        }
10582
10583
        return $content;
10584
    }
10585
10586
    /**
10587
     * @return int value in minutes
10588
     */
10589
    public function getResultAccess()
10590
    {
10591
        $extraFieldValue = new ExtraFieldValue('exercise');
10592
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
10593
            $this->iid,
10594
            'results_available_for_x_minutes'
10595
        );
10596
10597
        if (!empty($value) && isset($value['value'])) {
10598
            return (int) $value['value'];
10599
        }
10600
10601
        return 0;
10602
    }
10603
10604
    /**
10605
     * @param array $exerciseResultInfo
10606
     *
10607
     * @return bool
10608
     */
10609
    public function getResultAccessTimeDiff($exerciseResultInfo)
10610
    {
10611
        $value = $this->getResultAccess();
10612
        if (!empty($value)) {
10613
            $endDate = new DateTime($exerciseResultInfo['exe_date'], new DateTimeZone('UTC'));
10614
            $endDate->add(new DateInterval('PT'.$value.'M'));
10615
            $now = time();
10616
            if ($endDate->getTimestamp() > $now) {
10617
                return (int) $endDate->getTimestamp() - $now;
10618
            }
10619
        }
10620
10621
        return 0;
10622
    }
10623
10624
    /**
10625
     * @param array $exerciseResultInfo
10626
     *
10627
     * @return bool
10628
     */
10629
    public function hasResultsAccess($exerciseResultInfo)
10630
    {
10631
        $diff = $this->getResultAccessTimeDiff($exerciseResultInfo);
10632
        if (0 === $diff) {
10633
            return false;
10634
        }
10635
10636
        return true;
10637
    }
10638
10639
    /**
10640
     * @return int
10641
     */
10642
    public function getResultsAccess()
10643
    {
10644
        $extraFieldValue = new ExtraFieldValue('exercise');
10645
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
10646
            $this->iid,
10647
            'results_available_for_x_minutes'
10648
        );
10649
        if (!empty($value)) {
10650
            return (int) $value;
10651
        }
10652
10653
        return 0;
10654
    }
10655
10656
    /**
10657
     * Get results of a delineation type question.
10658
     * Params described here are only non-typed params.
10659
     *
10660
     * @param int   $questionId
10661
     * @param bool  $show_results
10662
     * @param array $question_result
10663
     */
10664
    public function getDelineationResult(Question $objQuestionTmp, $questionId, $show_results, $question_result)
10665
    {
10666
        $id = $objQuestionTmp->iid;
10667
        $questionId = (int) $questionId;
10668
10669
        $final_overlap = $question_result['extra']['final_overlap'];
10670
        $final_missing = $question_result['extra']['final_missing'];
10671
        $final_excess = $question_result['extra']['final_excess'];
10672
10673
        $overlap_color = $question_result['extra']['overlap_color'];
10674
        $missing_color = $question_result['extra']['missing_color'];
10675
        $excess_color = $question_result['extra']['excess_color'];
10676
10677
        $threadhold1 = $question_result['extra']['threadhold1'];
10678
        $threadhold2 = $question_result['extra']['threadhold2'];
10679
        $threadhold3 = $question_result['extra']['threadhold3'];
10680
10681
        if ($show_results) {
10682
            if ($overlap_color) {
10683
                $overlap_color = 'green';
10684
            } else {
10685
                $overlap_color = 'red';
10686
            }
10687
10688
            if ($missing_color) {
10689
                $missing_color = 'green';
10690
            } else {
10691
                $missing_color = 'red';
10692
            }
10693
            if ($excess_color) {
10694
                $excess_color = 'green';
10695
            } else {
10696
                $excess_color = 'red';
10697
            }
10698
10699
            if (!is_numeric($final_overlap)) {
10700
                $final_overlap = 0;
10701
            }
10702
10703
            if (!is_numeric($final_missing)) {
10704
                $final_missing = 0;
10705
            }
10706
            if (!is_numeric($final_excess)) {
10707
                $final_excess = 0;
10708
            }
10709
10710
            if ($final_excess > 100) {
10711
                $final_excess = 100;
10712
            }
10713
10714
            $table_resume = '
10715
                    <table class="table table-hover table-striped data_table">
10716
                        <tr class="row_odd" >
10717
                            <td>&nbsp;</td>
10718
                            <td><b>'.get_lang('Requirements').'</b></td>
10719
                            <td><b>'.get_lang('YourAnswer').'</b></td>
10720
                        </tr>
10721
                        <tr class="row_even">
10722
                            <td><b>'.get_lang('Overlap').'</b></td>
10723
                            <td>'.get_lang('Min').' '.$threadhold1.'</td>
10724
                            <td>
10725
                                <div style="color:'.$overlap_color.'">
10726
                                    '.(($final_overlap < 0) ? 0 : intval($final_overlap)).'
10727
                                </div>
10728
                            </td>
10729
                        </tr>
10730
                        <tr>
10731
                            <td><b>'.get_lang('Excess').'</b></td>
10732
                            <td>'.get_lang('Max').' '.$threadhold2.'</td>
10733
                            <td>
10734
                                <div style="color:'.$excess_color.'">
10735
                                    '.(($final_excess < 0) ? 0 : intval($final_excess)).'
10736
                                </div>
10737
                            </td>
10738
                        </tr>
10739
                        <tr class="row_even">
10740
                            <td><b>'.get_lang('Missing').'</b></td>
10741
                            <td>'.get_lang('Max').' '.$threadhold3.'</td>
10742
                            <td>
10743
                                <div style="color:'.$missing_color.'">
10744
                                    '.(($final_missing < 0) ? 0 : intval($final_missing)).'
10745
                                </div>
10746
                            </td>
10747
                        </tr>
10748
                    </table>
10749
                ';
10750
10751
            $answerType = $objQuestionTmp->selectType();
10752
            if ($answerType != HOT_SPOT_DELINEATION) {
10753
                $item_list = explode('@@', $destination);
10754
                $try = $item_list[0];
10755
                $lp = $item_list[1];
10756
                $destinationid = $item_list[2];
10757
                $url = $item_list[3];
10758
                $table_resume = '';
10759
            } else {
10760
                if ($next == 0) {
10761
                    $try = $try_hotspot;
10762
                    $lp = $lp_hotspot;
10763
                    $destinationid = $select_question_hotspot;
10764
                    $url = $url_hotspot;
10765
                } else {
10766
                    //show if no error
10767
                    $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
10768
                    $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
10769
                }
10770
            }
10771
10772
            echo '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>';
10773
            if ($answerType == HOT_SPOT_DELINEATION) {
10774
                if ($organs_at_risk_hit > 0) {
10775
                    $message = '<br />'.get_lang('ResultIs').' <b>'.$result_comment.'</b><br />';
10776
                    $message .= '<p style="color:#DC0A0A;"><b>'.get_lang('OARHit').'</b></p>';
10777
                } else {
10778
                    $message = '<p>'.get_lang('YourDelineation').'</p>';
10779
                    $message .= $table_resume;
10780
                    $message .= '<br />'.get_lang('ResultIs').' <b>'.$result_comment.'</b><br />';
10781
                }
10782
                $message .= '<p>'.$comment.'</p>';
10783
                echo $message;
10784
            } else {
10785
                echo '<p>'.$comment.'</p>';
10786
            }
10787
10788
            // Showing the score
10789
            /*$queryfree = "SELECT marks FROM $TBL_TRACK_ATTEMPT
10790
                          WHERE exe_id = $id AND question_id =  $questionId";
10791
            $resfree = Database::query($queryfree);
10792
            $questionScore = Database::result($resfree, 0, 'marks');
10793
            $totalScore += $questionScore;*/
10794
            $relPath = api_get_path(REL_CODE_PATH);
10795
            echo '</table></td></tr>';
10796
            echo "
10797
                        <tr>
10798
                            <td colspan=\"2\">
10799
                                <div id=\"hotspot-solution\"></div>
10800
                                <script>
10801
                                    $(function() {
10802
                                        new HotspotQuestion({
10803
                                            questionId: $questionId,
10804
                                            exerciseId: {$this->iid},
10805
                                            exeId: $id,
10806
                                            selector: '#hotspot-solution',
10807
                                            for: 'solution',
10808
                                            relPath: '$relPath'
10809
                                        });
10810
                                    });
10811
                                </script>
10812
                            </td>
10813
                        </tr>
10814
                    </table>
10815
                ";
10816
        }
10817
    }
10818
10819
    /**
10820
     * Clean exercise session variables.
10821
     */
10822
    public static function cleanSessionVariables()
10823
    {
10824
        Session::erase('objExercise');
10825
        Session::erase('exe_id');
10826
        Session::erase('calculatedAnswerId');
10827
        Session::erase('duration_time_previous');
10828
        Session::erase('duration_time');
10829
        Session::erase('objQuestion');
10830
        Session::erase('objAnswer');
10831
        Session::erase('questionList');
10832
        Session::erase('categoryList');
10833
        Session::erase('exerciseResult');
10834
        Session::erase('firstTime');
10835
        Session::erase('time_per_question');
10836
        Session::erase('question_start');
10837
        Session::erase('exerciseResultCoordinates');
10838
        Session::erase('hotspot_coord');
10839
        Session::erase('hotspot_dest');
10840
        Session::erase('hotspot_delineation_result');
10841
    }
10842
10843
    /**
10844
     * Get the first LP found matching the session ID.
10845
     *
10846
     * @param int $sessionId
10847
     *
10848
     * @return array
10849
     */
10850
    public function getLpBySession($sessionId)
10851
    {
10852
        if (!empty($this->lpList)) {
10853
            $sessionId = (int) $sessionId;
10854
10855
            foreach ($this->lpList as $lp) {
10856
                if ((int) $lp['session_id'] == $sessionId) {
10857
                    return $lp;
10858
                }
10859
            }
10860
10861
            return current($this->lpList);
10862
        }
10863
10864
        return [
10865
            'lp_id' => 0,
10866
            'max_score' => 0,
10867
            'session_id' => 0,
10868
        ];
10869
    }
10870
10871
    public static function saveExerciseInLp($safe_item_id, $safe_exe_id)
10872
    {
10873
        $lp = Session::read('oLP');
10874
10875
        $safe_exe_id = (int) $safe_exe_id;
10876
        $safe_item_id = (int) $safe_item_id;
10877
10878
        if (empty($lp) || empty($safe_exe_id) || empty($safe_item_id)) {
10879
            return false;
10880
        }
10881
10882
        $viewId = $lp->get_view_id();
10883
        $course_id = api_get_course_int_id();
10884
        $userId = (int) api_get_user_id();
10885
        $viewId = (int) $viewId;
10886
10887
        $TBL_TRACK_EXERCICES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
10888
        $TBL_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
10889
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
10890
10891
        $sql = "SELECT start_date, exe_date, exe_result, exe_weighting, exe_exo_id, exe_duration
10892
                FROM $TBL_TRACK_EXERCICES
10893
                WHERE exe_id = $safe_exe_id AND exe_user_id = $userId";
10894
        $res = Database::query($sql);
10895
        $row_dates = Database::fetch_array($res);
10896
10897
        if (empty($row_dates)) {
10898
            return false;
10899
        }
10900
10901
        $duration = (int) $row_dates['exe_duration'];
10902
        $score = (float) $row_dates['exe_result'];
10903
        $max_score = (float) $row_dates['exe_weighting'];
10904
10905
        $sql = "UPDATE $TBL_LP_ITEM SET
10906
                    max_score = '$max_score'
10907
                WHERE iid = $safe_item_id";
10908
        Database::query($sql);
10909
10910
        $sql = "SELECT iid FROM $TBL_LP_ITEM_VIEW
10911
                WHERE
10912
                    c_id = $course_id AND
10913
                    lp_item_id = $safe_item_id AND
10914
                    lp_view_id = $viewId
10915
                ORDER BY id DESC
10916
                LIMIT 1";
10917
        $res_last_attempt = Database::query($sql);
10918
10919
        if (Database::num_rows($res_last_attempt) && !api_is_invitee()) {
10920
            $row_last_attempt = Database::fetch_row($res_last_attempt);
10921
            $lp_item_view_id = $row_last_attempt[0];
10922
10923
            $exercise = new Exercise($course_id);
10924
            $exercise->read($row_dates['exe_exo_id']);
10925
            $status = 'completed';
10926
10927
            if (!empty($exercise->pass_percentage)) {
10928
                $status = 'failed';
10929
                $success = ExerciseLib::isSuccessExerciseResult(
10930
                    $score,
10931
                    $max_score,
10932
                    $exercise->pass_percentage
10933
                );
10934
                if ($success) {
10935
                    $status = 'passed';
10936
                }
10937
            }
10938
10939
            $sql = "UPDATE $TBL_LP_ITEM_VIEW SET
10940
                        status = '$status',
10941
                        score = $score,
10942
                        total_time = $duration
10943
                    WHERE iid = $lp_item_view_id";
10944
            Database::query($sql);
10945
10946
            $sql = "UPDATE $TBL_TRACK_EXERCICES SET
10947
                        orig_lp_item_view_id = $lp_item_view_id
10948
                    WHERE exe_id = ".$safe_exe_id;
10949
            Database::query($sql);
10950
        }
10951
    }
10952
10953
    /**
10954
     * Get the user answers saved in exercise.
10955
     *
10956
     * @param int $attemptId
10957
     *
10958
     * @return array
10959
     */
10960
    public function getUserAnswersSavedInExercise($attemptId)
10961
    {
10962
        $exerciseResult = [];
10963
        $attemptList = Event::getAllExerciseEventByExeId($attemptId);
10964
10965
        foreach ($attemptList as $questionId => $options) {
10966
            foreach ($options as $option) {
10967
                $question = Question::read($option['question_id']);
10968
                if ($question) {
10969
                    switch ($question->type) {
10970
                        case FILL_IN_BLANKS:
10971
                        case FILL_IN_BLANKS_COMBINATION:
10972
                            $option['answer'] = $this->fill_in_blank_answer_to_string($option['answer']);
10973
                            if ($option['answer'] === "0") {
10974
                                $option['answer'] = "there is 0 as answer so we do not want to consider it empty";
10975
                            }
10976
                            break;
10977
                    }
10978
                }
10979
10980
                if (!empty($option['answer'])) {
10981
                    $exerciseResult[] = $questionId;
10982
10983
                    break;
10984
                }
10985
            }
10986
        }
10987
10988
        return $exerciseResult;
10989
    }
10990
10991
    /**
10992
     * Get the number of user answers saved in exercise.
10993
     *
10994
     * @param int $attemptId
10995
     *
10996
     * @return int
10997
     */
10998
    public function countUserAnswersSavedInExercise($attemptId)
10999
    {
11000
        $answers = $this->getUserAnswersSavedInExercise($attemptId);
11001
11002
        return count($answers);
11003
    }
11004
11005
    public static function allowAction($action)
11006
    {
11007
        if (api_is_platform_admin()) {
11008
            return true;
11009
        }
11010
11011
        $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
11012
        $disableClean = api_get_configuration_value('disable_clean_exercise_results_for_teachers');
11013
11014
        switch ($action) {
11015
            case 'delete':
11016
                if (api_is_allowed_to_edit(null, true)) {
11017
                    if ($limitTeacherAccess) {
11018
                        return false;
11019
                    }
11020
11021
                    return true;
11022
                }
11023
                break;
11024
            case 'clean_results':
11025
                if (api_is_allowed_to_edit(null, true)) {
11026
                    if ($limitTeacherAccess) {
11027
                        return false;
11028
                    }
11029
11030
                    if ($disableClean) {
11031
                        return false;
11032
                    }
11033
11034
                    return true;
11035
                }
11036
11037
                break;
11038
        }
11039
11040
        return false;
11041
    }
11042
11043
    public static function getLpListFromExercise($exerciseId, $courseId)
11044
    {
11045
        $tableLpItem = Database::get_course_table(TABLE_LP_ITEM);
11046
        $tblLp = Database::get_course_table(TABLE_LP_MAIN);
11047
11048
        $exerciseId = (int) $exerciseId;
11049
        $courseId = (int) $courseId;
11050
11051
        $sql = "SELECT
11052
                    lp.name,
11053
                    lpi.lp_id,
11054
                    lpi.max_score,
11055
                    lp.session_id
11056
                FROM $tableLpItem lpi
11057
                INNER JOIN $tblLp lp
11058
                ON (lpi.lp_id = lp.iid AND lpi.c_id = lp.c_id)
11059
                WHERE
11060
                    lpi.c_id = $courseId AND
11061
                    lpi.item_type = '".TOOL_QUIZ."' AND
11062
                    lpi.path = '$exerciseId'";
11063
        $result = Database::query($sql);
11064
        $lpList = [];
11065
        if (Database::num_rows($result) > 0) {
11066
            $lpList = Database::store_result($result, 'ASSOC');
11067
        }
11068
11069
        return $lpList;
11070
    }
11071
11072
    public function getReminderTable($questionList, $exercise_stat_info, $disableCheckBoxes = false)
11073
    {
11074
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
11075
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
11076
        $learnpath_item_view_id = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
11077
        $categoryId = isset($_REQUEST['category_id']) ? (int) $_REQUEST['category_id'] : 0;
11078
11079
        if (empty($exercise_stat_info)) {
11080
            return '';
11081
        }
11082
11083
        $remindList = $exercise_stat_info['questions_to_check'];
11084
        $remindList = explode(',', $remindList);
11085
        $exeId = $exercise_stat_info['exe_id'];
11086
        $exerciseId = $exercise_stat_info['exe_exo_id'];
11087
        $exercise_result = $this->getUserAnswersSavedInExercise($exeId);
11088
11089
        $content = Display::label(get_lang('QuestionWithNoAnswer'), 'danger');
11090
        $content .= '<div class="clear"></div><br />';
11091
        $table = '';
11092
        $counter = 0;
11093
        // Loop over all question to show results for each of them, one by one
11094
        foreach ($questionList as $questionId) {
11095
            $objQuestionTmp = Question::read($questionId);
11096
            $check_id = 'remind_list['.$questionId.']';
11097
            $attributes = [
11098
                'id' => $check_id,
11099
                'onclick' => "save_remind_item(this, '$questionId');",
11100
                'data-question-id' => $questionId,
11101
            ];
11102
            if (in_array($questionId, $remindList)) {
11103
                $attributes['checked'] = 1;
11104
            }
11105
11106
            $checkbox = Display::input('checkbox', 'remind_list['.$questionId.']', '', $attributes);
11107
            $checkbox = '<div class="pretty p-svg p-curve">
11108
                        '.$checkbox.'
11109
                        <div class="state p-primary ">
11110
                         <svg class="svg svg-icon" viewBox="0 0 20 20">
11111
                            <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>
11112
                         </svg>
11113
                         <label>&nbsp;</label>
11114
                        </div>
11115
                    </div>';
11116
            $counter++;
11117
            $questionTitle = $counter.'. '.strip_tags($objQuestionTmp->selectTitle());
11118
            // Check if the question doesn't have an answer.
11119
            if (!in_array($questionId, $exercise_result)) {
11120
                $questionTitle = Display::label($questionTitle, 'danger');
11121
            }
11122
            $label_attributes = [];
11123
            $label_attributes['for'] = $check_id;
11124
            if (false === $disableCheckBoxes) {
11125
                $questionTitle = Display::tag('label', $checkbox.$questionTitle, $label_attributes);
11126
            }
11127
            $table .= Display::div($questionTitle, ['class' => 'exercise_reminder_item ']);
11128
        }
11129
11130
        $content .= Display::div('', ['id' => 'message']).
11131
                    Display::div($table, ['class' => 'question-check-test']);
11132
11133
        $content .= '<script>
11134
        var lp_data = $.param({
11135
            "learnpath_id": '.$learnpath_id.',
11136
            "learnpath_item_id" : '.$learnpath_item_id.',
11137
            "learnpath_item_view_id": '.$learnpath_item_view_id.'
11138
        });
11139
11140
        function final_submit() {
11141
            // Normal inputs.
11142
            window.location = "'.api_get_path(WEB_CODE_PATH).'exercise/exercise_result.php?'.api_get_cidreq().'&exe_id='.$exeId.'&" + lp_data;
11143
        }
11144
11145
        function selectAll() {
11146
            $("input[type=checkbox]").each(function () {
11147
                $(this).prop("checked", 1);
11148
                var question_id = $(this).data("question-id");
11149
                var action = "add";
11150
                $.ajax({
11151
                    url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
11152
                    data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
11153
                    success: function(returnValue) {
11154
                    }
11155
                });
11156
            });
11157
        }
11158
11159
        function changeOptionStatus(status)
11160
        {
11161
            $("input[type=checkbox]").each(function () {
11162
                $(this).prop("checked", status);
11163
            });
11164
            var action = "";
11165
            var option = "remove_all";
11166
            if (status == 1) {
11167
                option = "add_all";
11168
            }
11169
            $.ajax({
11170
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
11171
                data: "option="+option+"&exe_id='.$exeId.'&action="+action,
11172
                success: function(returnValue) {
11173
                }
11174
            });
11175
        }
11176
11177
        function reviewQuestions() {
11178
            var isChecked = 1;
11179
            $("input[type=checkbox]").each(function () {
11180
                if ($(this).prop("checked")) {
11181
                    isChecked = 2;
11182
                    return false;
11183
                }
11184
            });
11185
11186
            if (isChecked == 1) {
11187
                $("#message").addClass("warning-message");
11188
                $("#message").html("'.addslashes(get_lang('SelectAQuestionToReview')).'");
11189
            } else {
11190
                window.location = "exercise_submit.php?'.api_get_cidreq().'&category_id='.$categoryId.'&exerciseId='.$exerciseId.'&reminder=2&" + lp_data;
11191
            }
11192
        }
11193
11194
        function save_remind_item(obj, question_id) {
11195
            var action = "";
11196
            if ($(obj).prop("checked")) {
11197
                action = "add";
11198
            } else {
11199
                action = "delete";
11200
            }
11201
            $.ajax({
11202
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
11203
                data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
11204
                success: function(returnValue) {
11205
                }
11206
            });
11207
        }
11208
        </script>';
11209
11210
        return $content;
11211
    }
11212
11213
    public function getRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
11214
    {
11215
        $dataSet = [];
11216
        $labels = [];
11217
        $labelsWithId = [];
11218
        $cutLabelAtChar = 30;
11219
        /** @var Exercise $exercise */
11220
        foreach ($exercises as $exercise) {
11221
            if (empty($labels)) {
11222
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iid);
11223
                if (!empty($categoryNameList)) {
11224
                    $labelsWithId = array_column($categoryNameList, 'title', 'iid');
11225
                    asort($labelsWithId);
11226
                    $labels = array_values($labelsWithId);
11227
                    foreach ($labels as $labelId => $label) {
11228
                        // Cut if label is too long to maintain chart visibility
11229
                        $labels[$labelId] = cut($label, $cutLabelAtChar);
11230
                    }
11231
                }
11232
            }
11233
11234
            foreach ($userList as $userId) {
11235
                $results = Event::getExerciseResultsByUser(
11236
                    $userId,
11237
                    $exercise->iid,
11238
                    $courseId,
11239
                    $sessionId
11240
                );
11241
11242
                if ($results) {
11243
                    $firstAttempt = end($results);
11244
                    $exeId = $firstAttempt['exe_id'];
11245
11246
                    ob_start();
11247
                    $stats = ExerciseLib::displayQuestionListByAttempt(
11248
                        $exercise,
11249
                        $exeId,
11250
                        false
11251
                    );
11252
                    ob_end_clean();
11253
11254
                    $categoryList = $stats['category_list'];
11255
                    $tempResult = [];
11256
                    foreach ($labelsWithId as $category_id => $title) {
11257
                        if (isset($categoryList[$category_id])) {
11258
                            $categoryItem = $categoryList[$category_id];
11259
                            if ($categoryItem['total'] > 0) {
11260
                                $tempResult[] = round($categoryItem['score'] / $categoryItem['total'] * 10);
11261
                            } else {
11262
                                $tempResult[] = 0;
11263
                            }
11264
                        } else {
11265
                            $tempResult[] = 0;
11266
                        }
11267
                    }
11268
                    $dataSet[] = $tempResult;
11269
                }
11270
            }
11271
        }
11272
11273
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
11274
    }
11275
11276
    public function getAverageRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
11277
    {
11278
        $dataSet = [];
11279
        $labels = [];
11280
        $labelsWithId = [];
11281
        $cutLabelAtChar = 30;
11282
        $tempResult = [];
11283
        /** @var Exercise $exercise */
11284
        foreach ($exercises as $exercise) {
11285
            $exerciseId = $exercise->iid;
11286
            if (empty($labels)) {
11287
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iid);
11288
                if (!empty($categoryNameList)) {
11289
                    $labelsWithId = array_column($categoryNameList, 'title', 'iid');
11290
                    asort($labelsWithId);
11291
                    $labels = array_values($labelsWithId);
11292
                    foreach ($labels as $labelId => $label) {
11293
                        // Cut if label is too long to maintain chart visibility
11294
                        $labels[$labelId] = cut($label, $cutLabelAtChar);
11295
                    }
11296
                }
11297
            }
11298
11299
            foreach ($userList as $userId) {
11300
                $results = Event::getExerciseResultsByUser(
11301
                    $userId,
11302
                    $exerciseId,
11303
                    $courseId,
11304
                    $sessionId
11305
                );
11306
11307
                if ($results) {
11308
                    $firstAttempt = end($results);
11309
                    $exeId = $firstAttempt['exe_id'];
11310
11311
                    ob_start();
11312
                    $stats = ExerciseLib::displayQuestionListByAttempt(
11313
                        $exercise,
11314
                        $exeId,
11315
                        false
11316
                    );
11317
                    ob_end_clean();
11318
11319
                    $categoryList = $stats['category_list'];
11320
                    foreach ($labelsWithId as $category_id => $title) {
11321
                        if (isset($categoryList[$category_id])) {
11322
                            $categoryItem = $categoryList[$category_id];
11323
                            if (!isset($tempResult[$exerciseId][$category_id])) {
11324
                                $tempResult[$exerciseId][$category_id] = 0;
11325
                            }
11326
                            $tempResult[$exerciseId][$category_id] +=
11327
                                $categoryItem['score'] / $categoryItem['total'] * 10;
11328
                        }
11329
                    }
11330
                }
11331
            }
11332
        }
11333
11334
        foreach ($exercises as $exercise) {
11335
            $exerciseId = $exercise->iid;
11336
            $totalResults = ExerciseLib::getNumberStudentsFinishExercise($exerciseId, $courseId, $sessionId);
11337
            $data = [];
11338
            foreach ($labelsWithId as $category_id => $title) {
11339
                if (isset($tempResult[$exerciseId]) && isset($tempResult[$exerciseId][$category_id])) {
11340
                    $data[] = round($tempResult[$exerciseId][$category_id] / $totalResults);
11341
                } else {
11342
                    $data[] = 0;
11343
                }
11344
            }
11345
            $dataSet[] = $data;
11346
        }
11347
11348
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
11349
    }
11350
11351
    public function getRadar($labels, $dataSet, $dataSetLabels = [])
11352
    {
11353
        if (empty($labels) || empty($dataSet)) {
11354
            return '';
11355
        }
11356
11357
        $displayLegend = 0;
11358
        if (!empty($dataSetLabels)) {
11359
            $displayLegend = 1;
11360
        }
11361
11362
        $labels = json_encode($labels);
11363
11364
        $colorList = ChamiloApi::getColorPalette(true, true);
11365
11366
        $dataSetToJson = [];
11367
        $counter = 0;
11368
        foreach ($dataSet as $index => $resultsArray) {
11369
            $color = isset($colorList[$counter]) ? $colorList[$counter] : 'rgb('.rand(0, 255).', '.rand(0, 255).', '.rand(0, 255).', 1.0)';
11370
11371
            $label = isset($dataSetLabels[$index]) ? $dataSetLabels[$index] : '';
11372
            $background = str_replace('1.0', '0.2', $color);
11373
            $dataSetToJson[] = [
11374
                'fill' => false,
11375
                'label' => $label,
11376
                'backgroundColor' => $background,
11377
                'borderColor' => $color,
11378
                'pointBackgroundColor' => $color,
11379
                'pointBorderColor' => '#fff',
11380
                'pointHoverBackgroundColor' => '#fff',
11381
                'pointHoverBorderColor' => $color,
11382
                'pointRadius' => 6,
11383
                'pointBorderWidth' => 3,
11384
                'pointHoverRadius' => 10,
11385
                'data' => $resultsArray,
11386
            ];
11387
            $counter++;
11388
        }
11389
        $resultsToJson = json_encode($dataSetToJson);
11390
11391
        return "
11392
                <canvas id='categoryRadar' height='200'></canvas>
11393
                <script>
11394
                    var data = {
11395
                        labels: $labels,
11396
                        datasets: $resultsToJson
11397
                    }
11398
                    var options = {
11399
                        responsive: true,
11400
                        scale: {
11401
                            angleLines: {
11402
                                display: false
11403
                            },
11404
                            ticks: {
11405
                                beginAtZero: true,
11406
                                min: 0,
11407
                                max: 10,
11408
                                stepSize: 1,
11409
                            },
11410
                            pointLabels: {
11411
                              fontSize: 14,
11412
                              //fontStyle: 'bold'
11413
                            },
11414
                        },
11415
                        elements: {
11416
                            line: {
11417
                                tension: 0,
11418
                                borderWidth: 3
11419
                            }
11420
                        },
11421
                        legend: {
11422
                            //position: 'bottom'
11423
                            display: $displayLegend
11424
                        },
11425
                        animation: {
11426
                            animateScale: true,
11427
                            animateRotate: true
11428
                        },
11429
                    };
11430
                    var ctx = document.getElementById('categoryRadar').getContext('2d');
11431
                    var myRadarChart = new Chart(ctx, {
11432
                        type: 'radar',
11433
                        data: data,
11434
                        options: options
11435
                    });
11436
                </script>
11437
                ";
11438
    }
11439
11440
    /**
11441
     * Returns a list of users based on the id of an exercise, the course and the session.
11442
     * If the count is true, it returns only the number of users.
11443
     *
11444
     * @param int   $exerciseId
11445
     * @param int   $courseId
11446
     * @param int   $sessionId
11447
     * @param false $count
11448
     */
11449
    public static function getUsersInExercise(
11450
        $exerciseId = 0,
11451
        $courseId = 0,
11452
        $sessionId = 0,
11453
        $count = false,
11454
        $toUsers = [],
11455
        $withSelectAll = true
11456
    ) {
11457
        $sessionId = empty($sessionId) ? api_get_session_id() : (int) $sessionId;
11458
        $courseId = empty($courseId) ? api_get_course_id() : (int) $courseId;
11459
        $exerciseId = (int) $exerciseId;
11460
        if ((0 == $sessionId && 0 == $courseId) || 0 == $exerciseId) {
11461
            return [];
11462
        }
11463
        $tblCourse = Database::get_main_table(TABLE_MAIN_COURSE);
11464
        $tblCourseRelUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
11465
        $tblSessionRelUser = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
11466
11467
        $data = [];
11468
11469
        $tblQuiz = Database::get_course_table(TABLE_QUIZ_TEST);
11470
        $countSelect = " COUNT(*) AS total ";
11471
        $sqlToUsers = '';
11472
        if (0 == $sessionId) {
11473
            // Courses
11474
            if (false === $count) {
11475
                $countSelect = " cq.title AS quiz_title,
11476
                    cq.iid AS quiz_id,
11477
                    cru.c_id AS course_id,
11478
                    cru.user_id AS user_id,
11479
                    c.title AS title,
11480
                    c.code AS 'code',
11481
                    cq.active AS active,
11482
                    cq.session_id AS session_id ";
11483
            }
11484
11485
            $sql = "SELECT $countSelect
11486
                FROM $tblCourseRelUser AS cru
11487
                INNER JOIN $tblCourse AS c ON ( cru.c_id = c.id )
11488
                INNER JOIN $tblQuiz AS cq ON ( cq.c_id = c.id )
11489
                WHERE cru.is_tutor IS NULL
11490
                    AND ( cq.session_id = 0 OR cq.session_id IS NULL)
11491
                    AND cq.active != -1
11492
                    AND cq.c_id = $courseId
11493
                    AND cq.iid = $exerciseId ";
11494
            if (!empty($toUsers)) {
11495
                $sqlToUsers = ' AND cru.user_id IN ('.implode(',', $toUsers).') ';
11496
            }
11497
        } else {
11498
            //Sessions
11499
            if (false === $count) {
11500
                $countSelect = " cq.title AS quiz_title,
11501
                    cq.iid AS quiz_id,
11502
                    sru.user_id AS user_id,
11503
                    cq.c_id AS course_id,
11504
                    sru.session_id AS session_id,
11505
                    c.title AS title,
11506
                    c.code AS 'code',
11507
                    cq.active AS active ";
11508
            }
11509
            if (!empty($toUsers)) {
11510
                $sqlToUsers = ' AND sru.user_id IN ('.implode(',', $toUsers).') ';
11511
            }
11512
            $sql = " SELECT $countSelect
11513
                FROM $tblSessionRelUser AS sru
11514
                    INNER JOIN $tblQuiz AS cq ON ( sru.session_id = sru.session_id )
11515
                    INNER JOIN $tblCourse AS c ON ( c.id = cq.c_id )
11516
                WHERE cq.active != 1
11517
                  AND cq.c_id = $courseId
11518
                  AND sru.session_id = $sessionId
11519
                  AND cq.iid = $exerciseId ";
11520
        }
11521
        $sql .= " $sqlToUsers ORDER BY cq.c_id ";
11522
11523
        $result = Database::query($sql);
11524
        $data = Database::store_result($result);
11525
        Database::free_result($result);
11526
        if (true === $count) {
11527
            return (isset($data[0]) && isset($data[0]['total'])) ? $data[0]['total'] : 0;
11528
        }
11529
        $usersArray = [];
11530
11531
        $return = [];
11532
        if ($withSelectAll) {
11533
            $return[] = [
11534
                'user_id' => 'X',
11535
                'value' => 'X',
11536
                'user_name' => get_lang('AllStudents'),
11537
            ];
11538
        }
11539
11540
        foreach ($data as $index => $item) {
11541
            if (isset($item['user_id'])) {
11542
                $userId = (int) $item['user_id'];
11543
                if (!isset($usersArray[$userId])) {
11544
                    $usersArray[$userId] = api_get_user_info($userId);
11545
                }
11546
                $usersArray[$userId]['user_id'] = $userId;
11547
                $userData = $usersArray[$userId];
11548
                $data[$index]['user_name'] = $userData['complete_name'];
11549
                $return[] = $data[$index];
11550
            }
11551
        }
11552
11553
        return $return;
11554
    }
11555
11556
    /**
11557
     * Search the users who are in an exercise to send them an exercise reminder email and to the human resources
11558
     * managers, it is necessary the exercise id, the course and the session if it exists.
11559
     *
11560
     * @param int $exerciseId
11561
     * @param int $courseId
11562
     * @param int $sessionId
11563
     */
11564
    public static function notifyUsersOfTheExercise(
11565
        $exerciseId = 0,
11566
        $courseId = 0,
11567
        $sessionId = 0,
11568
        $toUsers = []
11569
    ) {
11570
        $users = self::getUsersInExercise(
11571
            $exerciseId,
11572
            $courseId,
11573
            $sessionId,
11574
            false,
11575
            $toUsers
11576
        );
11577
        $totalUsers = count($users);
11578
        $usersArray = [];
11579
        $courseTitle = '';
11580
        $quizTitle = '';
11581
11582
        for ($i = 0; $i < $totalUsers; $i++) {
11583
            $user = $users[$i];
11584
            $userId = (int) $user['user_id'];
11585
            if (0 != $userId) {
11586
                $quizTitle = $user['quiz_title'];
11587
                $courseTitle = $user['title'];
11588
                if (!isset($usersArray[$userId])) {
11589
                    $usersArray[$userId] = api_get_user_info($userId);
11590
                }
11591
            }
11592
        }
11593
11594
        $objExerciseTmp = new Exercise();
11595
        $objExerciseTmp->read($exerciseId);
11596
        $isAddedInLp = !empty($objExerciseTmp->lpList);
11597
        $end = $objExerciseTmp->end_time;
11598
        $start = $objExerciseTmp->start_time;
11599
        $minutes = $objExerciseTmp->expired_time;
11600
        $formatDate = DATE_TIME_FORMAT_LONG;
11601
        $tblCourseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
11602
        $tblSession = Database::get_main_table(TABLE_MAIN_SESSION);
11603
        $tblSessionUser = Database::get_main_table(TABLE_MAIN_SESSION_USER);
11604
        $tblSessionUserRelCourse = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
11605
        $teachersName = [];
11606
        $teachersPrint = [];
11607
        if (0 == $sessionId) {
11608
            $sql = "SELECT course_user.user_id AS user_id
11609
                FROM $tblCourseUser AS course_user
11610
                WHERE course_user.status = 1 AND course_user.c_id ='".$courseId."'";
11611
            $result = Database::query($sql);
11612
            $data = Database::store_result($result);
11613
            Database::free_result($result);
11614
            foreach ($data as $teacher) {
11615
                $teacherId = (int) $teacher['user_id'];
11616
                if (!isset($teachersName[$teacherId])) {
11617
                    $teachersName[$teacherId] = api_get_user_info($teacherId);
11618
                }
11619
                $teacherData = $teachersName[$teacherId];
11620
                $teachersPrint[] = $teacherData['complete_name'];
11621
            }
11622
        } else {
11623
            // general tutor
11624
            $sql = "SELECT sesion.id_coach AS user_id
11625
                FROM $tblSession AS sesion
11626
                WHERE sesion.id = $sessionId";
11627
            $result = Database::query($sql);
11628
            $data = Database::store_result($result);
11629
            Database::free_result($result);
11630
            foreach ($data as $teacher) {
11631
                $teacherId = (int) $teacher['user_id'];
11632
                if (!isset($teachersName[$teacherId])) {
11633
                    $teachersName[$teacherId] = api_get_user_info($teacherId);
11634
                }
11635
                $teacherData = $teachersName[$teacherId];
11636
                $teachersPrint[] = $teacherData['complete_name'];
11637
            }
11638
            // Teacher into sessions course
11639
            $sql = "SELECT session_rel_course_rel_user.user_id
11640
                FROM $tblSessionUserRelCourse AS session_rel_course_rel_user
11641
                WHERE session_rel_course_rel_user.session_id = $sessionId AND
11642
                      session_rel_course_rel_user.c_id = $courseId AND
11643
                      session_rel_course_rel_user.status = 2";
11644
            $result = Database::query($sql);
11645
            $data = Database::store_result($result);
11646
            Database::free_result($result);
11647
            foreach ($data as $teacher) {
11648
                $teacherId = (int) $teacher['user_id'];
11649
                if (!isset($teachersName[$teacherId])) {
11650
                    $teachersName[$teacherId] = api_get_user_info($teacherId);
11651
                }
11652
                $teacherData = $teachersName[$teacherId];
11653
                $teachersPrint[] = $teacherData['complete_name'];
11654
            }
11655
        }
11656
11657
        $teacherName = implode('<br>', $teachersPrint);
11658
11659
        if ($isAddedInLp) {
11660
            $lpInfo = current($objExerciseTmp->lpList);
11661
            $url = api_get_path(WEB_CODE_PATH)."lp/lp_controller.php?".api_get_cidreq().'&'
11662
                .http_build_query(['action' => 'view', 'lp_id' => $lpInfo['lp_id']]);
11663
        } else {
11664
            $url = api_get_path(WEB_CODE_PATH)."exercise/overview.php?".api_get_cidreq()."&exerciseId=$exerciseId";
11665
        }
11666
11667
        foreach ($usersArray as $userId => $userData) {
11668
            $studentName = $userData['complete_name'];
11669
            $title = sprintf(get_lang('QuizRemindSubject'), $teacherName);
11670
            $content = sprintf(
11671
                get_lang('QuizFirstRemindBody'),
11672
                $studentName,
11673
                $quizTitle,
11674
                $courseTitle,
11675
                $courseTitle,
11676
                $quizTitle
11677
            );
11678
            if (!empty($minutes)) {
11679
                $content .= sprintf(get_lang('QuizRemindDuration'), $minutes);
11680
            }
11681
            if (!empty($start)) {
11682
                // api_get_utc_datetime
11683
                $start = api_format_date(($start), $formatDate);
11684
11685
                $content .= sprintf(get_lang('QuizRemindStartDate'), $start);
11686
            }
11687
            if (!empty($end)) {
11688
                $end = api_format_date(($end), $formatDate);
11689
                $content .= sprintf(get_lang('QuizRemindEndDate'), $end);
11690
            }
11691
            $content .= sprintf(
11692
                get_lang('QuizLastRemindBody'),
11693
                $url,
11694
                $url,
11695
                $teacherName
11696
            );
11697
            $drhList = UserManager::getDrhListFromUser($userId);
11698
            if (!empty($drhList)) {
11699
                foreach ($drhList as $drhUser) {
11700
                    $drhUserData = api_get_user_info($drhUser['id']);
11701
                    $drhName = $drhUserData['complete_name'];
11702
                    $contentDHR = sprintf(
11703
                        get_lang('QuizDhrRemindBody'),
11704
                        $drhName,
11705
                        $studentName,
11706
                        $quizTitle,
11707
                        $courseTitle,
11708
                        $studentName,
11709
                        $courseTitle,
11710
                        $quizTitle
11711
                    );
11712
                    if (!empty($minutes)) {
11713
                        $contentDHR .= sprintf(get_lang('QuizRemindDuration'), $minutes);
11714
                    }
11715
                    if (!empty($start)) {
11716
                        // api_get_utc_datetime
11717
                        $start = api_format_date(($start), $formatDate);
11718
11719
                        $contentDHR .= sprintf(get_lang('QuizRemindStartDate'), $start);
11720
                    }
11721
                    if (!empty($end)) {
11722
                        $end = api_format_date(($end), $formatDate);
11723
                        $contentDHR .= sprintf(get_lang('QuizRemindEndDate'), $end);
11724
                    }
11725
                    MessageManager::send_message(
11726
                        $drhUser['id'],
11727
                        $title,
11728
                        $contentDHR
11729
                    );
11730
                }
11731
            }
11732
            MessageManager::send_message(
11733
                $userData['id'],
11734
                $title,
11735
                $content
11736
            );
11737
        }
11738
11739
        return $usersArray;
11740
    }
11741
11742
    /**
11743
     * Returns true if the exercise is locked by percentage. an exercise attempt must be passed.
11744
     */
11745
    public function isBlockedByPercentage(array $attempt = []): bool
11746
    {
11747
        if (empty($attempt)) {
11748
            return false;
11749
        }
11750
        $extraFieldValue = new ExtraFieldValue('exercise');
11751
        $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
11752
            $this->iid,
11753
            'blocking_percentage'
11754
        );
11755
11756
        if (empty($blockExercise['value'])) {
11757
            return false;
11758
        }
11759
11760
        $blockPercentage = (int) $blockExercise['value'];
11761
11762
        if (0 === $blockPercentage) {
11763
            return false;
11764
        }
11765
11766
        $resultPercentage = 0;
11767
11768
        if (isset($attempt['exe_result']) && isset($attempt['exe_weighting'])) {
11769
            $weight = (int) $attempt['exe_weighting'];
11770
            $weight = (0 == $weight) ? 1 : $weight;
11771
            $resultPercentage = float_format(
11772
                ($attempt['exe_result'] / $weight) * 100,
11773
                1
11774
            );
11775
        }
11776
        if ($resultPercentage <= $blockPercentage) {
11777
            return true;
11778
        }
11779
11780
        return false;
11781
    }
11782
11783
    /**
11784
     * Gets the question list ordered by the question_order setting (drag and drop).
11785
     *
11786
     * @param bool $adminView Optional.
11787
     *
11788
     * @return array
11789
     */
11790
    public function getQuestionOrderedList($adminView = false)
11791
    {
11792
        $TBL_QUIZ_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
11793
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
11794
11795
        // Getting question_order to verify that the question
11796
        // list is correct and all question_order's were set
11797
        $sql = "SELECT DISTINCT count(e.question_order) as count
11798
                FROM $TBL_QUIZ_QUESTION e
11799
                INNER JOIN $TBL_QUESTIONS q
11800
                ON e.question_id = q.iid
11801
                WHERE
11802
                  e.c_id = {$this->course_id} AND
11803
                  e.exercice_id	= ".$this->iid;
11804
11805
        $result = Database::query($sql);
11806
        $row = Database::fetch_array($result);
11807
        $count_question_orders = $row['count'];
11808
11809
        // Getting question list from the order (question list drag n drop interface).
11810
        $sql = "SELECT DISTINCT e.question_id, e.question_order
11811
                FROM $TBL_QUIZ_QUESTION e
11812
                INNER JOIN $TBL_QUESTIONS q
11813
                ON e.question_id = q.iid
11814
                WHERE
11815
                    e.c_id = {$this->course_id} AND
11816
                    e.exercice_id = '".$this->iid."'
11817
                ORDER BY question_order";
11818
        $result = Database::query($sql);
11819
11820
        // Fills the array with the question ID for this exercise
11821
        // the key of the array is the question position
11822
        $temp_question_list = [];
11823
        $counter = 1;
11824
        $questionList = [];
11825
        while ($new_object = Database::fetch_object($result)) {
11826
            if (!$adminView) {
11827
                // Correct order.
11828
                $questionList[$new_object->question_order] = $new_object->question_id;
11829
            } else {
11830
                $questionList[$counter] = $new_object->question_id;
11831
            }
11832
11833
            // Just in case we save the order in other array
11834
            $temp_question_list[$counter] = $new_object->question_id;
11835
            $counter++;
11836
        }
11837
11838
        if (!empty($temp_question_list)) {
11839
            /* If both array don't match it means that question_order was not correctly set
11840
               for all questions using the default mysql order */
11841
            if (count($temp_question_list) != $count_question_orders || count($temp_question_list) !== count($questionList)) {
11842
                $questionList = $temp_question_list;
11843
            }
11844
        }
11845
11846
        return $questionList;
11847
    }
11848
11849
    /**
11850
     * Returns a literal for the given numerical feedback type (usually
11851
     * coming from the DB or a constant). The literal is also the string
11852
     * used to get the translation, not the translation itself as it is
11853
     * more vulnerable to changes.
11854
     */
11855
    public static function getFeedbackTypeLiteral(int $feedbackType): string
11856
    {
11857
        $feedbackType = (int) $feedbackType;
11858
        $result = '';
11859
        static $arrayFeedbackTypes = [
11860
            EXERCISE_FEEDBACK_TYPE_END => 'ExerciseAtTheEndOfTheTest',
11861
            EXERCISE_FEEDBACK_TYPE_DIRECT => 'DirectFeedback',
11862
            EXERCISE_FEEDBACK_TYPE_EXAM => 'NoFeedback',
11863
            EXERCISE_FEEDBACK_TYPE_POPUP => 'ExerciseDirectPopUp',
11864
        ];
11865
11866
        if (array_key_exists($feedbackType, $arrayFeedbackTypes)) {
11867
            $result = $arrayFeedbackTypes[$feedbackType];
11868
        }
11869
11870
        return $result;
11871
    }
11872
11873
    /**
11874
     * Return the text to display, based on the score and the max score.
11875
     *
11876
     * @param int|float $score
11877
     * @param int|float $maxScore
11878
     */
11879
    public function getFinishText($score, $maxScore): string
11880
    {
11881
        if (true !== api_get_configuration_value('exercise_text_when_finished_failure')) {
11882
            return $this->getTextWhenFinished();
11883
        }
11884
11885
        $passPercentage = $this->selectPassPercentage();
11886
11887
        if (!empty($passPercentage)) {
11888
            $percentage = float_format(
11889
                ($score / (0 != $maxScore ? $maxScore : 1)) * 100,
11890
                1
11891
            );
11892
11893
            if ($percentage < $passPercentage) {
11894
                return $this->getTextWhenFinishedFailure();
11895
            }
11896
        }
11897
11898
        return $this->getTextWhenFinished();
11899
    }
11900
11901
    /**
11902
     * Get number of questions in exercise by user attempt.
11903
     *
11904
     * @return int
11905
     */
11906
    private function countQuestionsInExercise()
11907
    {
11908
        $lpId = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
11909
        $lpItemId = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
11910
        $lpItemViewId = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
11911
11912
        $trackInfo = $this->get_stat_track_exercise_info($lpId, $lpItemId, $lpItemViewId);
11913
11914
        if (!empty($trackInfo)) {
11915
            $questionIds = explode(',', $trackInfo['data_tracking']);
11916
11917
            return count($questionIds);
11918
        }
11919
11920
        return $this->getQuestionCount();
11921
    }
11922
11923
    /**
11924
     * Select N values from the questions per category array.
11925
     *
11926
     * @param array $categoriesAddedInExercise
11927
     * @param array $question_list
11928
     * @param array $questions_by_category
11929
     * @param bool  $flatResult
11930
     * @param bool  $randomizeQuestions
11931
     * @param array $questionsByCategoryMandatory
11932
     *
11933
     * @return array
11934
     */
11935
    private function pickQuestionsPerCategory(
11936
        $categoriesAddedInExercise,
11937
        $question_list,
11938
        &$questions_by_category,
11939
        $flatResult = true,
11940
        $randomizeQuestions = false,
11941
        $questionsByCategoryMandatory = []
11942
    ) {
11943
        $addAll = true;
11944
        $categoryCountArray = [];
11945
11946
        // Getting how many questions will be selected per category.
11947
        if (!empty($categoriesAddedInExercise)) {
11948
            $addAll = false;
11949
            // Parsing question according the category rel exercise settings
11950
            foreach ($categoriesAddedInExercise as $category_info) {
11951
                $category_id = $category_info['category_id'];
11952
                if (isset($questions_by_category[$category_id])) {
11953
                    // How many question will be picked from this category.
11954
                    $count = $category_info['count_questions'];
11955
                    // -1 means all questions
11956
                    $categoryCountArray[$category_id] = $count;
11957
                    if (-1 == $count) {
11958
                        $categoryCountArray[$category_id] = 999;
11959
                    }
11960
                }
11961
            }
11962
        }
11963
11964
        if (!empty($questions_by_category)) {
11965
            $temp_question_list = [];
11966
            foreach ($questions_by_category as $category_id => &$categoryQuestionList) {
11967
                if (isset($categoryCountArray) && !empty($categoryCountArray)) {
11968
                    $numberOfQuestions = 0;
11969
                    if (isset($categoryCountArray[$category_id])) {
11970
                        $numberOfQuestions = $categoryCountArray[$category_id];
11971
                    }
11972
                }
11973
11974
                if ($addAll) {
11975
                    $numberOfQuestions = 999;
11976
                }
11977
                if (!empty($numberOfQuestions)) {
11978
                    $mandatoryQuestions = [];
11979
                    if (isset($questionsByCategoryMandatory[$category_id])) {
11980
                        $mandatoryQuestions = $questionsByCategoryMandatory[$category_id];
11981
                    }
11982
11983
                    $elements = TestCategory::getNElementsFromArray(
11984
                        $categoryQuestionList,
11985
                        $numberOfQuestions,
11986
                        $randomizeQuestions,
11987
                        $mandatoryQuestions
11988
                    );
11989
                    if (!empty($elements)) {
11990
                        $temp_question_list[$category_id] = $elements;
11991
                        $categoryQuestionList = $elements;
11992
                    }
11993
                }
11994
            }
11995
11996
            if (!empty($temp_question_list)) {
11997
                if ($flatResult) {
11998
                    $temp_question_list = array_flatten($temp_question_list);
11999
                }
12000
                $question_list = $temp_question_list;
12001
            }
12002
        }
12003
12004
        return $question_list;
12005
    }
12006
12007
    /**
12008
     * Changes the exercise id.
12009
     *
12010
     * @param int $id - exercise id
12011
     */
12012
    private function updateId($id)
12013
    {
12014
        $this->iid = $id;
12015
    }
12016
12017
    /**
12018
     * Sends a notification when a user ends an examn.
12019
     *
12020
     * @param array  $question_list_answers
12021
     * @param string $origin
12022
     * @param array  $user_info
12023
     * @param string $url_email
12024
     * @param array  $teachers
12025
     */
12026
    private function sendNotificationForOpenQuestions(
12027
        $question_list_answers,
12028
        $origin,
12029
        $user_info,
12030
        $url_email,
12031
        $teachers
12032
    ) {
12033
        // Email configuration settings
12034
        $courseCode = api_get_course_id();
12035
        $courseInfo = api_get_course_info($courseCode);
12036
        $sessionId = api_get_session_id();
12037
        $sessionData = '';
12038
        if (!empty($sessionId)) {
12039
            $sessionInfo = api_get_session_info($sessionId);
12040
            if (!empty($sessionInfo)) {
12041
                $sessionData = '<tr>'
12042
                    .'<td><em>'.get_lang('SessionName').'</em></td>'
12043
                    .'<td>&nbsp;<b>'.$sessionInfo['name'].'</b></td>'
12044
                    .'</tr>';
12045
            }
12046
        }
12047
12048
        $msg = get_lang('OpenQuestionsAttempted').'<br /><br />'
12049
            .get_lang('AttemptDetails').' : <br /><br />'
12050
            .'<table>'
12051
            .'<tr>'
12052
            .'<td><em>'.get_lang('CourseName').'</em></td>'
12053
            .'<td>&nbsp;<b>#course#</b></td>'
12054
            .'</tr>'
12055
            .$sessionData
12056
            .'<tr>'
12057
            .'<td>'.get_lang('TestAttempted').'</td>'
12058
            .'<td>&nbsp;#exercise#</td>'
12059
            .'</tr>'
12060
            .'<tr>'
12061
            .'<td>'.get_lang('StudentName').'</td>'
12062
            .'<td>&nbsp;#firstName# #lastName#</td>'
12063
            .'</tr>'
12064
            .'<tr>'
12065
            .'<td>'.get_lang('StudentEmail').'</td>'
12066
            .'<td>&nbsp;#mail#</td>'
12067
            .'</tr>'
12068
            .'</table>';
12069
12070
        $open_question_list = null;
12071
        foreach ($question_list_answers as $item) {
12072
            $question = $item['question'];
12073
            $answer = $item['answer'];
12074
            $answer_type = $item['answer_type'];
12075
            if (!empty($question) && !empty($answer) && $answer_type == FREE_ANSWER) {
12076
                $open_question_list .=
12077
                    '<tr>'
12078
                    .'<td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>'
12079
                    .'<td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>'
12080
                    .'</tr>'
12081
                    .'<tr>'
12082
                    .'<td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>'
12083
                    .'<td valign="top" bgcolor="#F3F3F3">'.$answer.'</td>'
12084
                    .'</tr>';
12085
            }
12086
        }
12087
12088
        if (!empty($open_question_list)) {
12089
            $msg .= '<p><br />'.get_lang('OpenQuestionsAttemptedAre').' :</p>'.
12090
                '<table width="730" height="136" border="0" cellpadding="3" cellspacing="3">';
12091
            $msg .= $open_question_list;
12092
            $msg .= '</table><br />';
12093
12094
            $msg = str_replace('#exercise#', $this->exercise, $msg);
12095
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg);
12096
            $msg = str_replace('#lastName#', $user_info['lastname'], $msg);
12097
            $msg = str_replace('#mail#', $user_info['email'], $msg);
12098
            $msg = str_replace(
12099
                '#course#',
12100
                Display::url($courseInfo['title'], $courseInfo['course_public_url'].'?id_session='.$sessionId),
12101
                $msg
12102
            );
12103
12104
            if ($origin != 'learnpath') {
12105
                $msg .= '<br /><a href="#url#">'.get_lang('ClickToCommentAndGiveFeedback').'</a>';
12106
            }
12107
            $msg = str_replace('#url#', $url_email, $msg);
12108
            $subject = get_lang('OpenQuestionsAttempted');
12109
12110
            if (!empty($teachers)) {
12111
                foreach ($teachers as $user_id => $teacher_data) {
12112
                    MessageManager::send_message_simple(
12113
                        $user_id,
12114
                        $subject,
12115
                        $msg
12116
                    );
12117
                }
12118
            }
12119
        }
12120
    }
12121
12122
    /**
12123
     * Send notification for oral questions.
12124
     *
12125
     * @param array  $question_list_answers
12126
     * @param string $origin
12127
     * @param int    $exe_id
12128
     * @param array  $user_info
12129
     * @param string $url_email
12130
     * @param array  $teachers
12131
     */
12132
    private function sendNotificationForOralQuestions(
12133
        $question_list_answers,
12134
        $origin,
12135
        $exe_id,
12136
        $user_info,
12137
        $url_email,
12138
        $teachers
12139
    ) {
12140
        // Email configuration settings
12141
        $courseCode = api_get_course_id();
12142
        $courseInfo = api_get_course_info($courseCode);
12143
        $oral_question_list = null;
12144
        foreach ($question_list_answers as $item) {
12145
            $question = $item['question'];
12146
            $file = $item['generated_oral_file'];
12147
            $answer = $item['answer'];
12148
            if (0 == $answer) {
12149
                $answer = '';
12150
            }
12151
            $answer_type = $item['answer_type'];
12152
            if (!empty($question) && (!empty($answer) || !empty($file)) && $answer_type == ORAL_EXPRESSION) {
12153
                if (!empty($file)) {
12154
                    $file = Display::url($file, $file);
12155
                }
12156
                $oral_question_list .= '<br />
12157
                    <table width="730" height="136" border="0" cellpadding="3" cellspacing="3">
12158
                    <tr>
12159
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
12160
                        <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
12161
                    </tr>
12162
                    <tr>
12163
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
12164
                        <td valign="top" bgcolor="#F3F3F3">'.$answer.$file.'</td>
12165
                    </tr></table>';
12166
            }
12167
        }
12168
12169
        if (!empty($oral_question_list)) {
12170
            $msg = get_lang('OralQuestionsAttempted').'<br /><br />
12171
                    '.get_lang('AttemptDetails').' : <br /><br />
12172
                    <table>
12173
                        <tr>
12174
                            <td><em>'.get_lang('CourseName').'</em></td>
12175
                            <td>&nbsp;<b>#course#</b></td>
12176
                        </tr>
12177
                        <tr>
12178
                            <td>'.get_lang('TestAttempted').'</td>
12179
                            <td>&nbsp;#exercise#</td>
12180
                        </tr>
12181
                        <tr>
12182
                            <td>'.get_lang('StudentName').'</td>
12183
                            <td>&nbsp;#firstName# #lastName#</td>
12184
                        </tr>
12185
                        <tr>
12186
                            <td>'.get_lang('StudentEmail').'</td>
12187
                            <td>&nbsp;#mail#</td>
12188
                        </tr>
12189
                    </table>';
12190
            $msg .= '<br />'.sprintf(get_lang('OralQuestionsAttemptedAreX'), $oral_question_list).'<br />';
12191
            $msg1 = str_replace("#exercise#", $this->exercise, $msg);
12192
            $msg = str_replace("#firstName#", $user_info['firstname'], $msg1);
12193
            $msg1 = str_replace("#lastName#", $user_info['lastname'], $msg);
12194
            $msg = str_replace("#mail#", $user_info['email'], $msg1);
12195
            $msg = str_replace("#course#", $courseInfo['name'], $msg1);
12196
12197
            if (!in_array($origin, ['learnpath', 'embeddable', 'iframe'])) {
12198
                $msg .= '<br /><a href="#url#">'.get_lang('ClickToCommentAndGiveFeedback').'</a>';
12199
            }
12200
            $msg1 = str_replace("#url#", $url_email, $msg);
12201
            $mail_content = $msg1;
12202
            $subject = get_lang('OralQuestionsAttempted');
12203
12204
            if (!empty($teachers)) {
12205
                foreach ($teachers as $user_id => $teacher_data) {
12206
                    MessageManager::send_message_simple(
12207
                        $user_id,
12208
                        $subject,
12209
                        $mail_content
12210
                    );
12211
                }
12212
            }
12213
        }
12214
    }
12215
12216
    /**
12217
     * Returns an array with the media list.
12218
     *
12219
     * @param array question list
12220
     *
12221
     * @example there's 1 question with iid 5 that belongs to the media question with iid = 100
12222
     * <code>
12223
     * array (size=2)
12224
     *  999 =>
12225
     *    array (size=3)
12226
     *      0 => int 7
12227
     *      1 => int 6
12228
     *      2 => int 3254
12229
     *  100 =>
12230
     *   array (size=1)
12231
     *      0 => int 5
12232
     *  </code>
12233
     */
12234
    private function setMediaList($questionList)
12235
    {
12236
        $mediaList = [];
12237
        /*
12238
         * Media feature is not activated in 1.11.x
12239
        if (!empty($questionList)) {
12240
            foreach ($questionList as $questionId) {
12241
                $objQuestionTmp = Question::read($questionId, $this->course_id);
12242
                // If a media question exists
12243
                if (isset($objQuestionTmp->parent_id) && $objQuestionTmp->parent_id != 0) {
12244
                    $mediaList[$objQuestionTmp->parent_id][] = $objQuestionTmp->iid;
12245
                } else {
12246
                    // Always the last item
12247
                    $mediaList[999][] = $objQuestionTmp->iid;
12248
                }
12249
            }
12250
        }*/
12251
12252
        $this->mediaList = $mediaList;
12253
    }
12254
12255
    /**
12256
     * Returns the part of the form for the disabled results option.
12257
     *
12258
     * @return HTML_QuickForm_group
12259
     */
12260
    private function setResultDisabledGroup(FormValidator $form)
12261
    {
12262
        $resultDisabledGroup = [];
12263
12264
        $resultDisabledGroup[] = $form->createElement(
12265
            'radio',
12266
            'results_disabled',
12267
            null,
12268
            get_lang('ShowScoreAndRightAnswer'),
12269
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
12270
            ['id' => 'result_disabled_0']
12271
        );
12272
12273
        $warning = sprintf(
12274
            get_lang('TheSettingXWillChangeToX'),
12275
            get_lang('FeedbackType'),
12276
            get_lang('NoFeedback')
12277
        );
12278
12279
        $resultDisabledGroup[] = $form->createElement(
12280
            'radio',
12281
            'results_disabled',
12282
            null,
12283
            get_lang('DoNotShowScoreNorRightAnswer'),
12284
            RESULT_DISABLE_NO_SCORE_AND_EXPECTED_ANSWERS,
12285
            [
12286
                'id' => 'result_disabled_1',
12287
                //'onclick' => 'check_results_disabled()'
12288
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
12289
            ]
12290
        );
12291
12292
        $resultDisabledGroup[] = $form->createElement(
12293
            'radio',
12294
            'results_disabled',
12295
            null,
12296
            get_lang('OnlyShowScore'),
12297
            RESULT_DISABLE_SHOW_SCORE_ONLY,
12298
            [
12299
                'id' => 'result_disabled_2',
12300
                //'onclick' => 'check_results_disabled()'
12301
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
12302
            ]
12303
        );
12304
12305
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
12306
            return $form->addGroup(
12307
                $resultDisabledGroup,
12308
                null,
12309
                get_lang('ShowResultsToStudents')
12310
            );
12311
        }
12312
12313
        $resultDisabledGroup[] = $form->createElement(
12314
            'radio',
12315
            'results_disabled',
12316
            null,
12317
            get_lang('ShowScoreEveryAttemptShowAnswersLastAttempt'),
12318
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
12319
            ['id' => 'result_disabled_4']
12320
        );
12321
12322
        $resultDisabledGroup[] = $form->createElement(
12323
            'radio',
12324
            'results_disabled',
12325
            null,
12326
            get_lang('DontShowScoreOnlyWhenUserFinishesAllAttemptsButShowFeedbackEachAttempt'),
12327
            RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
12328
            [
12329
                'id' => 'result_disabled_5',
12330
                //'onclick' => 'check_results_disabled()'
12331
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
12332
            ]
12333
        );
12334
12335
        $resultDisabledGroup[] = $form->createElement(
12336
            'radio',
12337
            'results_disabled',
12338
            null,
12339
            get_lang('ExerciseRankingMode'),
12340
            RESULT_DISABLE_RANKING,
12341
            ['id' => 'result_disabled_6']
12342
        );
12343
12344
        $resultDisabledGroup[] = $form->createElement(
12345
            'radio',
12346
            'results_disabled',
12347
            null,
12348
            get_lang('ExerciseShowOnlyGlobalScoreAndCorrectAnswers'),
12349
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
12350
            ['id' => 'result_disabled_7']
12351
        );
12352
12353
        $resultDisabledGroup[] = $form->createElement(
12354
            'radio',
12355
            'results_disabled',
12356
            null,
12357
            get_lang('ExerciseAutoEvaluationAndRankingMode'),
12358
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
12359
            ['id' => 'result_disabled_8']
12360
        );
12361
12362
        $resultDisabledGroup[] = $form->createElement(
12363
            'radio',
12364
            'results_disabled',
12365
            null,
12366
            get_lang('ExerciseCategoriesRadarMode'),
12367
            RESULT_DISABLE_RADAR,
12368
            ['id' => 'result_disabled_9']
12369
        );
12370
12371
        $resultDisabledGroup[] = $form->createElement(
12372
            'radio',
12373
            'results_disabled',
12374
            null,
12375
            get_lang('ShowScoreEveryAttemptShowAnswersLastAttemptNoFeedback'),
12376
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
12377
            ['id' => 'result_disabled_10']
12378
        );
12379
12380
        return $form->addGroup(
12381
            $resultDisabledGroup,
12382
            null,
12383
            get_lang('ShowResultsToStudents')
12384
        );
12385
    }
12386
}
12387