Passed
Push — 1.11.x ( 31fff3...86e7ae )
by Yannick
15:37 queued 10s
created

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

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