Passed
Push — 1.11.x ( 44adc0...3e450b )
by Yannick
29:26 queued 14s
created

Exercise::hasQuestion()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

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

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

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

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4563
                                $studentAnswer = trim($listStudentAnswerTemp[$i]);
4564
                                $studentAnswerToShow = $studentAnswer;
4565
4566
                                if (empty($studentAnswer)) {
4567
                                    break;
4568
                                }
4569
4570
                                if ($debug) {
4571
                                    error_log("Student answer: $i");
4572
                                    error_log($studentAnswer);
4573
                                }
4574
4575
                                if (!$from_database) {
4576
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4577
                                    if ($debug) {
4578
                                        error_log("Student answer cleaned:");
4579
                                        error_log($studentAnswer);
4580
                                    }
4581
                                }
4582
4583
                                $found = false;
4584
                                for ($j = 0; $j < count($listTeacherAnswerTemp); $j++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

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

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

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4585
                                    $correctAnswer = isset($listTeacherAnswerTemp[$j]) ? $listTeacherAnswerTemp[$j] : '';
4586
                                    if (is_array($listTeacherAnswerTemp)) {
4587
                                        $correctAnswer = implode('||', $listTeacherAnswerTemp);
4588
                                    }
4589
4590
                                    if (empty($correctAnswer)) {
4591
                                        break;
4592
                                    }
4593
4594
                                    if (FillBlanks::isStudentAnswerGood(
4595
                                        $studentAnswer,
4596
                                        $correctAnswer,
4597
                                        $from_database,
4598
                                        true
4599
                                    )) {
4600
                                        $questionScore += $answerWeighting[$i];
4601
                                        $totalScore += $answerWeighting[$i];
4602
                                        if (is_array($listTeacherAnswerTemp)) {
4603
                                            $searchAnswer = array_search(api_htmlentities($studentAnswer), $listTeacherAnswerTemp);
4604
                                            if (false !== $searchAnswer) {
4605
                                                // Remove from array
4606
                                                unset($listTeacherAnswerTemp[$searchAnswer]);
4607
                                            }
4608
                                        } else {
4609
                                            $listTeacherAnswerTemp[$j] = null;
4610
                                        }
4611
                                        $found = true;
4612
                                    }
4613
4614
                                    if (FillBlanks::FILL_THE_BLANK_MENU != $listCorrectAnswers['words_types'][$j]) {
4615
                                        break;
4616
                                    }
4617
4618
                                    $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4619
                                    foreach ($listMenu as $item) {
4620
                                        if (sha1($item) === $studentAnswer) {
4621
                                            $studentAnswerToShow = $item;
4622
                                        }
4623
                                    }
4624
                                }
4625
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4626
                                $listCorrectAnswers['student_score'][$i] = $found ? 1 : 0;
4627
                            }
4628
                        }
4629
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
4630
                    }
4631
                    break;
4632
                case CALCULATED_ANSWER:
4633
                    $calculatedAnswerList = Session::read('calculatedAnswerId');
4634
                    $calculatedAnswerId = null;
4635
                    if (!empty($calculatedAnswerList) && isset($calculatedAnswerList[$questionId])) {
4636
                        $calculatedAnswerId = $calculatedAnswerList[$questionId];
4637
                        $answer = $objAnswerTmp->selectAnswer($calculatedAnswerId);
4638
                        $preArray = explode('@@', $answer);
4639
                        $last = count($preArray) - 1;
4640
                        $answer = '';
4641
                        for ($k = 0; $k < $last; $k++) {
4642
                            $answer .= $preArray[$k];
4643
                        }
4644
                        $answerWeighting = [$answerWeighting];
4645
                        // we save the answer because it will be modified
4646
                        $temp = $answer;
4647
                        $answer = '';
4648
                        $j = 0;
4649
                        // initialise answer tags
4650
                        $userTags = $correctTags = $realText = [];
4651
                        // the loop will stop at the end of the text
4652
                        while (1) {
4653
                            // quits the loop if there are no more blanks (detect '[')
4654
                            if ($temp == false || ($pos = api_strpos($temp, '[')) === false) {
4655
                                // adds the end of the text
4656
                                $answer = $temp;
4657
                                $realText[] = $answer;
4658
                                break; //no more "blanks", quit the loop
4659
                            }
4660
                            // adds the piece of text that is before the blank
4661
                            // and ends with '[' into a general storage array
4662
                            $realText[] = api_substr($temp, 0, $pos + 1);
4663
                            $answer .= api_substr($temp, 0, $pos + 1);
4664
                            // take the string remaining (after the last "[" we found)
4665
                            $temp = api_substr($temp, $pos + 1);
4666
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4667
                            if (false === ($pos = api_strpos($temp, ']'))) {
4668
                                // adds the end of the text
4669
                                $answer .= $temp;
4670
                                break;
4671
                            }
4672
4673
                            if ($from_database) {
4674
                                $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4675
                                        WHERE
4676
                                            exe_id = $exeId AND
4677
                                            question_id = $questionId ";
4678
                                $result = Database::query($sql);
4679
                                $str = Database::result($result, 0, 'answer');
4680
4681
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4682
                                $str = str_replace('\r\n', '', $str);
4683
                                $choice = $arr[1];
4684
                                if (isset($choice[$j])) {
4685
                                    $tmp = api_strrpos($choice[$j], ' / ');
4686
                                    if ($tmp) {
4687
                                        $choice[$j] = api_substr($choice[$j], 0, $tmp);
4688
                                    } else {
4689
                                        $tmp = ltrim($tmp, '[');
4690
                                        $tmp = rtrim($tmp, ']');
4691
                                    }
4692
                                    $choice[$j] = trim($choice[$j]);
4693
                                    // Needed to let characters ' and " to work as part of an answer
4694
                                    $choice[$j] = stripslashes($choice[$j]);
4695
                                } else {
4696
                                    $choice[$j] = null;
4697
                                }
4698
                            } else {
4699
                                // This value is the user input not escaped while correct answer is escaped by ckeditor
4700
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4701
                            }
4702
                            $userTags[] = $choice[$j];
4703
                            // put the contents of the [] answer tag into correct_tags[]
4704
                            $correctTags[] = api_substr($temp, 0, $pos);
4705
                            $j++;
4706
                            $temp = api_substr($temp, $pos + 1);
4707
                        }
4708
                        $answer = '';
4709
                        $realCorrectTags = $correctTags;
4710
                        $calculatedStatus = Display::label(get_lang('Incorrect'), 'danger');
4711
                        $expectedAnswer = '';
4712
                        $calculatedChoice = '';
4713
4714
                        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...
4715
                            if (0 == $i) {
4716
                                $answer .= $realText[0];
4717
                            }
4718
                            // Needed to parse ' and " characters
4719
                            $userTags[$i] = stripslashes($userTags[$i]);
4720
                            if ($correctTags[$i] == $userTags[$i]) {
4721
                                // gives the related weighting to the student
4722
                                $questionScore += $answerWeighting[$i];
4723
                                // increments total score
4724
                                $totalScore += $answerWeighting[$i];
4725
                                // adds the word in green at the end of the string
4726
                                $answer .= $correctTags[$i];
4727
                                $calculatedChoice = $correctTags[$i];
4728
                            } elseif (!empty($userTags[$i])) {
4729
                                // else if the word entered by the student IS NOT the same as
4730
                                // the one defined by the professor
4731
                                // adds the word in red at the end of the string, and strikes it
4732
                                $answer .= '<font color="red"><s>'.$userTags[$i].'</s></font>';
4733
                                $calculatedChoice = $userTags[$i];
4734
                            } else {
4735
                                // adds a tabulation if no word has been typed by the student
4736
                                $answer .= ''; // remove &nbsp; that causes issue
4737
                            }
4738
                            // adds the correct word, followed by ] to close the blank
4739
                            if (EXERCISE_FEEDBACK_TYPE_EXAM != $this->results_disabled) {
4740
                                $answer .= ' / <font color="green"><b>'.$realCorrectTags[$i].'</b></font>';
4741
                                $calculatedStatus = Display::label(get_lang('Correct'), 'success');
4742
                                $expectedAnswer = $realCorrectTags[$i];
4743
                            }
4744
                            $answer .= ']';
4745
                            if (isset($realText[$i + 1])) {
4746
                                $answer .= $realText[$i + 1];
4747
                            }
4748
                        }
4749
                    } else {
4750
                        if ($from_database) {
4751
                            $sql = "SELECT *
4752
                                    FROM $TBL_TRACK_ATTEMPT
4753
                                    WHERE
4754
                                        exe_id = $exeId AND
4755
                                        question_id = $questionId ";
4756
                            $result = Database::query($sql);
4757
                            $resultData = Database::fetch_array($result, 'ASSOC');
4758
                            $answer = $resultData['answer'];
4759
                            $questionScore = $resultData['marks'];
4760
                        }
4761
                    }
4762
                    break;
4763
                case UPLOAD_ANSWER:
4764
                case FREE_ANSWER:
4765
                    if ($from_database) {
4766
                        $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
4767
                                 WHERE
4768
                                    exe_id = $exeId AND
4769
                                    question_id= ".$questionId;
4770
                        $result = Database::query($sql);
4771
                        $data = Database::fetch_array($result);
4772
                        $choice = '';
4773
                        $questionScore = 0;
4774
                        if ($data) {
4775
                            $choice = $data['answer'];
4776
                            $questionScore = $data['marks'];
4777
                        }
4778
                        $choice = str_replace('\r\n', '', $choice);
4779
                        $choice = stripslashes($choice);
4780
                        if (-1 == $questionScore) {
4781
                            $totalScore += 0;
4782
                        } else {
4783
                            $totalScore += $questionScore;
4784
                        }
4785
                        if ('' == $questionScore) {
4786
                            $questionScore = 0;
4787
                        }
4788
                        $arrques = $questionName;
4789
                        $arrans = $choice;
4790
                    } else {
4791
                        $studentChoice = $choice;
4792
                        if ($studentChoice) {
4793
                            //Fixing negative puntation see #2193
4794
                            $questionScore = 0;
4795
                            $totalScore += 0;
4796
                        }
4797
                    }
