getHideAttemptsTableOnStartPageConfiguration()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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