Exercise::enable_results()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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