4798
                    break;
4799
                case ORAL_EXPRESSION:
4800
                    if ($from_database) {
4801
                        $query = "SELECT answer, marks
4802
                                  FROM $TBL_TRACK_ATTEMPT
4803
                                  WHERE
4804
                                        exe_id = $exeId AND
4805
                                        question_id = $questionId
4806
                                 ";
4807
                        $resq = Database::query($query);
4808
                        $row = Database::fetch_assoc($resq);
4809
4810
                        $choice = [
4811
                            'answer' => '',
4812
                            'marks' => 0,
4813
                        ];
4814
                        $questionScore = 0;
4815
4816
                        if (is_array($row)) {
4817
                            $choice = $row['answer'];
4818
                            $choice = str_replace('\r\n', '', $choice);
4819
                            $choice = stripslashes($choice);
4820
                            $questionScore = $row['marks'];
4821
                        }
4822
4823
                        if ($questionScore == -1) {
4824
                            $totalScore += 0;
4825
                        } else {
4826
                            $totalScore += $questionScore;
4827
                        }
4828
                        $arrques = $questionName;
4829
                        $arrans = $choice;
4830
                    } else {
4831
                        $studentChoice = $choice;
4832
                        if ($studentChoice) {
4833
                            //Fixing negative puntation see #2193
4834
                            $questionScore = 0;
4835
                            $totalScore += 0;
4836
                        }
4837
                    }
4838
                    break;
4839
                case DRAGGABLE:
4840
                case MATCHING_DRAGGABLE:
4841
                case MATCHING_DRAGGABLE_COMBINATION:
4842
                case MATCHING_COMBINATION:
4843
                case MATCHING:
4844
                    if ($from_database) {
4845
                        $sql = "SELECT iid, answer, id_auto
4846
                                FROM $table_ans
4847
                                WHERE
4848
                                    question_id = $questionId AND
4849
                                    correct = 0
4850
                                ";
4851
                        $result = Database::query($sql);
4852
                        // Getting the real answer
4853
                        $real_list = [];
4854
                        while ($realAnswer = Database::fetch_array($result)) {
4855
                            $real_list[$realAnswer['iid']] = $realAnswer['answer'];
4856
                        }
4857
4858
                        $orderBy = ' ORDER BY iid ';
4859
                        if (DRAGGABLE == $answerType) {
4860
                            $orderBy = ' ORDER BY correct ';
4861
                        }
4862
4863
                        $sql = "SELECT iid, answer, correct, id_auto, ponderation
4864
                                FROM $table_ans
4865
                                WHERE
4866
                                    question_id = $questionId AND
4867
                                    correct <> 0
4868
                                $orderBy";
4869
                        $result = Database::query($sql);
4870
                        $options = [];
4871
                        $correctAnswers = [];
4872
                        while ($row = Database::fetch_array($result, 'ASSOC')) {
4873
                            $options[] = $row;
4874
                            $correctAnswers[$row['correct']] = $row['answer'];
4875
                        }
4876
4877
                        $questionScore = 0;
4878
                        $counterAnswer = 1;
4879
                        foreach ($options as $a_answers) {
4880
                            $i_answer_id = $a_answers['iid']; //3
4881
                            $s_answer_label = $a_answers['answer']; // your daddy - your mother
4882
                            $i_answer_correct_answer = $a_answers['correct']; //1 - 2
4883
4884
                            $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4885
                                    WHERE
4886
                                        exe_id = '$exeId' AND
4887
                                        question_id = '$questionId' AND
4888
                                        position = '$i_answer_id'";
4889
                            $result = Database::query($sql);
4890
                            $s_user_answer = 0;
4891
                            if (Database::num_rows($result) > 0) {
4892
                                //  rich - good looking
4893
                                $s_user_answer = Database::result($result, 0, 0);
4894
                            }
4895
                            $i_answerWeighting = $a_answers['ponderation'];
4896
                            $user_answer = '';
4897
                            $status = Display::label(get_lang('Incorrect'), 'danger');
4898
4899
                            if (!empty($s_user_answer)) {
4900
                                if (DRAGGABLE == $answerType) {
4901
                                    $sql = "SELECT answer FROM $table_ans WHERE iid = $s_user_answer";
4902
                                    $rsDragAnswer = Database::query($sql);
4903
                                    $dragAns = Database::result($rsDragAnswer, 0, 0);
4904
                                    if ($s_user_answer == $i_answer_correct_answer) {
4905
                                        $questionScore += $i_answerWeighting;
4906
                                        $totalScore += $i_answerWeighting;
4907
                                        $user_answer = Display::label(get_lang('Correct'), 'success');
4908
                                        if ($this->showExpectedChoice() && !empty($i_answer_id)) {
4909
                                            $user_answer = $answerMatching[$i_answer_id];
4910
                                        } else {
4911
                                            $user_answer = $dragAns;
4912
                                        }
4913
                                        $status = Display::label(get_lang('Correct'), 'success');
4914
                                    } else {
4915
                                        $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4916
                                        if ($this->showExpectedChoice() && !empty($s_user_answer)) {
4917
                                            /*$data = $options[$real_list[$s_user_answer] - 1];
4918
                                            $user_answer = $data['answer'];*/
4919
                                            $user_answer = $correctAnswers[$s_user_answer] ?? '';
4920
                                        } else {
4921
                                            $user_answer = $dragAns;
4922
                                        }
4923
                                    }
4924
                                } else {
4925
                                    if ($s_user_answer == $i_answer_correct_answer) {
4926
                                        $questionScore += $i_answerWeighting;
4927
                                        $totalScore += $i_answerWeighting;
4928
                                        $status = Display::label(get_lang('Correct'), 'success');
4929
                                        // Try with id
4930
                                        if (isset($real_list[$i_answer_id])) {
4931
                                            $user_answer = Display::span(
4932
                                                $real_list[$i_answer_id],
4933
                                                ['style' => 'color: #008000; font-weight: bold;']
4934
                                            );
4935
                                        }
4936
4937
                                        // Try with $i_answer_id_auto
4938
                                        if (empty($user_answer)) {
4939
                                            if (isset($real_list[$i_answer_id])) {
4940
                                                $user_answer = Display::span(
4941
                                                    $real_list[$i_answer_id],
4942
                                                    ['style' => 'color: #008000; font-weight: bold;']
4943
                                                );
4944
                                            }
4945
                                        }
4946
4947
                                        if (isset($real_list[$i_answer_correct_answer])) {
4948
                                            $matchingCorrectAnswers[$questionId]['from_database']['correct'][$i_answer_correct_answer] = $real_list[$i_answer_correct_answer];
4949
                                            $user_answer = Display::span(
4950
                                                $real_list[$i_answer_correct_answer],
4951
                                                ['style' => 'color: #008000; font-weight: bold;']
4952
                                            );
4953
                                        }
4954
                                    } else {
4955
                                        $user_answer = Display::span(
4956
                                            $real_list[$s_user_answer],
4957
                                            ['style' => 'color: #FF0000; text-decoration: line-through;']
4958
                                        );
4959
                                        if ($this->showExpectedChoice()) {
4960
                                            if (isset($real_list[$s_user_answer])) {
4961
                                                $user_answer = Display::span($real_list[$s_user_answer]);
4962
                                            }
4963
                                        }
4964
                                    }
4965
                                }
4966
                            } elseif (DRAGGABLE == $answerType) {
4967
                                $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4968
                                if ($this->showExpectedChoice()) {
4969
                                    $user_answer = '';
4970
                                }
4971
                            } else {
4972
                                $user_answer = Display::span(
4973
                                    get_lang('Incorrect').' &nbsp;',
4974
                                    ['style' => 'color: #FF0000; text-decoration: line-through;']
4975
                                );
4976
                                if ($this->showExpectedChoice()) {
4977
                                    $user_answer = '';
4978
                                }
4979
                            }
4980
4981
                            if ($show_result) {
4982
                                if (false === $this->showExpectedChoice() &&
4983
                                    false === $showTotalScoreAndUserChoicesInLastAttempt
4984
                                ) {
4985
                                    $user_answer = '';
4986
                                }
4987
                                switch ($answerType) {
4988
                                    case MATCHING:
4989
                                    case MATCHING_COMBINATION:
4990
                                    case MATCHING_DRAGGABLE_COMBINATION:
4991
                                    case MATCHING_DRAGGABLE:
4992
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4993
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4994
                                                break;
4995
                                            }
4996
                                        }
4997
4998
                                        echo '<tr>';
4999
                                        if (!in_array(
5000
                                            $this->results_disabled,
5001
                                            [
5002
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
5003
                                            ]
5004
                                        )
5005
                                        ) {
5006
                                            echo '<td>'.$s_answer_label.'</td>';
5007
                                            echo '<td>'.$user_answer.'</td>';
5008
                                        } else {
5009
                                            echo '<td>'.$s_answer_label.'</td>';
5010
                                            $status = Display::label(get_lang('Correct'), 'success');
5011
                                        }
5012
5013
                                        if ($this->showExpectedChoice()) {
5014
                                            if ($this->showExpectedChoiceColumn()) {
5015
                                                echo '<td>';
5016
                                                if (in_array($answerType, [MATCHING, MATCHING_COMBINATION, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION])) {
5017
                                                    if (isset($real_list[$i_answer_correct_answer]) &&
5018
                                                        $showTotalScoreAndUserChoicesInLastAttempt == true
5019
                                                    ) {
5020
                                                        echo Display::span(
5021
                                                            $real_list[$i_answer_correct_answer]
5022
                                                        );
5023
                                                    }
5024
                                                }
5025
                                                echo '</td>';
5026
                                            }
5027
                                            echo '<td>'.$status.'</td>';
5028
                                        } else {
5029
                                            if (in_array($answerType, [MATCHING, MATCHING_COMBINATION, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION])) {
5030
                                                if (isset($real_list[$i_answer_correct_answer]) &&
5031
                                                    $showTotalScoreAndUserChoicesInLastAttempt === true
5032
                                                ) {
5033
                                                    if ($this->showExpectedChoiceColumn()) {
5034
                                                        echo '<td>';
5035
                                                        echo Display::span(
5036
                                                            $real_list[$i_answer_correct_answer],
5037
                                                            ['style' => 'color: #008000; font-weight: bold;']
5038
                                                        );
5039
                                                        echo '</td>';
5040
                                                    }
5041
                                                }
5042
                                            }
5043
                                        }
5044
                                        echo '</tr>';
5045
                                        break;
5046
                                    case DRAGGABLE:
5047
                                        if (false == $showTotalScoreAndUserChoicesInLastAttempt) {
5048
                                            $s_answer_label = '';
5049
                                        }
5050
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
5051
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
5052
                                                break;
5053
                                            }
5054
                                        }
