Passed
Push — master ( 924d6f...5db41b )
by Julito
10:11
created

Exercise::added_in_lp()   A

Complexity

Conditions 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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