Exercise::countQuestionsInExercise()   A
last analyzed

Complexity

Conditions 5

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
10991
    }
10992
}
10993