5055
5056
                                        echo '<tr>';
5057
                                        if ($this->showExpectedChoice()) {
5058
                                            if (!in_array($this->results_disabled, [
5059
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
5060
                                                //RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
5061
                                            ])
5062
                                            ) {
5063
                                                echo '<td>'.$user_answer.'</td>';
5064
                                            } else {
5065
                                                $status = Display::label(get_lang('Correct'), 'success');
5066
                                            }
5067
                                            echo '<td>'.$s_answer_label.'</td>';
5068
                                            echo '<td>'.$status.'</td>';
5069
                                        } else {
5070
                                            echo '<td>'.$s_answer_label.'</td>';
5071
                                            echo '<td>'.$user_answer.'</td>';
5072
                                            echo '<td>'.$counterAnswer.'</td>';
5073
                                            echo '<td>'.$status.'</td>';
5074
                                            echo '<td>';
5075
                                            if (in_array($answerType, [MATCHING, MATCHING_COMBINATION, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION])) {
5076
                                                if (isset($real_list[$i_answer_correct_answer]) &&
5077
                                                    $showTotalScoreAndUserChoicesInLastAttempt === true
5078
                                                ) {
5079
                                                    echo Display::span(
5080
                                                        $real_list[$i_answer_correct_answer],
5081
                                                        ['style' => 'color: #008000; font-weight: bold;']
5082
                                                    );
5083
                                                }
5084
                                            }
5085
                                            echo '</td>';
5086
                                        }
5087
                                        echo '</tr>';
5088
                                        break;
5089
                                }
5090
                            }
5091
                            $counterAnswer++;
5092
                        }
5093
                        $matchingCorrectAnswers[$questionId]['from_database']['count_options'] = count($options);
5094
                        break 2; // break the switch and the "for" condition
5095
                    } else {
5096
                        if ($answerCorrect) {
5097
                            if (isset($choice[$answerAutoId]) &&
5098
                                $answerCorrect == $choice[$answerAutoId]
5099
                            ) {
5100
                                $matchingCorrectAnswers[$questionId]['form_values']['correct'][$answerAutoId] = $choice[$answerAutoId];
5101
                                $correctAnswerId[] = $answerAutoId;
5102
                                $questionScore += $answerWeighting;
5103
                                $totalScore += $answerWeighting;
5104
                                $user_answer = Display::span($answerMatching[$choice[$answerAutoId]]);
5105
                            } else {
5106
                                if (isset($answerMatching[$choice[$answerAutoId]])) {
5107
                                    $user_answer = Display::span(
5108
                                        $answerMatching[$choice[$answerAutoId]],
5109
                                        ['style' => 'color: #FF0000; text-decoration: line-through;']
5110
                                    );
5111
                                }
5112
                            }
5113
                            $matching[$answerAutoId] = $choice[$answerAutoId];
5114
                        }
5115
                        $matchingCorrectAnswers[$questionId]['form_values']['count_options'] = count($choice);
5116
                    }
5117
                    break;
5118
                case HOT_SPOT:
5119
                case HOT_SPOT_COMBINATION:
5120
                    if ($from_database) {
5121
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
5122
                        // Check auto id
5123
                        $foundAnswerId = $answerAutoId;
5124
                        $sql = "SELECT hotspot_correct
5125
                                FROM $TBL_TRACK_HOTSPOT
5126
                                WHERE
5127
                                    hotspot_exe_id = $exeId AND
5128
                                    hotspot_question_id= $questionId AND
5129
                                    hotspot_answer_id = $answerAutoId
5130
                                ORDER BY hotspot_id ASC";
5131
                        $result = Database::query($sql);
5132
                        if (Database::num_rows($result)) {
5133
                            $studentChoice = Database::result(
5134
                                $result,
5135
                                0,
5136
                                'hotspot_correct'
5137
                            );
5138
5139
                            if ($studentChoice) {
5140
                                $questionScore += $answerWeighting;
5141
                                $totalScore += $answerWeighting;
5142
                            }
5143
                        } else {
5144
                            // If answer.id is different:
5145
                            $sql = "SELECT hotspot_correct
5146
                                FROM $TBL_TRACK_HOTSPOT
5147
                                WHERE
5148
                                    hotspot_exe_id = $exeId AND
5149
                                    hotspot_question_id= $questionId AND
5150
                                    hotspot_answer_id = ".intval($answerId)."
5151
                                ORDER BY hotspot_id ASC";
5152
                            $result = Database::query($sql);
5153
                            $foundAnswerId = $answerId;
5154
                            if (Database::num_rows($result)) {
5155
                                $studentChoice = Database::result(
5156
                                    $result,
5157
                                    0,
5158
                                    'hotspot_correct'
5159
                                );
5160
5161
                                if ($studentChoice) {
5162
                                    $questionScore += $answerWeighting;
5163
                                    $totalScore += $answerWeighting;
5164
                                }
5165
                            } else {
5166
                                // check answer.iid
5167
                                if (!empty($answerIid)) {
5168
                                    $sql = "SELECT hotspot_correct
5169
                                            FROM $TBL_TRACK_HOTSPOT
5170
                                            WHERE
5171
                                                hotspot_exe_id = $exeId AND
5172
                                                hotspot_question_id= $questionId AND
5173
                                                hotspot_answer_id = $answerIid
5174
                                            ORDER BY hotspot_id ASC";
5175
                                    $result = Database::query($sql);
5176
                                    $foundAnswerId = $answerIid;
5177
                                    $studentChoice = Database::result(
5178
                                        $result,
5179
                                        0,
5180
                                        'hotspot_correct'
5181
                                    );
5182
5183
                                    if ($studentChoice) {
5184
                                        $questionScore += $answerWeighting;
5185
                                        $totalScore += $answerWeighting;
5186
                                    }
5187
                                }
5188
                            }
5189
                        }
5190
                    } else {
5191
                        if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
5192
                            $choice[$answerAutoId] = 0;
5193
                            $choice[$answerIid] = 0;
5194
                        } else {
5195
                            $studentChoice = $choice[$answerAutoId];
5196
                            if (empty($studentChoice)) {
5197
                                $studentChoice = $choice[$answerIid];
5198
                            }
5199
                            $choiceIsValid = false;
5200
                            if (!empty($studentChoice)) {
5201
                                $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
5202
                                $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
5203
                                $choicePoint = Geometry::decodePoint($studentChoice);
5204
5205
                                switch ($hotspotType) {
5206
                                    case 'square':
5207
                                        $hotspotProperties = Geometry::decodeSquare($hotspotCoordinates);
5208
                                        $choiceIsValid = Geometry::pointIsInSquare($hotspotProperties, $choicePoint);
5209
                                        break;
5210
                                    case 'circle':
5211
                                        $hotspotProperties = Geometry::decodeEllipse($hotspotCoordinates);
5212
                                        $choiceIsValid = Geometry::pointIsInEllipse($hotspotProperties, $choicePoint);
5213
                                        break;
5214
                                    case 'poly':
5215
                                        $hotspotProperties = Geometry::decodePolygon($hotspotCoordinates);
5216
                                        $choiceIsValid = Geometry::pointIsInPolygon($hotspotProperties, $choicePoint);
5217
                                        break;
5218
                                }
5219
                            }
5220
5221
                            $choice[$answerAutoId] = 0;
5222
                            if ($choiceIsValid) {
5223
                                $questionScore += $answerWeighting;
5224
                                $totalScore += $answerWeighting;
5225
                                $choice[$answerAutoId] = 1;
5226
                                $choice[$answerIid] = 1;
5227
                            }
5228
                        }
5229
                    }
5230
                    break;
5231
                case HOT_SPOT_ORDER:
5232
                    // @todo never added to chamilo
5233
                    // for hotspot with fixed order
5234
                    $studentChoice = $choice['order'][$answerId];
5235
                    if ($studentChoice == $answerId) {
5236
                        $questionScore += $answerWeighting;
5237
                        $totalScore += $answerWeighting;
5238
                        $studentChoice = true;
5239
                    } else {
5240
                        $studentChoice = false;
5241
                    }
5242
                    break;
5243
                case HOT_SPOT_DELINEATION:
5244
                    // for hotspot with delineation
5245
                    if ($from_database) {
5246
                        // getting the user answer
5247
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
5248
                        $query = "SELECT hotspot_correct, hotspot_coordinate
5249
                                    FROM $TBL_TRACK_HOTSPOT
5250
                                    WHERE
5251
                                        hotspot_exe_id = $exeId AND
5252
                                        hotspot_question_id= $questionId AND
5253
                                        hotspot_answer_id = '1'";
5254
                        // By default we take 1 because it's a delineation
5255
                        $resq = Database::query($query);
5256
                        $row = Database::fetch_array($resq, 'ASSOC');
5257
5258
                        $choice = $row['hotspot_correct'];
5259
                        $user_answer = $row['hotspot_coordinate'];
5260
5261
                        // THIS is very important otherwise the poly_compile will throw an error!!
5262
                        // round-up the coordinates
5263
                        $coords = explode('/', $user_answer);
5264
                        $coords = array_filter($coords);
5265
                        $user_array = '';
5266
                        foreach ($coords as $coord) {
5267
                            [$x, $y] = explode(';', $coord);
5268
                            $user_array .= round($x).';'.round($y).'/';
5269
                        }
5270
                        $user_array = substr($user_array, 0, -1) ?: '';
5271
                    } else {
5272
                        if (!empty($studentChoice)) {
5273
                            $correctAnswerId[] = $answerAutoId;
5274
                            $newquestionList[] = $questionId;
5275
                        }
5276
5277
                        if (1 === $answerId) {
5278
                            $studentChoice = $choice[$answerId];
5279
                            $questionScore += $answerWeighting;
5280
                        }
5281
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
5282
                            $user_array = $_SESSION['exerciseResultCoordinates'][$questionId];
5283
                        }
5284
                    }
5285
                    $_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
5286
                    $_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
5287
                    break;
5288
                case ANNOTATION:
5289
                    if ($from_database) {
5290
                        $sql = "SELECT answer, marks
5291
                                FROM $TBL_TRACK_ATTEMPT
5292
                                WHERE
5293
                                  exe_id = $exeId AND
5294
                                  question_id = $questionId ";
5295
                        $resq = Database::query($sql);
5296
                        $data = Database::fetch_array($resq);
5297
5298
                        $questionScore = empty($data['marks']) ? 0 : $data['marks'];
5299
                        $arrques = $questionName;
5300
                        break;
5301
                    }
5302
                    $studentChoice = $choice;
5303
                    if ($studentChoice) {
5304
                        $questionScore = 0;
5305
                    }
5306
                    break;
5307
            }
5308
5309
            if ($show_result) {
5310
                if ('exercise_result' === $from) {
5311
                    // Display answers (if not matching type, or if the answer is correct)
5312
                    if (!in_array($answerType, [MATCHING, MATCHING_COMBINATION, DRAGGABLE, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION]) ||
5313
                        $answerCorrect
5314
                    ) {
5315
                        if (in_array(
5316
                            $answerType,
5317
                            [
5318
                                UNIQUE_ANSWER,
5319
                                UNIQUE_ANSWER_IMAGE,
5320
                                UNIQUE_ANSWER_NO_OPTION,
5321
                                MULTIPLE_ANSWER,
5322
                                MULTIPLE_ANSWER_COMBINATION,
5323
                                GLOBAL_MULTIPLE_ANSWER,
5324
                                READING_COMPREHENSION,
5325
                            ]
5326
                        )) {
5327
                            ExerciseShowFunctions::display_unique_or_multiple_answer(
5328
                                $this,
5329
                                $feedback_type,
5330
                                $answerType,
5331
                                $studentChoice,
5332
                                $answer,
5333
                                $answerComment,
5334
                                $answerCorrect,
5335
                                0,
5336
                                0,
5337
                                0,
5338
                                $results_disabled,
5339
                                $showTotalScoreAndUserChoicesInLastAttempt,
5340
                                $this->export
5341
                            );
5342
                        } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE) {
5343
                            ExerciseShowFunctions::display_multiple_answer_true_false(
5344
                                $this,
5345
                                $feedback_type,
5346
                                $answerType,
5347
                                $studentChoice,
5348
                                $answer,
5349
                                $answerComment,
5350
                                $answerCorrect,
5351
                                0,
5352
                                $questionId,
5353
                                0,
5354
                                $results_disabled,
5355
                                $showTotalScoreAndUserChoicesInLastAttempt
5356
                            );
5357
                        } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
5358
                            ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5359
                                $this,
5360
                                $feedback_type,
5361
                                $studentChoice,
5362
                                $studentChoiceDegree,
5363
                                $answer,
5364
                                $answerComment,
5365
                                $answerCorrect,
5366
                                $questionId,
5367
                                $results_disabled
5368
                            );
5369
                        } elseif ($answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE) {
5370
                            ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5371
                                $this,
5372
                                $feedback_type,
5373
                                $answerType,
5374
                                $studentChoice,
5375
                                $answer,
5376
                                $answerComment,
5377
                                $answerCorrect,
5378
                                0,
5379
                                0,
5380
                                0,
5381
                                $results_disabled,
5382
                                $showTotalScoreAndUserChoicesInLastAttempt
5383
                            );
5384
                        } elseif (in_array($answerType, [FILL_IN_BLANKS, FILL_IN_BLANKS_COMBINATION])) {
5385
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5386
                                $this,
5387
                                $feedback_type,
5388
                                $answer,
5389
                                0,
5390
                                0,
5391
                                $results_disabled,
5392
                                '',
5393
                                $showTotalScoreAndUserChoicesInLastAttempt
5394
                            );
5395
                        } elseif ($answerType == CALCULATED_ANSWER) {
5396
                            ExerciseShowFunctions::display_calculated_answer(
5397
                                $this,
5398
                                $feedback_type,
5399
                                $answer,
5400
                                0,
5401
                                0,
5402
                                $results_disabled,
5403
                                $showTotalScoreAndUserChoicesInLastAttempt,
5404
                                $expectedAnswer,
5405
                                $calculatedChoice,
5406
                                $calculatedStatus
5407
                            );
5408
                        } elseif ($answerType == FREE_ANSWER) {
5409
                            ExerciseShowFunctions::display_free_answer(
5410
                                $feedback_type,
5411
                                $choice,
5412
                                $exeId,
5413
                                $questionId,
5414
                                $questionScore,
5415
                                $results_disabled
5416
                            );
5417
                        } elseif ($answerType == UPLOAD_ANSWER) {
5418
                            ExerciseShowFunctions::displayUploadAnswer(
5419
                                $feedback_type,
5420
                                $choice,
5421
                                $exeId,
5422
                                $questionId,
5423
                                $questionScore,
5424
                                $results_disabled
5425
                            );
5426
                        } elseif ($answerType == ORAL_EXPRESSION) {
5427
                            // to store the details of open questions in an array to be used in mail
5428
                            /** @var OralExpression $objQuestionTmp */
5429
                            ExerciseShowFunctions::display_oral_expression_answer(
5430
                                $feedback_type,
5431
                                $choice,
5432
                                0,
5433
                                0,
5434
                                $objQuestionTmp->getFileUrl(true),
5435
                                $results_disabled,
5436
                                $questionScore
5437
                            );
5438
                        } elseif (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION])) {
5439
                            $correctAnswerId = 0;
5440
                            /** @var TrackEHotspot $hotspot */
5441
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5442
                                if ($hotspot->getHotspotAnswerId() == $answerIid) {
5443
                                    break;
5444
                                }
5445
                            }
5446
5447
                            // force to show whether the choice is correct or not
5448
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
5449
                            ExerciseShowFunctions::display_hotspot_answer(
5450
                                $this,
5451
                                $feedback_type,
5452
                                $answerId,
5453
                                $answer,
5454
                                $studentChoice,
5455
                                $answerComment,
5456
                                $results_disabled,
5457
                                $answerId,
5458
                                $showTotalScoreAndUserChoicesInLastAttempt
5459
                            );
5460
                        } elseif ($answerType == HOT_SPOT_ORDER) {
5461
                            ExerciseShowFunctions::display_hotspot_order_answer(
5462
                                $feedback_type,
5463
                                $answerId,
5464
                                $answer,
5465
                                $studentChoice,
5466
                                $answerComment
5467
                            );
5468
                        } elseif ($answerType == HOT_SPOT_DELINEATION) {
5469
                            $user_answer = $_SESSION['exerciseResultCoordinates'][$questionId];
5470
5471
                            // Round-up the coordinates
5472
                            $coords = explode('/', $user_answer);
5473
                            $coords = array_filter($coords);
5474
                            $user_array = '';
5475
                            foreach ($coords as $coord) {
5476
                                if (!empty($coord)) {
5477
                                    $parts = explode(';', $coord);
5478
                                    if (!empty($parts)) {
5479
                                        $user_array .= round($parts[0]).';'.round($parts[1]).'/';
5480
                                    }
5481
                                }
5482
                            }
5483
                            $user_array = substr($user_array, 0, -1) ?: '';
5484
                            if ($next) {
5485
                                $user_answer = $user_array;
5486
                                // We compare only the delineation not the other points
5487
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5488
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5489
5490
                                // Calculating the area
5491
                                $poly_user = convert_coordinates($user_answer, '/');
5492
                                $poly_answer = convert_coordinates($answer_question, '|');
5493
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5494
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5495
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5496
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5497
5498
                                $overlap = $poly_results['both'];
5499
                                $poly_answer_area = $poly_results['s1'];
5500
                                $poly_user_area = $poly_results['s2'];
5501
                                $missing = $poly_results['s1Only'];
5502
                                $excess = $poly_results['s2Only'];
5503
5504
                                // //this is an area in pixels
5505
                                if ($debug > 0) {
5506
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5507
                                }
5508
5509
                                if ($overlap < 1) {
5510
                                    // Shortcut to avoid complicated calculations
5511
                                    $final_overlap = 0;
5512
                                    $final_missing = 100;
5513
                                    $final_excess = 100;
5514
                                } else {
5515
                                    // the final overlap is the percentage of the initial polygon
5516
                                    // that is overlapped by the user's polygon
5517
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5518
                                    if ($debug > 1) {
5519
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap, 0);
5520
                                    }
5521
                                    // the final missing area is the percentage of the initial polygon
5522
                                    // that is not overlapped by the user's polygon
5523
                                    $final_missing = 100 - $final_overlap;
5524
                                    if ($debug > 1) {
5525
                                        error_log(__LINE__.' - Final missing is '.$final_missing, 0);
5526
                                    }
5527
                                    // the final excess area is the percentage of the initial polygon's size
5528
                                    // that is covered by the user's polygon outside of the initial polygon
5529
                                    $final_excess = round((((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100);
5530
                                    if ($debug > 1) {
5531
                                        error_log(__LINE__.' - Final excess is '.$final_excess, 0);
5532
                                    }
5533
                                }
5534
5535
                                // Checking the destination parameters parsing the "@@"
5536
                                $destination_items = explode('@@', $answerDestination);
5537
                                $threadhold_total = $destination_items[0];
5538
                                $threadhold_items = explode(';', $threadhold_total);
5539
                                $threadhold1 = $threadhold_items[0]; // overlap
5540
                                $threadhold2 = $threadhold_items[1]; // excess
5541
                                $threadhold3 = $threadhold_items[2]; // missing
5542
5543
                                // if is delineation
5544
                                if ($answerId === 1) {
5545
                                    //setting colors
5546
                                    if ($final_overlap >= $threadhold1) {
5547
                                        $overlap_color = true;
5548
                                    }
5549
                                    if ($final_excess <= $threadhold2) {
5550
                                        $excess_color = true;
5551
                                    }
5552
                                    if ($final_missing <= $threadhold3) {
5553
                                        $missing_color = true;
5554
                                    }
5555
5556
                                    // if pass
5557
                                    if ($final_overlap >= $threadhold1 &&
5558
                                        $final_missing <= $threadhold3 &&
5559
                                        $final_excess <= $threadhold2
5560
                                    ) {
5561
                                        $next = 1; //go to the oars
5562
                                        $result_comment = get_lang('Acceptable');
5563
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5564
                                    } else {
5565
                                        $next = 0;
5566
                                        $result_comment = get_lang('Unacceptable');
5567
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5568
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5569
                                        // checking the destination parameters parsing the "@@"
5570
                                        $destination_items = explode('@@', $answerDestination);
5571
                                    }
5572
                                } elseif ($answerId > 1) {
5573
                                    if ($objAnswerTmp->selectHotspotType($answerId) == 'noerror') {
5574
                                        if ($debug > 0) {
5575
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5576
                                        }
5577
                                        //type no error shouldn't be treated
5578
                                        $next = 1;
5579
                                        continue;
5580
                                    }
5581
                                    if ($debug > 0) {
5582
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5583
                                    }
5584
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5585
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5586
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5587
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5588
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5589
5590
                                    if ($overlap == false) {
5591
                                        //all good, no overlap
5592
                                        $next = 1;
5593
                                        continue;
5594
                                    } else {
5595
                                        if ($debug > 0) {
5596
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5597
                                        }
5598
                                        $organs_at_risk_hit++;
5599
                                        //show the feedback
5600
                                        $next = 0;
5601
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5602
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5603
5604
                                        $destination_items = explode('@@', $answerDestination);
5605
                                        $try_hotspot = $destination_items[1];
5606
                                        $lp_hotspot = $destination_items[2];
5607
                                        $select_question_hotspot = $destination_items[3];
5608
                                        $url_hotspot = $destination_items[4];
5609
                                    }
5610
                                }
5611
                            } else {
5612
                                // the first delineation feedback
5613
                                if ($debug > 0) {
5614
                                    error_log(__LINE__.' first', 0);
5615
                                }
5616
                            }
5617
                        } elseif (in_array($answerType, [MATCHING, MATCHING_COMBINATION, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION])) {
5618
                            echo '<tr>';
5619
                            echo Display::tag('td', $answerMatching[$answerId]);
5620
                            echo Display::tag(
5621
                                'td',
5622
                                "$user_answer / ".Display::tag(
5623
                                    'strong',
5624
                                    $answerMatching[$answerCorrect],
5625
                                    ['style' => 'color: #008000; font-weight: bold;']
5626
                                )
5627
                            );
5628
                            echo '</tr>';
5629
                        } elseif ($answerType == ANNOTATION) {
5630
                            ExerciseShowFunctions::displayAnnotationAnswer(
5631
                                $feedback_type,
5632
                                $exeId,
5633
                                $questionId,
5634
                                $questionScore,
5635
                                $results_disabled
5636
                            );
5637
                        }
5638
                    }
5639
                } else {
5640
                    if ($debug) {
5641
                        error_log('Showing questions $from '.$from);
5642
                    }
5643
5644
                    switch ($answerType) {
5645
                        case UNIQUE_ANSWER:
5646
                        case UNIQUE_ANSWER_IMAGE:
5647
                        case UNIQUE_ANSWER_NO_OPTION:
5648
                        case MULTIPLE_ANSWER:
5649
                        case GLOBAL_MULTIPLE_ANSWER:
5650
                        case MULTIPLE_ANSWER_COMBINATION:
5651
                        case READING_COMPREHENSION:
5652
                            if ($answerId == 1) {
5653
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5654
                                    $this,
5655
                                    $feedback_type,
5656
                                    $answerType,
5657
                                    $studentChoice,
5658
                                    $answer,
5659
                                    $answerComment,
5660
                                    $answerCorrect,
5661
                                    $exeId,
5662
                                    $questionId,
5663
                                    $answerId,
5664
                                    $results_disabled,
5665
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5666
                                    $this->export
5667
                                );
5668
                            } else {
5669
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5670
                                    $this,
5671
                                    $feedback_type,
5672
                                    $answerType,
5673
                                    $studentChoice,
5674
                                    $answer,
5675
                                    $answerComment,
5676
                                    $answerCorrect,
5677
                                    $exeId,
5678
                                    $questionId,
5679
                                    '',
5680
                                    $results_disabled,
5681
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5682
                                    $this->export
5683
                                );
5684
                            }
5685
                            break;
5686
                        case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
5687
                            if ($answerId == 1) {
5688
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5689
                                    $this,
5690
                                    $feedback_type,
5691
                                    $answerType,
5692
                                    $studentChoice,
5693
                                    $answer,
5694
                                    $answerComment,
5695
                                    $answerCorrect,
5696
                                    $exeId,
5697
                                    $questionId,
5698
                                    $answerId,
5699
                                    $results_disabled,
5700
                                    $showTotalScoreAndUserChoicesInLastAttempt
5701
                                );
5702
                            } else {
5703
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5704
                                    $this,
5705
                                    $feedback_type,
5706
                                    $answerType,
5707
                                    $studentChoice,
5708
                                    $answer,
5709
                                    $answerComment,
5710
                                    $answerCorrect,
5711
                                    $exeId,
5712
                                    $questionId,
5713
                                    '',
5714
                                    $results_disabled,
5715
                                    $showTotalScoreAndUserChoicesInLastAttempt
5716
                                );
5717
                            }
5718
                            break;
5719
                        case MULTIPLE_ANSWER_TRUE_FALSE:
5720
                            if ($answerId == 1) {
5721
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5722
                                    $this,
5723
                                    $feedback_type,
5724
                                    $answerType,
5725
                                    $studentChoice,
5726
                                    $answer,
5727
                                    $answerComment,
5728
                                    $answerCorrect,
5729
                                    $exeId,
5730
                                    $questionId,
5731
                                    $answerId,
5732
                                    $results_disabled,
5733
                                    $showTotalScoreAndUserChoicesInLastAttempt
5734
                                );
5735
                            } else {
5736
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5737
                                    $this,
5738
                                    $feedback_type,
5739
                                    $answerType,
5740
                                    $studentChoice,
5741
                                    $answer,
5742
                                    $answerComment,
5743
                                    $answerCorrect,
5744
                                    $exeId,
5745
                                    $questionId,
5746
                                    '',
5747
                                    $results_disabled,
5748
                                    $showTotalScoreAndUserChoicesInLastAttempt
5749
                                );
5750
                            }
5751
                            break;
5752
                        case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
5753
                            if ($answerId == 1) {
5754
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5755
                                    $this,
5756
                                    $feedback_type,
5757
                                    $studentChoice,
5758
                                    $studentChoiceDegree,
5759
                                    $answer,
5760
                                    $answerComment,
5761
                                    $answerCorrect,
5762
                                    $questionId,
5763
                                    $results_disabled
5764
                                );
5765
                            } else {
5766
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5767
                                    $this,
5768
                                    $feedback_type,
5769
                                    $studentChoice,
5770
                                    $studentChoiceDegree,
5771
                                    $answer,
5772
                                    $answerComment,
5773
                                    $answerCorrect,
5774
                                    $questionId,
5775
                                    $results_disabled
5776
                                );
5777
                            }
5778
                            break;
5779
                        case FILL_IN_BLANKS:
5780
                        case FILL_IN_BLANKS_COMBINATION:
5781
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5782
                                $this,
5783
                                $feedback_type,
5784
                                $answer,
5785
                                $exeId,
5786
                                $questionId,
5787
                                $results_disabled,
5788
                                $str,
5789
                                $showTotalScoreAndUserChoicesInLastAttempt
5790
                            );
5791
                            break;
5792
                        case CALCULATED_ANSWER:
5793
                            ExerciseShowFunctions::display_calculated_answer(
5794
                                $this,
5795
                                $feedback_type,
5796
                                $answer,
5797
                                $exeId,
5798
                                $questionId,
5799
                                $results_disabled,
5800
                                '',
5801
                                $showTotalScoreAndUserChoicesInLastAttempt
5802
                            );
5803
                            break;
5804
                        case FREE_ANSWER:
5805
                            echo ExerciseShowFunctions::display_free_answer(
5806
                                $feedback_type,
5807
                                $choice,
5808
                                $exeId,
5809
                                $questionId,
5810
                                $questionScore,
5811
                                $results_disabled
5812
                            );
5813
                            break;
5814
                        case UPLOAD_ANSWER:
5815
                            echo ExerciseShowFunctions::displayUploadAnswer(
5816
                                $feedback_type,
5817
                                $choice,
5818
                                $exeId,
5819
                                $questionId,
5820
                                $questionScore,
5821
                                $results_disabled
5822
                            );
5823
                            break;
5824
                        case ORAL_EXPRESSION:
5825
                            echo '<tr>
5826
                                <td>'.
5827
                                ExerciseShowFunctions::display_oral_expression_answer(
5828
                                    $feedback_type,
5829
                                    $choice,
5830
                                    $exeId,
5831
                                    $questionId,
5832
                                    $objQuestionTmp->getFileUrl(),
5833
                                    $results_disabled,
5834
                                    $questionScore
5835
                                ).'</td>
5836
                                </tr>
5837
                                </table>';
5838
                            break;
5839
                        case HOT_SPOT:
5840
                        case HOT_SPOT_COMBINATION:
5841
                            $correctAnswerId = 0;
5842
                            /** @var TrackEHotspot $hotspot */
5843
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5844
                                if ($hotspot->getHotspotAnswerId() == $foundAnswerId) {
5845
                                    break;
5846
                                }
5847
                            }
5848
5849
                            ExerciseShowFunctions::display_hotspot_answer(
5850
                                $this,
5851
                                $feedback_type,
5852
                                $answerId,
5853
                                $answer,
5854
                                $studentChoice,
5855
                                $answerComment,
5856
                                $results_disabled,
5857
                                $answerId,
5858
                                $showTotalScoreAndUserChoicesInLastAttempt
5859
                            );
5860
                            break;
5861
                        case HOT_SPOT_DELINEATION:
5862
                            $user_answer = $user_array;
5863
                            if ($next) {
5864
                                $user_answer = $user_array;
5865
                                // we compare only the delineation not the other points
5866
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5867
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5868
5869
                                // calculating the area
5870
                                $poly_user = convert_coordinates($user_answer, '/');
5871
                                $poly_answer = convert_coordinates($answer_question, '|');
5872
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5873
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5874
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5875
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5876
5877
                                $overlap = $poly_results['both'];
5878
                                $poly_answer_area = $poly_results['s1'];
5879
                                $poly_user_area = $poly_results['s2'];
5880
                                $missing = $poly_results['s1Only'];
5881
                                $excess = $poly_results['s2Only'];
5882
                                if ($debug > 0) {
5883
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5884
                                }
5885
                                if ($overlap < 1) {
5886
                                    //shortcut to avoid complicated calculations
5887
                                    $final_overlap = 0;
5888
                                    $final_missing = 100;
5889
                                    $final_excess = 100;
5890
                                } else {
5891
                                    // the final overlap is the percentage of the initial polygon
5892
                                    // that is overlapped by the user's polygon
5893
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5894
5895
                                    // the final missing area is the percentage of the initial polygon that
5896
                                    // is not overlapped by the user's polygon
5897
                                    $final_missing = 100 - $final_overlap;
5898
                                    // the final excess area is the percentage of the initial polygon's size that is
5899
                                    // covered by the user's polygon outside of the initial polygon
5900
                                    $final_excess = round((((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100);
5901
5902
                                    if ($debug > 1) {
5903
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap);
5904
                                        error_log(__LINE__.' - Final excess is '.$final_excess);
5905
                                        error_log(__LINE__.' - Final missing is '.$final_missing);
5906
                                    }
5907
                                }
5908
5909
                                // Checking the destination parameters parsing the "@@"
5910
                                $destination_items = explode('@@', $answerDestination);
5911
                                $threadhold_total = $destination_items[0];
5912
                                $threadhold_items = explode(';', $threadhold_total);
5913
                                $threadhold1 = $threadhold_items[0]; // overlap
5914
                                $threadhold2 = $threadhold_items[1]; // excess
5915
                                $threadhold3 = $threadhold_items[2]; //missing
5916
                                // if is delineation
5917
                                if ($answerId === 1) {
5918
                                    //setting colors
5919
                                    if ($final_overlap >= $threadhold1) {
5920
                                        $overlap_color = true;
5921
                                    }
5922
                                    if ($final_excess <= $threadhold2) {
5923
                                        $excess_color = true;
5924
                                    }
5925
                                    if ($final_missing <= $threadhold3) {
5926
                                        $missing_color = true;
5927
                                    }
5928
5929
                                    // if pass
5930
                                    if ($final_overlap >= $threadhold1 &&
5931
                                        $final_missing <= $threadhold3 &&
5932
                                        $final_excess <= $threadhold2
5933
                                    ) {
5934
                                        $next = 1; //go to the oars
5935
                                        $result_comment = get_lang('Acceptable');
5936
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5937
                                    } else {
5938
                                        $next = 0;
5939
                                        $result_comment = get_lang('Unacceptable');
5940
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5941
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5942
                                        //checking the destination parameters parsing the "@@"
5943
                                        $destination_items = explode('@@', $answerDestination);
5944
                                    }
5945
                                } elseif ($answerId > 1) {
5946
                                    if ($objAnswerTmp->selectHotspotType($answerId) === 'noerror') {
5947
                                        if ($debug > 0) {
5948
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5949
                                        }
5950
                                        //type no error shouldn't be treated
5951
                                        $next = 1;
5952
                                        break;
5953
                                    }
5954
                                    if ($debug > 0) {
5955
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5956
                                    }
5957
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5958
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5959
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5960
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5961
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5962
5963
                                    if ($overlap == false) {
5964
                                        //all good, no overlap
5965
                                        $next = 1;
5966
                                        break;
5967
                                    } else {
5968
                                        if ($debug > 0) {
5969
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5970
                                        }
5971
                                        $organs_at_risk_hit++;
5972
                                        //show the feedback
5973
                                        $next = 0;
5974
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5975
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5976
5977
                                        $destination_items = explode('@@', $answerDestination);
5978
                                        $try_hotspot = $destination_items[1];
5979
                                        $lp_hotspot = $destination_items[2];
5980
                                        $select_question_hotspot = $destination_items[3];
5981
                                        $url_hotspot = $destination_items[4];
5982
                                    }
5983
                                }
5984
                            }
5985
                            break;
5986
                        case HOT_SPOT_ORDER:
5987
                            ExerciseShowFunctions::display_hotspot_order_answer(
5988
                                $feedback_type,
5989
                                $answerId,
5990
                                $answer,
5991
                                $studentChoice,
5992
                                $answerComment
5993
                            );
5994
                            break;
5995
                        case DRAGGABLE:
5996
                        case MATCHING_DRAGGABLE:
5997
                        case MATCHING_DRAGGABLE_COMBINATION:
5998
                        case MATCHING_COMBINATION:
5999
                        case MATCHING:
6000
                            echo '<tr>';
6001
                            echo Display::tag('td', $answerMatching[$answerId]);
6002
                            echo Display::tag(
6003
                                'td',
6004
                                "$user_answer / ".Display::tag(
6005
                                    'strong',
6006
                                    $answerMatching[$answerCorrect],
6007
                                    ['style' => 'color: #008000; font-weight: bold;']
6008
                                )
6009
                            );
6010
                            echo '</tr>';
6011
                            break;
6012
                        case ANNOTATION:
6013
                            ExerciseShowFunctions::displayAnnotationAnswer(
6014
                                $feedback_type,
6015
                                $exeId,
6016
                                $questionId,
6017
                                $questionScore,
6018
                                $results_disabled
6019
                            );
6020
                            break;
6021
                    }
6022
                }
6023
            }
6024
        } // end for that loops over all answers of the current question
6025
6026
        // It validates unique score when all answers are correct for global questions
6027
        if (FILL_IN_BLANKS_COMBINATION === $answerType) {
6028
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
6029
                $answerType,
6030
                $listCorrectAnswers,
6031
                $exeId,
6032
                $questionId,
6033
                $questionWeighting
6034
            );
6035
        }
6036
        if (HOT_SPOT_COMBINATION === $answerType) {
6037
            $listCorrectAnswers = isset($exerciseResultCoordinates[$questionId]) ? $exerciseResultCoordinates[$questionId] : [];
6038
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
6039
                $answerType,
6040
                $listCorrectAnswers,
6041
                $exeId,
6042
                $questionId,
6043
                $questionWeighting,
6044
                $choice,
6045
                $nbrAnswers
6046
            );
6047
        }
6048
        if (in_array($answerType, [MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
6049
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
6050
                $answerType,
6051
                $matchingCorrectAnswers[$questionId],
6052
                $exeId,
6053
                $questionId,
6054
                $questionWeighting
6055
            );
6056
        }
6057
6058
        if ($debug) {
6059
            error_log('-- End answer loop --');
6060
        }
6061
6062
        $final_answer = true;
6063
6064
        foreach ($real_answers as $my_answer) {
6065
            if (!$my_answer) {
6066
                $final_answer = false;
6067
            }
6068
        }
6069
6070
        // We add the total score after dealing with the answers.
6071
        if ($answerType == MULTIPLE_ANSWER_COMBINATION ||
6072
            $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE
6073
        ) {
6074
            if ($final_answer) {
6075
                //getting only the first score where we save the weight of all the question
6076
                $answerWeighting = $objAnswerTmp->selectWeighting(1);
6077
                if (empty($answerWeighting) && !empty($firstAnswer) && isset($firstAnswer['ponderation'])) {
6078
                    $answerWeighting = $firstAnswer['ponderation'];
6079
                }
6080
                $questionScore += $answerWeighting;
6081
            }
6082
        }
6083
6084
        $extra_data = [
6085
            'final_overlap' => $final_overlap,
6086
            'final_missing' => $final_missing,
6087
            'final_excess' => $final_excess,
6088
            'overlap_color' => $overlap_color,
6089
            'missing_color' => $missing_color,
6090
            'excess_color' => $excess_color,
6091
            'threadhold1' => $threadhold1,
6092
            'threadhold2' => $threadhold2,
6093
            'threadhold3' => $threadhold3,
6094
        ];
6095
6096
        if ($from === 'exercise_result') {
6097
            // if answer is hotspot. To the difference of exercise_show.php,
6098
            //  we use the results from the session (from_db=0)
6099
            // TODO Change this, because it is wrong to show the user
6100
            //  some results that haven't been stored in the database yet
6101
            if (in_array($answerType, [HOT_SPOT, HOT_SPOT_ORDER, HOT_SPOT_DELINEATION, HOT_SPOT_COMBINATION])) {
6102
                if ($debug) {
6103
                    error_log('$from AND this is a hotspot kind of question ');
6104
                }
6105
                if ($answerType === HOT_SPOT_DELINEATION) {
6106
                    if ($showHotSpotDelineationTable) {
6107
                        if (!is_numeric($final_overlap)) {
6108
                            $final_overlap = 0;
6109
                        }
6110
                        if (!is_numeric($final_missing)) {
6111
                            $final_missing = 0;
6112
                        }
6113
                        if (!is_numeric($final_excess)) {
6114
                            $final_excess = 0;
6115
                        }
6116
6117
                        if ($final_overlap > 100) {
6118
                            $final_overlap = 100;
6119
                        }
6120
6121
                        $table_resume = '<table class="table table-hover table-striped data_table">
6122
                                <tr class="row_odd" >
6123
                                    <td></td>
6124
                                    <td ><b>'.get_lang('Requirements').'</b></td>
6125
                                    <td><b>'.get_lang('YourAnswer').'</b></td>
6126
                                </tr>
6127
                                <tr class="row_even">
6128
                                    <td><b>'.get_lang('Overlap').'</b></td>
6129
                                    <td>'.get_lang('Min').' '.$threadhold1.'</td>
6130
                                    <td class="text-right '.($overlap_color ? 'text-success' : 'text-danger').'">'
6131
                            .(($final_overlap < 0) ? 0 : intval($final_overlap)).'</td>
6132
                                </tr>
6133
                                <tr>
6134
                                    <td><b>'.get_lang('Excess').'</b></td>
6135
                                    <td>'.get_lang('Max').' '.$threadhold2.'</td>
6136
                                    <td class="text-right '.($excess_color ? 'text-success' : 'text-danger').'">'
6137
                            .(($final_excess < 0) ? 0 : intval($final_excess)).'</td>
6138
                                </tr>
6139
                                <tr class="row_even">
6140
                                    <td><b>'.get_lang('Missing').'</b></td>
6141
                                    <td>'.get_lang('Max').' '.$threadhold3.'</td>
6142
                                    <td class="text-right '.($missing_color ? 'text-success' : 'text-danger').'">'
6143
                            .(($final_missing < 0) ? 0 : intval($final_missing)).'</td>
6144
                                </tr>
6145
                            </table>';
6146
                        if ($next == 0) {
6147
                        } else {
6148
                            $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
6149
                            $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
6150
                        }
6151
6152
                        $message = '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>
6153
                                    <p style="text-align:center">';
6154
                        $message .= '<p>'.get_lang('YourDelineation').'</p>';
6155
                        $message .= $table_resume;
6156
                        $message .= '<br />'.get_lang('ResultIs').' '.$result_comment.'<br />';
6157
                        if ($organs_at_risk_hit > 0) {
6158
                            $message .= '<p><b>'.get_lang('OARHit').'</b></p>';
6159
                        }
6160
                        $message .= '<p>'.$comment.'</p>';
6161
                        echo $message;
6162
6163
                        $_SESSION['hotspot_delineation_result'][$this->selectId()][$questionId][0] = $message;
6164
                        $_SESSION['hotspot_delineation_result'][$this->selectId()][$questionId][1] = $_SESSION['exerciseResultCoordinates'][$questionId];
6165
                    } else {
6166
                        echo $hotspot_delineation_result[0];
6167
                    }
6168
6169
                    // Save the score attempts
6170
                    if (1) {
6171
                        //getting the answer 1 or 0 comes from exercise_submit_modal.php
6172
                        $final_answer = isset($hotspot_delineation_result[1]) ? $hotspot_delineation_result[1] : '';
6173
                        if ($final_answer == 0) {
6174
                            $questionScore = 0;
6175
                        }
6176
                        // we always insert the answer_id 1 = delineation
6177
                        Event::saveQuestionAttempt($questionScore, 1, $quesId, $exeId, 0);
6178
                        //in delineation mode, get the answer from $hotspot_delineation_result[1]
6179
                        $hotspotValue = isset($hotspot_delineation_result[1]) ? (int) $hotspot_delineation_result[1] === 1 ? 1 : 0 : 0;
6180
                        Event::saveExerciseAttemptHotspot(
6181
                            $exeId,
6182
                            $quesId,
6183
                            1,
6184
                            $hotspotValue,
6185
                            $exerciseResultCoordinates[$quesId],
6186
                            false,
6187
                            0,
6188
                            $learnpath_id,
6189
                            $learnpath_item_id
6190
                        );
6191
                    } else {
6192
                        if ($final_answer == 0) {
6193
                            $questionScore = 0;
6194
                            $answer = 0;
6195
                            Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0);
6196
                            if (is_array($exerciseResultCoordinates[$quesId])) {
6197
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
6198
                                    Event::saveExerciseAttemptHotspot(
6199
                                        $exeId,
6200
                                        $quesId,
6201
                                        $idx,
6202
                                        0,
6203
                                        $val,
6204
                                        false,
6205
                                        0,
6206
                                        $learnpath_id,
6207
                                        $learnpath_item_id
6208
                                    );
6209
                                }
6210
                            }
6211
                        } else {
6212
                            Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0);
6213
                            if (is_array($exerciseResultCoordinates[$quesId])) {
6214
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
6215
                                    $hotspotValue = (int) $choice[$idx] === 1 ? 1 : 0;
6216
                                    Event::saveExerciseAttemptHotspot(
6217
                                        $exeId,
6218
                                        $quesId,
6219
                                        $idx,
6220
                                        $hotspotValue,
6221
                                        $val,
6222
                                        false,
6223
                                        0,
6224
                                        $learnpath_id,
6225
                                        $learnpath_item_id
6226
                                    );
6227
                                }
6228
                            }
6229
                        }
6230
                    }
6231
                }
6232
            }
6233
6234
            $relPath = api_get_path(WEB_CODE_PATH);
6235
6236
            if (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION, HOT_SPOT_ORDER])) {
6237
                // We made an extra table for the answers
6238
                if ($show_result) {
6239
                    echo '</table></td></tr>';
6240
                    echo "
6241
                        <tr>
6242
                            <td>
6243
                                <p><em>".get_lang('HotSpot')."</em></p>
6244
                                <div id=\"hotspot-solution-$questionId\"></div>
6245
                                <script>
6246
                                    $(function() {
6247
                                        new HotspotQuestion({
6248
                                            questionId: $questionId,
6249
                                            exerciseId: {$this->iid},
6250
                                            exeId: $exeId,
6251
                                            selector: '#hotspot-solution-$questionId',
6252
                                            for: 'solution',
6253
                                            relPath: '$relPath'
6254
                                        });
6255
                                    });
6256
                                </script>
6257
                            </td>
6258
                        </tr>
6259
                    ";
6260
                }
6261
            } elseif ($answerType == ANNOTATION) {
6262
                if ($show_result) {
6263
                    echo '
6264
                        <p><em>'.get_lang('Annotation').'</em></p>
6265
                        <div id="annotation-canvas-'.$questionId.'"></div>
6266
                        <script>
6267
                            AnnotationQuestion({
6268
                                questionId: parseInt('.$questionId.'),
6269
                                exerciseId: parseInt('.$exeId.'),
6270
                                relPath: \''.$relPath.'\',
6271
                                courseId: parseInt('.$course_id.')
6272
                            });
6273
                        </script>
6274
                    ';
6275
                }
6276
            }
6277
6278
            if ($show_result && $answerType != ANNOTATION) {
6279
                echo '</table>';
6280
            }
6281
        }
6282
        unset($objAnswerTmp);
6283
6284
        $totalWeighting += $questionWeighting;
6285
        // Store results directly in the database
6286
        // For all in one page exercises, the results will be
6287
        // stored by exercise_results.php (using the session)
6288
        if ($save_results) {
6289
            if ($debug) {
6290
                error_log("Save question results $save_results");
6291
                error_log("Question score: $questionScore");
6292
                error_log('choice: ');
6293
                error_log(print_r($choice, 1));
6294
            }
6295
6296
            if (empty($choice)) {
6297
                $choice = 0;
6298
            }
6299
            // with certainty degree
6300
            if (empty($choiceDegreeCertainty)) {
6301
                $choiceDegreeCertainty = 0;
6302
            }
6303
            if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE ||
6304
                $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE ||
6305
                $answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY
6306
            ) {
6307
                if ($choice != 0) {
6308
                    $reply = array_keys($choice);
6309
                    $countReply = count($reply);
6310
                    for ($i = 0; $i < $countReply; $i++) {
6311
                        $chosenAnswer = $reply[$i];
6312
                        if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
6313
                            if ($choiceDegreeCertainty != 0) {
6314
                                $replyDegreeCertainty = array_keys($choiceDegreeCertainty);
6315
                                $answerDegreeCertainty = isset($replyDegreeCertainty[$i]) ? $replyDegreeCertainty[$i] : '';
6316
                                $answerValue = isset($choiceDegreeCertainty[$answerDegreeCertainty]) ? $choiceDegreeCertainty[$answerDegreeCertainty] : '';
6317
                                Event::saveQuestionAttempt(
6318
                                    $questionScore,
6319
                                    $chosenAnswer.':'.$choice[$chosenAnswer].':'.$answerValue,
6320
                                    $quesId,
6321
                                    $exeId,
6322
                                    $i,
6323
                                    $this->iid,
6324
                                    $updateResults,
6325
                                    $questionDuration
6326
                                );
6327
                            }
6328
                        } else {
6329
                            Event::saveQuestionAttempt(
6330
                                $questionScore,
6331
                                $chosenAnswer.':'.$choice[$chosenAnswer],
6332
                                $quesId,
6333
                                $exeId,
6334
                                $i,
6335
                                $this->iid,
6336
                                $updateResults,
6337
                                $questionDuration
6338
                            );
6339
                        }
6340
                        if ($debug) {
6341
                            error_log('result =>'.$questionScore.' '.$chosenAnswer.':'.$choice[$chosenAnswer]);
6342
                        }
6343
                    }
6344
                } else {
6345
                    Event::saveQuestionAttempt(
6346
                        $questionScore,
6347
                        0,
6348
                        $quesId,
6349
                        $exeId,
6350
                        0,
6351
                        $this->iid,
6352
                        false,
6353
                        $questionDuration
6354
                    );
6355
                }
6356
            } elseif (
6357
                in_array(
6358
                    $answerType,
6359
                    [
6360
                        MULTIPLE_ANSWER,
6361
                        GLOBAL_MULTIPLE_ANSWER,
6362
                        MULTIPLE_ANSWER_DROPDOWN,
6363
                        MULTIPLE_ANSWER_DROPDOWN_COMBINATION,
6364
                    ]
6365
                )
6366
            ) {
6367
                if ($choice != 0) {
6368
                    if (in_array($answerType, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION])) {
6369
                        $reply = array_values($choice);
6370
                    } else {
6371
                        $reply = array_keys($choice);
6372
                    }
6373
6374
                    for ($i = 0; $i < count($reply); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

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

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

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
6375
                        $ans = $reply[$i];
6376
                        Event::saveQuestionAttempt(
6377
                            $questionScore,
6378
                            $ans,
6379
                            $quesId,
6380
                            $exeId,
6381
                            $i,
6382
                            $this->iid,
6383
                            false,
6384
                            $questionDuration
6385
                        );
6386
                    }
6387
                } else {
6388
                    Event::saveQuestionAttempt(
6389
                        $questionScore,
6390
                        0,
6391
                        $quesId,
6392
                        $exeId,
6393
                        0,
6394
                        $this->iid,
6395
                        false,
6396
                        $questionDuration
6397
                    );
6398
                }
6399
            } elseif ($answerType == MULTIPLE_ANSWER_COMBINATION) {
6400
                if ($choice != 0) {
6401
                    $reply = array_keys($choice);
6402
                    for ($i = 0; $i < count($reply); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

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

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

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
6403
                        $ans = $reply[$i];
6404
                        Event::saveQuestionAttempt(
6405
                            $questionScore,
6406
                            $ans,
6407
                            $quesId,
6408
                            $exeId,
6409
                            $i,
6410
                            $this->iid,
6411
                            false,
6412
                            $questionDuration
6413
                        );
6414
                    }
6415
                } else {
6416
                    Event::saveQuestionAttempt(
6417
                        $questionScore,
6418
                        0,
6419
                        $quesId,
6420
                        $exeId,
6421
                        0,
6422
                        $this->iid,
6423
                        false,
6424
                        $questionDuration
6425
                    );
6426
                }
6427
            } elseif (in_array($answerType, [MATCHING, MATCHING_COMBINATION, DRAGGABLE, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION])) {
6428
                if (isset($matching)) {
6429
                    foreach ($matching as $j => $val) {
6430
                        Event::saveQuestionAttempt(
6431
                            $questionScore,
6432
                            $val,
6433
                            $quesId,
6434
                            $exeId,
6435
                            $j,
6436
                            $this->iid,
6437
                            false,
6438
                            $questionDuration
6439
                        );
6440
                    }
6441
                }
6442
            } elseif ($answerType == FREE_ANSWER) {
6443
                $answer = $choice;
6444
                Event::saveQuestionAttempt(
6445
                    $questionScore,
6446
                    $answer,
6447
                    $quesId,
6448
                    $exeId,
6449
                    0,
6450
                    $this->iid,
6451
                    false,
6452
                    $questionDuration
6453
                );
6454
            } elseif ($answerType == UPLOAD_ANSWER) {
6455
                $answer = $choice;
6456
                Event::saveQuestionAttempt(
6457
                    $questionScore,
6458
                    $answer,
6459
                    $quesId,
6460
                    $exeId,
6461
                    0,
6462
                    $this->iid,
6463
                    false,
6464
                    $questionDuration
6465
                );
6466
            } elseif ($answerType == ORAL_EXPRESSION) {
6467
                $answer = $choice;
6468
                $absFilePath = $objQuestionTmp->getAbsoluteFilePath();
6469
                if (empty($answer) && !empty($absFilePath)) {
6470
                    // it takes the filename as answer to recognise has been saved
6471
                    $answer = basename($absFilePath);
6472
                }
6473
                Event::saveQuestionAttempt(
6474
                    $questionScore,
6475
                    $answer,
6476
                    $quesId,
6477
                    $exeId,
6478
                    0,
6479
                    $this->iid,
6480
                    false,
6481
                    $questionDuration,
6482
                    $absFilePath
6483
                );
6484
            } elseif (
6485
                in_array(
6486
                    $answerType,
6487
                    [UNIQUE_ANSWER, UNIQUE_ANSWER_IMAGE, UNIQUE_ANSWER_NO_OPTION, READING_COMPREHENSION]
6488
                )
6489
            ) {
6490
                $answer = $choice;
6491
                Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0, $this->iid, false, $questionDuration);
6492
            } elseif (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION, ANNOTATION])) {
6493
                $answer = [];
6494
                if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) {
6495
                    if ($debug) {
6496
                        error_log('Checking result coordinates');
6497
                    }
6498
                    Database::delete(
6499
                        Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
6500
                        [
6501
                            'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
6502
                                $exeId,
6503
                                $questionId,
6504
                                api_get_course_int_id(),
6505
                            ],
6506
                        ]
6507
                    );
6508
6509
                    foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
6510
                        $answer[] = $val;
6511
                        $hotspotValue = (int) $choice[$idx] === 1 ? 1 : 0;
6512
                        if ($debug) {
6513
                            error_log('Hotspot value: '.$hotspotValue);
6514
                        }
6515
                        Event::saveExerciseAttemptHotspot(
6516
                            $exeId,
6517
                            $quesId,
6518
                            $idx,
6519
                            $hotspotValue,
6520
                            $val,
6521
                            false,
6522
                            $this->iid,
6523
                            $learnpath_id,
6524
                            $learnpath_item_id
6525
                        );
6526
                    }
6527
                } else {
6528
                    if ($debug) {
6529
                        error_log('Empty: exerciseResultCoordinates');
6530
                    }
6531
                }
6532
                Event::saveQuestionAttempt(
6533
                    $questionScore,
6534
                    implode('|', $answer),
6535
                    $quesId,
6536
                    $exeId,
6537
                    0,
6538
                    $this->iid,
6539
                    false,
6540
                    $questionDuration
6541
                );
6542
            } else {
6543
                if ($answerType === CALCULATED_ANSWER && !empty($calculatedAnswerId)) {
6544
                    $answer .= ':::'.$calculatedAnswerId;
6545
                }
6546
6547
                Event::saveQuestionAttempt(
6548
                    $questionScore,
6549
                    $answer,
6550
                    $quesId,
6551
                    $exeId,
6552
                    0,
6553
                    $this->iid,
6554
                    false,
6555
                    $questionDuration
6556
                );
6557
            }
6558
        }
6559
6560
        if ($propagate_neg == 0 && $questionScore < 0) {
6561
            $questionScore = 0;
6562
        }
6563
6564
        if ($save_results) {
6565
            $statsTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6566
            $sql = "UPDATE $statsTable SET
6567
                        exe_result = exe_result + ".floatval($questionScore)."
6568
                    WHERE exe_id = $exeId";
6569
            Database::query($sql);
6570
        }
6571
6572
        return [
6573
            'score' => $questionScore,
6574
            'weight' => $questionWeighting,
6575
            'extra' => $extra_data,
6576
            'open_question' => $arrques,
6577
            'open_answer' => $arrans,
6578
            'answer_type' => $answerType,
6579
            'generated_oral_file' => $generatedFile,
6580
            'user_answered' => $userAnsweredQuestion,
6581
            'correct_answer_id' => $correctAnswerId,
6582
            'answer_destination' => $answerDestination,
6583
        ];
6584
    }
6585
6586
    /**
6587
     * Sends a notification when a user ends an examn.
6588
     *
6589
     * @param string $type                  'start' or 'end' of an exercise
6590
     * @param array  $question_list_answers
6591
     * @param string $origin
6592
     * @param int    $exe_id
6593
     * @param float  $score
6594
     * @param float  $weight
6595
     *
6596
     * @return bool
6597
     */
6598
    public function send_mail_notification_for_exam(
6599
        $type,
6600
        $question_list_answers,
6601
        $origin,
6602
        $exe_id,
6603
        $score = null,
6604
        $weight = null
6605
    ) {
6606
        $setting = api_get_course_setting('email_alert_manager_on_new_quiz');
6607
6608
        if (empty($setting) && empty($this->getNotifications())) {
6609
            return false;
6610
        }
6611
6612
        $settingFromExercise = $this->getNotifications();
6613
        if (!empty($settingFromExercise)) {
6614
            $setting = $settingFromExercise;
6615
        }
6616
6617
        // Email configuration settings
6618
        $courseCode = api_get_course_id();
6619
        $courseInfo = api_get_course_info($courseCode);
6620
6621
        if (empty($courseInfo)) {
6622
            return false;
6623
        }
6624
6625
        $sessionId = api_get_session_id();
6626
6627
        $sessionData = '';
6628
        if (!empty($sessionId)) {
6629
            $sessionInfo = api_get_session_info($sessionId);
6630
            if (!empty($sessionInfo)) {
6631
                $sessionData = '<tr>'
6632
                    .'<td>'.get_lang('SessionName').'</td>'
6633
                    .'<td>'.$sessionInfo['name'].'</td>'
6634
                    .'</tr>';
6635
            }
6636
        }
6637
6638
        $sendStart = false;
6639
        $sendEnd = false;
6640
        $sendEndOpenQuestion = false;
6641
        $sendEndOralQuestion = false;
6642
6643
        foreach ($setting as $option) {
6644
            switch ($option) {
6645
                case 0:
6646
                    return false;
6647
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

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

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

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

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