Exercise::getHideQuestionNumber()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 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
            }
2362
2363
            $skillList = SkillModel::addSkillsToForm($form, ITEM_TYPE_EXERCISE, $this->iId);
2364
2365
            $extraField = new ExtraField('exercise');
2366
            $extraField->addElements(
2367
                $form,
2368
                $this->iId,
2369
                ['notifications'], //exclude
2370
                false, // filter
2371
                false, // tag as select
2372
                [], //show only fields
2373
                [], // order fields
2374
                [] // extra data
2375
            );
2376
            $settings = api_get_configuration_value('exercise_finished_notification_settings');
2377
            if (!empty($settings)) {
2378
                $options = [];
2379
                foreach ($settings as $name => $data) {
2380
                    $options[$name] = $name;
2381
                }
2382
                $form->addSelect(
2383
                    'extra_notifications',
2384
                    get_lang('Notifications'),
2385
                    $options,
2386
                    ['placeholder' => get_lang('Please select an option')]
2387
                );
2388
            }
2389
            $form->addElement('html', '</div>'); //End advanced setting
2390
            $form->addElement('html', '</div>');
2391
        }
2392
2393
        // submit
2394
        if (isset($_GET['id'])) {
2395
            $form->addButtonSave(get_lang('Edit test name and settings'), 'submitExercise');
2396
        } else {
2397
            $form->addButtonUpdate(get_lang('Proceed to questions'), 'submitExercise');
2398
        }
2399
2400
        $form->addRule('exerciseTitle', get_lang('Name'), 'required');
2401
2402
        // defaults
2403
        if ('full' == $type) {
2404
            // rules
2405
            $form->addRule('exerciseAttempts', get_lang('Numerical'), 'numeric');
2406
            $form->addRule('start_time', get_lang('Invalid date'), 'datetime');
2407
            $form->addRule('end_time', get_lang('Invalid date'), 'datetime');
2408
2409
            if ($this->getId() > 0) {
2410
                $defaults['randomQuestions'] = $this->random;
2411
                $defaults['randomAnswers'] = $this->getRandomAnswers();
2412
                $defaults['exerciseType'] = $this->selectType();
2413
                $defaults['exerciseTitle'] = $this->get_formated_title();
2414
                $defaults['exerciseDescription'] = $this->selectDescription();
2415
                $defaults['exerciseAttempts'] = $this->selectAttempts();
2416
                $defaults['exerciseFeedbackType'] = $this->getFeedbackType();
2417
                $defaults['results_disabled'] = $this->selectResultsDisabled();
2418
                $defaults['propagate_neg'] = $this->selectPropagateNeg();
2419
                $defaults['save_correct_answers'] = $this->getSaveCorrectAnswers();
2420
                $defaults['review_answers'] = $this->review_answers;
2421
                $defaults['randomByCat'] = $this->getRandomByCategory();
2422
                $defaults['text_when_finished'] = $this->getTextWhenFinished();
2423
                $defaults['text_when_finished_failure'] = $this->getTextWhenFinishedFailure();
2424
                $defaults['display_category_name'] = $this->selectDisplayCategoryName();
2425
                $defaults['pass_percentage'] = $this->selectPassPercentage();
2426
                $defaults['question_selection_type'] = $this->getQuestionSelectionType();
2427
                $defaults['hide_question_title'] = $this->getHideQuestionTitle();
2428
                $defaults['show_previous_button'] = $this->showPreviousButton();
2429
                $defaults['quiz_category_id'] = $this->getQuizCategoryId();
2430
                $defaults['prevent_backwards'] = $this->getPreventBackwards();
2431
                $defaults['hide_question_number'] = $this->getHideQuestionNumber();
2432
2433
                if (!empty($this->start_time)) {
2434
                    $defaults['activate_start_date_check'] = 1;
2435
                }
2436
                if (!empty($this->end_time)) {
2437
                    $defaults['activate_end_date_check'] = 1;
2438
                }
2439
2440
                $defaults['start_time'] = !empty($this->start_time) ? api_get_local_time($this->start_time) : date(
2441
                    'Y-m-d 12:00:00'
2442
                );
2443
                $defaults['end_time'] = !empty($this->end_time) ? api_get_local_time($this->end_time) : date(
2444
                    'Y-m-d 12:00:00',
2445
                    time() + 84600
2446
                );
2447
2448
                // Get expired time
2449
                if ('0' != $this->expired_time) {
2450
                    $defaults['enabletimercontrol'] = 1;
2451
                    $defaults['enabletimercontroltotalminutes'] = $this->expired_time;
2452
                } else {
2453
                    $defaults['enabletimercontroltotalminutes'] = 0;
2454
                }
2455
                $defaults['skills'] = array_keys($skillList);
2456
                $defaults['notifications'] = $this->getNotifications();
2457
            } else {
2458
                $defaults['exerciseType'] = 2;
2459
                $defaults['exerciseAttempts'] = 0;
2460
                $defaults['randomQuestions'] = 0;
2461
                $defaults['randomAnswers'] = 0;
2462
                $defaults['exerciseDescription'] = '';
2463
                $defaults['exerciseFeedbackType'] = 0;
2464
                $defaults['results_disabled'] = 0;
2465
                $defaults['randomByCat'] = 0;
2466
                $defaults['text_when_finished'] = '';
2467
                $defaults['text_when_finished_failure'] = '';
2468
                $defaults['start_time'] = date('Y-m-d 12:00:00');
2469
                $defaults['display_category_name'] = 1;
2470
                $defaults['end_time'] = date('Y-m-d 12:00:00', time() + 84600);
2471
                $defaults['pass_percentage'] = '';
2472
                $defaults['end_button'] = $this->selectEndButton();
2473
                $defaults['question_selection_type'] = 1;
2474
                $defaults['hide_question_title'] = 0;
2475
                $defaults['show_previous_button'] = 1;
2476
                $defaults['on_success_message'] = null;
2477
                $defaults['on_failed_message'] = null;
2478
            }
2479
        } else {
2480
            $defaults['exerciseTitle'] = $this->selectTitle();
2481
            $defaults['exerciseDescription'] = $this->selectDescription();
2482
        }
2483
2484
        if ('true' === api_get_setting('search_enabled')) {
2485
            $defaults['index_document'] = 'checked="checked"';
2486
        }
2487
2488
        $this->setPageResultConfigurationDefaults($defaults);
2489
        $form->setDefaults($defaults);
2490
2491
        // Freeze some elements.
2492
        if (0 != $this->getId() && false == $this->edit_exercise_in_lp) {
2493
            $elementsToFreeze = [
2494
                'randomQuestions',
2495
                //'randomByCat',
2496
                'exerciseAttempts',
2497
                'propagate_neg',
2498
                'enabletimercontrol',
2499
                'review_answers',
2500
            ];
2501
2502
            foreach ($elementsToFreeze as $elementName) {
2503
                /** @var HTML_QuickForm_element $element */
2504
                $element = $form->getElement($elementName);
2505
                $element->freeze();
2506
            }
2507
        }
2508
    }
2509
2510
    public function setResultFeedbackGroup(FormValidator $form, $checkFreeze = true)
2511
    {
2512
        // Feedback type.
2513
        $feedback = [];
2514
        $warning = sprintf(
2515
            get_lang("The setting \"%s\" will change to \"%s\""),
2516
            get_lang('Show score to learner'),
2517
            get_lang('Auto-evaluation mode: show score and expected answers')
2518
        );
2519
        $endTest = $form->createElement(
2520
            'radio',
2521
            'exerciseFeedbackType',
2522
            null,
2523
            get_lang('At end of test'),
2524
            EXERCISE_FEEDBACK_TYPE_END,
2525
            [
2526
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_END,
2527
                //'onclick' => 'if confirm() check_feedback()',
2528
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_feedback(); } else { return false;} ',
2529
            ]
2530
        );
2531
2532
        $noFeedBack = $form->createElement(
2533
            'radio',
2534
            'exerciseFeedbackType',
2535
            null,
2536
            get_lang('Exam (no feedback)'),
2537
            EXERCISE_FEEDBACK_TYPE_EXAM,
2538
            [
2539
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_EXAM,
2540
            ]
2541
        );
2542
2543
        $feedback[] = $endTest;
2544
        $feedback[] = $noFeedBack;
2545
2546
        $scenarioEnabled = 'true' === api_get_setting('enable_quiz_scenario');
2547
        $freeze = true;
2548
        if ($scenarioEnabled) {
2549
            if ($this->getQuestionCount() > 0) {
2550
                $hasDifferentQuestion = $this->hasQuestionWithTypeNotInList([UNIQUE_ANSWER, HOT_SPOT_DELINEATION]);
2551
                if (false === $hasDifferentQuestion) {
2552
                    $freeze = false;
2553
                }
2554
            } else {
2555
                $freeze = false;
2556
            }
2557
            // Can't convert a question from one feedback to another
2558
            $direct = $form->createElement(
2559
                'radio',
2560
                'exerciseFeedbackType',
2561
                null,
2562
                get_lang('Adaptative test with immediate feedback'),
2563
                EXERCISE_FEEDBACK_TYPE_DIRECT,
2564
                [
2565
                    'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_DIRECT,
2566
                    'onclick' => 'check_direct_feedback()',
2567
                ]
2568
            );
2569
2570
            $directPopUp = $form->createElement(
2571
                'radio',
2572
                'exerciseFeedbackType',
2573
                null,
2574
                get_lang('Direct pop-up mode'),
2575
                EXERCISE_FEEDBACK_TYPE_POPUP,
2576
                ['id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_POPUP, 'onclick' => 'check_direct_feedback()']
2577
            );
2578
            if ($freeze) {
2579
                $direct->freeze();
2580
                $directPopUp->freeze();
2581
            }
2582
2583
            // If has delineation freeze all.
2584
            $hasDelineation = $this->hasQuestionWithType(HOT_SPOT_DELINEATION);
2585
            if ($hasDelineation) {
2586
                $endTest->freeze();
2587
                $noFeedBack->freeze();
2588
                $direct->freeze();
2589
                $directPopUp->freeze();
2590
            }
2591
2592
            $feedback[] = $direct;
2593
            $feedback[] = $directPopUp;
2594
        }
2595
2596
        $form->addGroup(
2597
            $feedback,
2598
            null,
2599
            [
2600
                get_lang('Feedback'),
2601
                get_lang(
2602
                    '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.'
2603
                ),
2604
            ]
2605
        );
2606
    }
2607
2608
    /**
2609
     * function which process the creation of exercises.
2610
     *
2611
     * @param FormValidator $form
2612
     *
2613
     * @return int c_quiz.iid
2614
     */
2615
    public function processCreation($form)
2616
    {
2617
        $this->updateTitle(self::format_title_variable($form->getSubmitValue('exerciseTitle')));
2618
        $this->updateDescription($form->getSubmitValue('exerciseDescription'));
2619
        $this->updateAttempts($form->getSubmitValue('exerciseAttempts'));
2620
        $this->updateFeedbackType($form->getSubmitValue('exerciseFeedbackType'));
2621
        $this->updateType($form->getSubmitValue('exerciseType'));
2622
2623
        // If direct feedback then force to One per page
2624
        if (EXERCISE_FEEDBACK_TYPE_DIRECT == $form->getSubmitValue('exerciseFeedbackType')) {
2625
            $this->updateType(ONE_PER_PAGE);
2626
        }
2627
2628
        $this->setRandom($form->getSubmitValue('randomQuestions'));
2629
        $this->updateRandomAnswers($form->getSubmitValue('randomAnswers'));
2630
        $this->updateResultsDisabled($form->getSubmitValue('results_disabled'));
2631
        $this->updateExpiredTime($form->getSubmitValue('enabletimercontroltotalminutes'));
2632
        $this->updatePropagateNegative($form->getSubmitValue('propagate_neg'));
2633
        $this->updateSaveCorrectAnswers($form->getSubmitValue('save_correct_answers'));
2634
        $this->updateRandomByCat($form->getSubmitValue('randomByCat'));
2635
        $this->setTextWhenFinished($form->getSubmitValue('text_when_finished'));
2636
        $this->setTextWhenFinishedFailure($form->getSubmitValue('text_when_finished_failure'));
2637
        $this->updateDisplayCategoryName($form->getSubmitValue('display_category_name'));
2638
        $this->updateReviewAnswers($form->getSubmitValue('review_answers'));
2639
        $this->updatePassPercentage($form->getSubmitValue('pass_percentage'));
2640
        $this->updateCategories($form->getSubmitValue('category'));
2641
        $this->updateEndButton($form->getSubmitValue('end_button'));
2642
        $this->setOnSuccessMessage($form->getSubmitValue('on_success_message'));
2643
        $this->setOnFailedMessage($form->getSubmitValue('on_failed_message'));
2644
        $this->updateEmailNotificationTemplate($form->getSubmitValue('email_notification_template'));
2645
        $this->setEmailNotificationTemplateToUser($form->getSubmitValue('email_notification_template_to_user'));
2646
        $this->setNotifyUserByEmail($form->getSubmitValue('notify_user_by_email'));
2647
        $this->setModelType($form->getSubmitValue('model_type'));
2648
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2649
        $this->setHideQuestionTitle($form->getSubmitValue('hide_question_title'));
2650
        $this->sessionId = api_get_session_id();
2651
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2652
        $this->setScoreTypeModel($form->getSubmitValue('score_type_model'));
2653
        $this->setGlobalCategoryId($form->getSubmitValue('global_category_id'));
2654
        $this->setShowPreviousButton($form->getSubmitValue('show_previous_button'));
2655
        $this->setNotifications($form->getSubmitValue('notifications'));
2656
        $this->setQuizCategoryId($form->getSubmitValue('quiz_category_id'));
2657
        $this->setPageResultConfiguration($form->getSubmitValues());
2658
        $this->setHideQuestionNumber($form->getSubmitValue('hide_question_number'));
2659
        $this->preventBackwards = (int) $form->getSubmitValue('prevent_backwards');
2660
2661
        $this->start_time = null;
2662
        if (1 == $form->getSubmitValue('activate_start_date_check')) {
2663
            $start_time = $form->getSubmitValue('start_time');
2664
            $this->start_time = api_get_utc_datetime($start_time);
2665
        }
2666
2667
        $this->end_time = null;
2668
        if (1 == $form->getSubmitValue('activate_end_date_check')) {
2669
            $end_time = $form->getSubmitValue('end_time');
2670
            $this->end_time = api_get_utc_datetime($end_time);
2671
        }
2672
2673
        $this->expired_time = 0;
2674
        if (1 == $form->getSubmitValue('enabletimercontrol')) {
2675
            $expired_total_time = $form->getSubmitValue('enabletimercontroltotalminutes');
2676
            if (0 == $this->expired_time) {
2677
                $this->expired_time = $expired_total_time;
2678
            }
2679
        }
2680
2681
        $this->random_answers = 0;
2682
        if (1 == $form->getSubmitValue('randomAnswers')) {
2683
            $this->random_answers = 1;
2684
        }
2685
2686
        // Update title in all LPs that have this quiz added
2687
        if (1 == $form->getSubmitValue('update_title_in_lps')) {
2688
            $table = Database::get_course_table(TABLE_LP_ITEM);
2689
            $sql = "SELECT iid FROM $table
2690
                    WHERE
2691
                        item_type = 'quiz' AND
2692
                        path = '".$this->getId()."'
2693
                    ";
2694
            $result = Database::query($sql);
2695
            $items = Database::store_result($result);
2696
            if (!empty($items)) {
2697
                foreach ($items as $item) {
2698
                    $itemId = $item['iid'];
2699
                    $sql = "UPDATE $table
2700
                            SET title = '".$this->title."'
2701
                            WHERE iid = $itemId ";
2702
                    Database::query($sql);
2703
                }
2704
            }
2705
        }
2706
2707
        $iId = $this->save();
2708
        if (!empty($iId)) {
2709
            $values = $form->getSubmitValues();
2710
            $values['item_id'] = $iId;
2711
            $extraFieldValue = new ExtraFieldValue('exercise');
2712
            $extraFieldValue->saveFieldValues($values);
2713
2714
            SkillModel::saveSkills($form, ITEM_TYPE_EXERCISE, $iId);
2715
        }
2716
    }
2717
2718
    public function search_engine_save()
2719
    {
2720
        // Legacy search engine (v1) is disabled in C2.
2721
        return;
2722
    }
2723
2724
    public function search_engine_edit()
2725
    {
2726
        // Legacy search engine (v1) is disabled in C2.
2727
        return;
2728
    }
2729
2730
    public function search_engine_delete()
2731
    {
2732
        // Legacy search engine (v1) is disabled in C2.
2733
        return;
2734
    }
2735
2736
    public function selectExpiredTime()
2737
    {
2738
        return $this->expired_time;
2739
    }
2740
2741
    /**
2742
     * Cleans the student's results only for the Exercise tool (Not from the LP)
2743
     * The LP results are NOT deleted by default, otherwise put $cleanLpTests = true
2744
     * Works with exercises in sessions.
2745
     *
2746
     * @param bool   $cleanLpTests
2747
     * @param string $cleanResultBeforeDate
2748
     *
2749
     * @return int quantity of user's exercises deleted
2750
     */
2751
    public function cleanResults($cleanLpTests = false, $cleanResultBeforeDate = null)
2752
    {
2753
        $sessionId = api_get_session_id();
2754
        $table_track_e_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2755
        $table_track_e_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
2756
2757
        $sql_where = '  AND
2758
                        orig_lp_id = 0 AND
2759
                        orig_lp_item_id = 0';
2760
2761
        // if we want to delete results from LP too
2762
        if ($cleanLpTests) {
2763
            $sql_where = '';
2764
        }
2765
2766
        // if we want to delete attempts before date $cleanResultBeforeDate
2767
        // $cleanResultBeforeDate must be a valid UTC-0 date yyyy-mm-dd
2768
        if (!empty($cleanResultBeforeDate)) {
2769
            $cleanResultBeforeDate = Database::escape_string($cleanResultBeforeDate);
2770
            if (api_is_valid_date($cleanResultBeforeDate)) {
2771
                $sql_where .= "  AND exe_date <= '$cleanResultBeforeDate' ";
2772
            } else {
2773
                return 0;
2774
            }
2775
        }
2776
2777
        $sessionCondition = api_get_session_condition($sessionId);
2778
        $sql = "SELECT exe_id
2779
                FROM $table_track_e_exercises
2780
                WHERE
2781
                    c_id = ".api_get_course_int_id().' AND
2782
                    exe_exo_id = '.$this->getId()."
2783
                    $sessionCondition
2784
                    $sql_where";
2785
2786
        $result = Database::query($sql);
2787
        $exe_list = Database::store_result($result);
2788
2789
        // deleting TRACK_E_ATTEMPT table
2790
        // check if exe in learning path or not
2791
        $i = 0;
2792
        if (is_array($exe_list) && count($exe_list) > 0) {
2793
            foreach ($exe_list as $item) {
2794
                $sql = "DELETE FROM $table_track_e_attempt
2795
                        WHERE exe_id = '".$item['exe_id']."'";
2796
                Database::query($sql);
2797
                $i++;
2798
            }
2799
        }
2800
2801
        // delete TRACK_E_EXERCISES table
2802
        $sql = "DELETE FROM $table_track_e_exercises
2803
                WHERE
2804
                  c_id = ".api_get_course_int_id().' AND
2805
                  exe_exo_id = '.$this->getId()." $sql_where $sessionCondition";
2806
        Database::query($sql);
2807
2808
        $this->generateStats($this->getId(), api_get_course_info(), $sessionId);
2809
2810
        Event::addEvent(
2811
            LOG_EXERCISE_RESULT_DELETE,
2812
            LOG_EXERCISE_ID,
2813
            $this->getId(),
2814
            null,
2815
            null,
2816
            api_get_course_int_id(),
2817
            $sessionId
2818
        );
2819
2820
        return $i;
2821
    }
2822
2823
    /**
2824
     * Copies an exercise (duplicate all questions and answers).
2825
     */
2826
    public function copyExercise()
2827
    {
2828
        $exerciseObject = $this;
2829
        $categories = $exerciseObject->getCategoriesInExercise(true);
2830
        // Get all questions no matter the order/category settings
2831
        $questionList = $exerciseObject->getQuestionOrderedList();
2832
        $sourceId = $exerciseObject->iId;
2833
        // Force the creation of a new exercise
2834
        $exerciseObject->updateTitle($exerciseObject->selectTitle().' - '.get_lang('Copy'));
2835
        $exerciseObject->iId = 0;
2836
        $exerciseObject->sessionId = api_get_session_id();
2837
        $courseId = api_get_course_int_id();
2838
        $exerciseObject->save();
2839
        $newId = $exerciseObject->getId();
2840
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
2841
2842
        $count = 1;
2843
        $batchSize = 20;
2844
        $em = Database::getManager();
2845
        if ($newId && !empty($questionList)) {
2846
            $extraField = new ExtraFieldValue('exercise');
2847
            $extraField->copy($sourceId, $newId);
2848
            // Question creation
2849
            foreach ($questionList as $oldQuestionId) {
2850
                $oldQuestionObj = Question::read($oldQuestionId, null, false);
2851
                $newQuestionId = $oldQuestionObj->duplicate();
2852
                if ($newQuestionId) {
2853
                    $newQuestionObj = Question::read($newQuestionId, null, false);
2854
                    if (isset($newQuestionObj) && $newQuestionObj) {
2855
                        $sql = "INSERT INTO $exerciseRelQuestionTable (question_id, quiz_id, question_order)
2856
                                VALUES (".$newQuestionId.", ".$newId.", '$count')";
2857
                        Database::query($sql);
2858
                        $count++;
2859
                        if (!empty($oldQuestionObj->category)) {
2860
                            $newQuestionObj->saveCategory($oldQuestionObj->category);
2861
                        }
2862
2863
                        // This should be moved to the duplicate function
2864
                        $newAnswerObj = new Answer($oldQuestionId, $courseId, $exerciseObject);
2865
                        $newAnswerObj->read();
2866
                        $newAnswerObj->duplicate($newQuestionObj);
2867
                        if (($count % $batchSize) === 0) {
2868
                            $em->clear(); // Detaches all objects from Doctrine!
2869
                        }
2870
                    }
2871
                }
2872
            }
2873
            if (!empty($categories)) {
2874
                $newCategoryList = [];
2875
                foreach ($categories as $category) {
2876
                    $newCategoryList[$category['category_id']] = $category['count_questions'];
2877
                }
2878
                $exerciseObject->saveCategoriesInExercise($newCategoryList);
2879
            }
2880
        }
2881
    }
2882
2883
    /**
2884
     * Changes the exercise status.
2885
     *
2886
     * @param string $status - exercise status
2887
     */
2888
    public function updateStatus($status)
2889
    {
2890
        $this->active = $status;
2891
    }
2892
2893
    /**
2894
     * @param int    $lp_id
2895
     * @param int    $lp_item_id
2896
     * @param int    $lp_item_view_id
2897
     * @param string $status
2898
     *
2899
     * @return array
2900
     */
2901
    public function get_stat_track_exercise_info(
2902
        $lp_id = 0,
2903
        $lp_item_id = 0,
2904
        $lp_item_view_id = 0,
2905
        $status = 'incomplete'
2906
    ) {
2907
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2908
        $lp_id = (int) $lp_id;
2909
        $lp_item_id = (int) $lp_item_id;
2910
        $lp_item_view_id = (int) $lp_item_view_id;
2911
2912
        $sessionCondition = api_get_session_condition(api_get_session_id());
2913
        $condition = " WHERE exe_exo_id 	= ".$this->getId()." AND
2914
					   exe_user_id 			= '".api_get_user_id()."' AND
2915
					   c_id                 = ".api_get_course_int_id()." AND
2916
					   status 				= '".Database::escape_string($status)."' AND
2917
					   orig_lp_id 			= $lp_id AND
2918
					   orig_lp_item_id 		= $lp_item_id AND
2919
                       orig_lp_item_view_id =  $lp_item_view_id
2920
					   ";
2921
2922
        $sql_track = " SELECT * FROM  $track_exercises $condition $sessionCondition LIMIT 1 ";
2923
2924
        $result = Database::query($sql_track);
2925
        $new_array = [];
2926
        if (Database::num_rows($result) > 0) {
2927
            $new_array = Database::fetch_assoc($result);
2928
            $new_array['num_exe'] = Database::num_rows($result);
2929
        }
2930
2931
        return $new_array;
2932
    }
2933
2934
    /**
2935
     * Saves a test attempt.
2936
     *
2937
     * @param int   $clock_expired_time   clock_expired_time
2938
     * @param int   $safe_lp_id           lp id
2939
     * @param int   $safe_lp_item_id      lp item id
2940
     * @param int   $safe_lp_item_view_id lp item_view id
2941
     * @param array $questionList
2942
     * @param float $weight
2943
     *
2944
     * @throws \Doctrine\ORM\ORMException
2945
     * @throws \Doctrine\ORM\OptimisticLockException
2946
     * @throws \Doctrine\ORM\TransactionRequiredException
2947
     *
2948
     * @return int
2949
     */
2950
    public function save_stat_track_exercise_info(
2951
        $clock_expired_time,
2952
        $safe_lp_id = 0,
2953
        $safe_lp_item_id = 0,
2954
        $safe_lp_item_view_id = 0,
2955
        $questionList = [],
2956
        $weight = 0
2957
    ) {
2958
        $safe_lp_id = (int) $safe_lp_id;
2959
        $safe_lp_item_id = (int) $safe_lp_item_id;
2960
        $safe_lp_item_view_id = (int) $safe_lp_item_view_id;
2961
2962
        if (empty($clock_expired_time)) {
2963
            $clock_expired_time = null;
2964
        }
2965
2966
        $questionList = array_filter(
2967
            $questionList,
2968
            function (int $qid) {
2969
                $q = Question::read($qid);
2970
                return $q
2971
                    && !in_array(
2972
                        $q->type,
2973
                        [PAGE_BREAK, MEDIA_QUESTION],
2974
                        true
2975
                    );
2976
            }
2977
        );
2978
2979
        $questionList = array_map('intval', $questionList);
2980
        $em = Database::getManager();
2981
2982
        $quiz = $em->find(CQuiz::class, $this->getId());
2983
2984
        $trackExercise = (new TrackEExercise())
2985
            ->setSession(api_get_session_entity())
2986
            ->setCourse(api_get_course_entity())
2987
            ->setMaxScore($weight)
2988
            ->setDataTracking(implode(',', $questionList))
2989
            ->setUser(api_get_user_entity())
2990
            ->setUserIp(api_get_real_ip())
2991
            ->setOrigLpId($safe_lp_id)
2992
            ->setOrigLpItemId($safe_lp_item_id)
2993
            ->setOrigLpItemViewId($safe_lp_item_view_id)
2994
            ->setExpiredTimeControl($clock_expired_time)
2995
            ->setQuiz($quiz)
2996
        ;
2997
        $em->persist($trackExercise);
2998
        $em->flush();
2999
3000
        return $trackExercise->getExeId();
3001
    }
3002
3003
    /**
3004
     * @param int    $question_id
3005
     * @param int    $questionNum
3006
     * @param array  $questions_in_media
3007
     * @param string $currentAnswer
3008
     * @param array  $myRemindList
3009
     * @param bool   $showPreviousButton
3010
     *
3011
     * @return string
3012
     */
3013
    public function show_button(
3014
        $question_id,
3015
        $questionNum,
3016
        $questions_in_media = [],
3017
        $currentAnswer = '',
3018
        $myRemindList = [],
3019
        $showPreviousButton = true
3020
    ) {
3021
        global $safe_lp_id, $safe_lp_item_id, $safe_lp_item_view_id;
3022
        $nbrQuestions = $this->countQuestionsInExercise();
3023
        $buttonList = [];
3024
        $html = $label = '';
3025
        $hotspotGet = isset($_POST['hotspot']) ? Security::remove_XSS($_POST['hotspot']) : null;
3026
3027
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]) &&
3028
            ONE_PER_PAGE == $this->type
3029
        ) {
3030
            $urlTitle = get_lang('Proceed with the test');
3031
            if ($questionNum == count($this->questionList)) {
3032
                $urlTitle = get_lang('End test');
3033
            }
3034
3035
            $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit_modal.php?'.api_get_cidreq();
3036
            $url .= '&'.http_build_query(
3037
                    [
3038
                        'learnpath_id' => $safe_lp_id,
3039
                        'learnpath_item_id' => $safe_lp_item_id,
3040
                        'learnpath_item_view_id' => $safe_lp_item_view_id,
3041
                        'hotspot' => $hotspotGet,
3042
                        'nbrQuestions' => $nbrQuestions,
3043
                        'num' => $questionNum,
3044
                        'exerciseType' => $this->type,
3045
                        'exerciseId' => $this->getId(),
3046
                        'reminder' => empty($myRemindList) ? null : 2,
3047
                        'tryagain' => isset($_REQUEST['tryagain']) && 1 === (int) $_REQUEST['tryagain'] ? 1 : 0,
3048
                    ]
3049
                );
3050
3051
            $params = [
3052
                'class' => 'ajax btn btn--plain no-close-button',
3053
                'data-title' => Security::remove_XSS(get_lang('Comment')),
3054
                'data-size' => 'md',
3055
                'id' => "button_$question_id",
3056
            ];
3057
3058
            if (EXERCISE_FEEDBACK_TYPE_POPUP === $this->getFeedbackType()) {
3059
                //$params['data-block-div-after-closing'] = "question_div_$question_id";
3060
                $params['data-block-closing'] = 'true';
3061
                $params['class'] .= ' no-header ';
3062
            }
3063
3064
            $html .= Display::url($urlTitle, $url, $params);
3065
            $html .= '<br />';
3066
3067
            return $html;
3068
        }
3069
3070
        if (!api_is_allowed_to_session_edit()) {
3071
            return '';
3072
        }
3073
3074
        $isReviewingAnswers = isset($_REQUEST['reminder']) && 2 == $_REQUEST['reminder'];
3075
3076
        // User
3077
        $endReminderValue = false;
3078
        if (!empty($myRemindList) && $isReviewingAnswers) {
3079
            $endValue = end($myRemindList);
3080
            if ($endValue == $question_id) {
3081
                $endReminderValue = true;
3082
            }
3083
        }
3084
        $endTest = false;
3085
        if (ALL_ON_ONE_PAGE == $this->type || $nbrQuestions == $questionNum || $endReminderValue) {
3086
            if ($this->review_answers) {
3087
                $label = get_lang('Review selected questions');
3088
                $class = 'btn btn--success';
3089
            } else {
3090
                $endTest = true;
3091
                $label = get_lang('End test');
3092
                $class = 'btn btn--warning';
3093
            }
3094
        } else {
3095
            $label = get_lang('Next question');
3096
            $class = 'btn btn--primary';
3097
        }
3098
        // used to select it with jquery
3099
        $class .= ' question-validate-btn';
3100
        if (ONE_PER_PAGE == $this->type) {
3101
            if (1 != $questionNum && $this->showPreviousButton()) {
3102
                $prev_question = $questionNum - 2;
3103
                $showPreview = true;
3104
                if (!empty($myRemindList) && $isReviewingAnswers) {
3105
                    $beforeId = null;
3106
                    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...
3107
                        if (isset($myRemindList[$i]) && $myRemindList[$i] == $question_id) {
3108
                            $beforeId = isset($myRemindList[$i - 1]) ? $myRemindList[$i - 1] : null;
3109
3110
                            break;
3111
                        }
3112
                    }
3113
3114
                    if (empty($beforeId)) {
3115
                        $showPreview = false;
3116
                    } else {
3117
                        $num = 0;
3118
                        foreach ($this->questionList as $originalQuestionId) {
3119
                            if ($originalQuestionId == $beforeId) {
3120
                                break;
3121
                            }
3122
                            $num++;
3123
                        }
3124
                        $prev_question = $num;
3125
                    }
3126
                }
3127
3128
                if ($showPreviousButton && $showPreview && 0 === $this->getPreventBackwards()) {
3129
                    $buttonList[] = Display::button(
3130
                        'previous_question_and_save',
3131
                        get_lang('Previous question'),
3132
                        [
3133
                            'type' => 'button',
3134
                            'class' => 'btn btn--plain',
3135
                            'data-prev' => $prev_question,
3136
                            'data-question' => $question_id,
3137
                        ]
3138
                    );
3139
                }
3140
            }
3141
3142
            // Next question
3143
            if (!empty($questions_in_media)) {
3144
                $buttonList[] = Display::button(
3145
                    'save_question_list',
3146
                    $label,
3147
                    [
3148
                        'type' => 'button',
3149
                        'class' => $class,
3150
                        'data-list' => implode(',', $questions_in_media),
3151
                    ]
3152
                );
3153
            } else {
3154
                $attributes = ['type' => 'button', 'class' => $class, 'data-question' => $question_id];
3155
                $name = 'save_now';
3156
                if ($endTest && api_get_configuration_value('quiz_check_all_answers_before_end_test')) {
3157
                    $name = 'check_answers';
3158
                }
3159
                $buttonList[] = Display::button(
3160
                    $name,
3161
                    $label,
3162
                    $attributes
3163
                );
3164
            }
3165
            $buttonList[] = '<span id="save_for_now_'.$question_id.'" class="exercise_save_mini_message"></span>';
3166
3167
            $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3168
3169
            return $html;
3170
        }
3171
3172
        if ($this->review_answers) {
3173
            $all_label = get_lang('Review selected questions');
3174
            $class = 'btn btn--success';
3175
        } else {
3176
            $all_label = get_lang('End test');
3177
            $class = 'btn btn--warning';
3178
        }
3179
        // used to select it with jquery
3180
        $class .= ' question-validate-btn';
3181
        $buttonList[] = Display::button(
3182
            'validate_all',
3183
            $all_label,
3184
            ['type' => 'button', 'class' => $class]
3185
        );
3186
        $buttonList[] = Display::span(null, ['id' => 'save_all_response']);
3187
        $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3188
3189
        return $html;
3190
    }
3191
3192
    /**
3193
     * @param int    $timeLeft in seconds
3194
     * @param string $url
3195
     *
3196
     * @return string
3197
     */
3198
    public function showSimpleTimeControl($timeLeft, $url = '')
3199
    {
3200
        $timeLeft = (int) $timeLeft;
3201
3202
        return "<script>
3203
        (function ensureModalStyles() {
3204
            if (document.getElementById('ch-modal-styles')) return;
3205
            var css = `
3206
#clock_warning { display: none; }
3207
.ch-modal { position: fixed; inset: 0; z-index: 9999; display: grid; place-items: center; padding: 16px; }
3208
.ch-modal::before { content: ''; position: absolute; inset: 0; background: rgba(0,0,0,.3); }
3209
.ch-panel { position: relative; width: 100%; max-width: 520px; background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,.08); padding: 16px; color:#111827; font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif; }
3210
.ch-close { position:absolute; right: 10px; top:10px; cursor:pointer; color:#6b7280; }
3211
            `;
3212
            var s = document.createElement('style');
3213
            s.id = 'ch-modal-styles';
3214
            s.textContent = css;
3215
            document.head.appendChild(s);
3216
        })();
3217
3218
        function wrapClockWarning() {
3219
            var \$cw = \$('#clock_warning');
3220
            if (!\$cw.length) return \$cw;
3221
            // turn container into modal
3222
            \$cw.addClass('ch-modal');
3223
            // ensure inner panel
3224
            if (!\$cw.find('.ch-panel').length) {
3225
                \$cw.html('<div class=\"ch-panel\"><span class=\"mdi mdi-close ch-close\" title=\"Close\" aria-label=\"Close\"></span>' + \$cw.html() + '</div>');
3226
            }
3227
            // close button
3228
            \$cw.on('click', '.ch-close', function(){ closeClockWarning(); });
3229
            return \$cw;
3230
        }
3231
3232
        function openClockWarning() {
3233
            var \$cw = wrapClockWarning();
3234
            if (!\$cw || !\$cw.length) return;
3235
            \$cw.show();
3236
3237
            // countdown 5s -> redirect
3238
            \$('#counter_to_redirect').epiclock({
3239
                mode: \$.epiclock.modes.countdown,
3240
                offset: {seconds: 5},
3241
                format: 's'
3242
            }).bind('timer', function () {
3243
                window.location.href = '$url';
3244
            });
3245
        }
3246
3247
        function closeClockWarning() {
3248
            \$('#clock_warning').hide();
3249
            window.location.href = '$url';
3250
        }
3251
3252
        function onExpiredTimeExercise() {
3253
            \$('#wrapper-clock').hide();
3254
            \$('#expired-message-id').show();
3255
            \$('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3256
            openClockWarning();
3257
        }
3258
3259
        \$(function() {
3260
            var time_left = parseInt(".$timeLeft.");
3261
            \$('#exercise_clock_warning').epiclock({
3262
                mode: \$.epiclock.modes.countdown,
3263
                offset: {seconds: time_left},
3264
                format: 'x:i:s',
3265
                renderer: 'minute'
3266
            }).bind('timer', function () {
3267
                onExpiredTimeExercise();
3268
            });
3269
            \$('#submit_save').click(function () {});
3270
        });
3271
    </script>";
3272
    }
3273
3274
    /**
3275
     * So the time control will work.
3276
     *
3277
     * @param int    $timeLeft
3278
     * @param string $redirectToUrl
3279
     *
3280
     * @return string
3281
     */
3282
    public function showTimeControlJS($timeLeft, $redirectToUrl = '')
3283
    {
3284
        $timeLeft = (int) $timeLeft;
3285
        $script = 'redirectExerciseToResult();';
3286
        if (ALL_ON_ONE_PAGE == $this->type) {
3287
            $script = "save_now_all('validate');";
3288
        } elseif (ONE_PER_PAGE == $this->type) {
3289
            $script = 'window.quizTimeEnding = true; $(\'[name=\"save_now\"]\').trigger(\'click\');';
3290
        }
3291
3292
        $exerciseSubmitRedirect = '';
3293
        if (!empty($redirectToUrl)) {
3294
            $exerciseSubmitRedirect = "window.location = '$redirectToUrl'";
3295
        }
3296
3297
        return "<script>
3298
        (function ensureModalStyles() {
3299
            if (document.getElementById('ch-modal-styles')) return;
3300
            var css = `
3301
#clock_warning { display: none; }
3302
.ch-modal { position: fixed; inset: 0; z-index: 9999; display: grid; place-items: center; padding: 16px; }
3303
.ch-modal::before { content: ''; position: absolute; inset: 0; background: rgba(0,0,0,.3); }
3304
.ch-panel { position: relative; width: 100%; max-width: 520px; background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,.08); padding: 16px; color:#111827; font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif; }
3305
.ch-close { position:absolute; right: 10px; top:10px; cursor:pointer; color:#6b7280; }
3306
            `;
3307
            var s = document.createElement('style');
3308
            s.id = 'ch-modal-styles';
3309
            s.textContent = css;
3310
            document.head.appendChild(s);
3311
        })();
3312
3313
        function wrapClockWarning() {
3314
            var \$cw = \$('#clock_warning');
3315
            if (!\$cw.length) return \$cw;
3316
            \$cw.addClass('ch-modal');
3317
            if (!\$cw.find('.ch-panel').length) {
3318
                \$cw.html('<div class=\"ch-panel\"><span class=\"mdi mdi-close ch-close\" title=\"Close\" aria-label=\"Close\"></span>' + \$cw.html() + '</div>');
3319
            }
3320
            \$cw.on('click', '.ch-close', function(){ closeClockWarning(); });
3321
            return \$cw;
3322
        }
3323
3324
        function openClockWarning() {
3325
            var \$cw = wrapClockWarning();
3326
            if (!\$cw || !\$cw.length) return;
3327
            \$cw.show();
3328
3329
            \$('#counter_to_redirect').epiclock({
3330
                mode: \$.epiclock.modes.countdown,
3331
                offset: {seconds: 5},
3332
                format: 's'
3333
            }).bind('timer', function () {
3334
                send_form();
3335
            });
3336
        }
3337
3338
        function closeClockWarning() {
3339
            \$('#clock_warning').hide();
3340
            send_form();
3341
        }
3342
3343
        function send_form() {
3344
            if (\$('#exercise_form').length) {
3345
                $script
3346
            } else {
3347
                $exerciseSubmitRedirect
3348
                // In exercise_reminder.php
3349
                final_submit();
3350
            }
3351
        }
3352
3353
        function onExpiredTimeExercise() {
3354
            \$('#wrapper-clock').hide();
3355
            \$('#expired-message-id').show();
3356
            \$('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3357
            openClockWarning();
3358
        }
3359
3360
        \$(function() {
3361
            var time_left = parseInt(".$timeLeft.");
3362
            \$('#exercise_clock_warning').epiclock({
3363
                mode: \$.epiclock.modes.countdown,
3364
                offset: {seconds: time_left},
3365
                format: 'x:C:s',
3366
                renderer: 'minute'
3367
            }).bind('timer', function () {
3368
                onExpiredTimeExercise();
3369
            });
3370
            \$('#submit_save').click(function () {});
3371
        });
3372
    </script>";
3373
    }
3374
3375
    /**
3376
     * This function was originally found in the exercise_show.php.
3377
     *
3378
     * @param int    $exeId
3379
     * @param int    $questionId
3380
     * @param mixed  $choice                                    the user-selected option
3381
     * @param string $from                                      function is called from 'exercise_show' or
3382
     *                                                          'exercise_result'
3383
     * @param array  $exerciseResultCoordinates                 the hotspot coordinates $hotspot[$question_id] =
3384
     *                                                          coordinates
3385
     * @param bool   $save_results                              save results in the DB or just show the response
3386
     * @param bool   $from_database                             gets information from DB or from the current selection
3387
     * @param bool   $show_result                               show results or not
3388
     * @param int    $propagate_neg
3389
     * @param array  $hotspot_delineation_result
3390
     * @param bool   $showTotalScoreAndUserChoicesInLastAttempt
3391
     * @param bool   $updateResults
3392
     * @param bool   $showHotSpotDelineationTable
3393
     * @param int    $questionDuration                          seconds
3394
     *
3395
     * @return string html code
3396
     *
3397
     * @todo    reduce parameters of this function
3398
     */
3399
    public function manage_answer(
3400
        $exeId,
3401
        $questionId,
3402
        $choice,
3403
        $from = 'exercise_show',
3404
        $exerciseResultCoordinates = [],
3405
        $save_results = true,
3406
        $from_database = false,
3407
        $show_result = true,
3408
        $propagate_neg = 0,
3409
        $hotspot_delineation_result = [],
3410
        $showTotalScoreAndUserChoicesInLastAttempt = true,
3411
        $updateResults = false,
3412
        $showHotSpotDelineationTable = false,
3413
        $questionDuration = 0
3414
    ) {
3415
        $debug = false;
3416
        //needed in order to use in the exercise_attempt() for the time
3417
        global $learnpath_id, $learnpath_item_id;
3418
        require_once api_get_path(LIBRARY_PATH).'geometry.lib.php';
3419
        $em = Database::getManager();
3420
        $feedback_type = $this->getFeedbackType();
3421
        $results_disabled = $this->selectResultsDisabled();
3422
        $questionDuration = (int) $questionDuration;
3423
        $hotspotsSavedEarly = false;
3424
3425
        if ($debug) {
3426
            error_log('<------ manage_answer ------> ');
3427
            error_log('exe_id: '.$exeId);
3428
            error_log('$from:  '.$from);
3429
            error_log('$save_results: '.(int) $save_results);
3430
            error_log('$from_database: '.(int) $from_database);
3431
            error_log('$show_result: '.(int) $show_result);
3432
            error_log('$propagate_neg: '.$propagate_neg);
3433
            error_log('$exerciseResultCoordinates: '.print_r($exerciseResultCoordinates, 1));
3434
            error_log('$hotspot_delineation_result: '.print_r($hotspot_delineation_result, 1));
3435
            error_log('$learnpath_id: '.$learnpath_id);
3436
            error_log('$learnpath_item_id: '.$learnpath_item_id);
3437
            error_log('$choice: '.print_r($choice, 1));
3438
            error_log('-----------------------------');
3439
        }
3440
3441
        $final_overlap = 0;
3442
        $final_missing = 0;
3443
        $final_excess = 0;
3444
        $overlap_color = 0;
3445
        $missing_color = 0;
3446
        $excess_color = 0;
3447
        $threadhold1 = 0;
3448
        $threadhold2 = 0;
3449
        $threadhold3 = 0;
3450
        $arrques = null;
3451
        $arrans = null;
3452
        $studentChoice = null;
3453
        $expectedAnswer = '';
3454
        $calculatedChoice = '';
3455
        $calculatedStatus = '';
3456
        $questionId = (int) $questionId;
3457
        $exeId = (int) $exeId;
3458
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3459
        $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
3460
        $studentChoiceDegree = null;
3461
        $listCorrectAnswers = [];
3462
3463
        // Creates a temporary Question object
3464
        $course_id = $this->course_id;
3465
        $objQuestionTmp = Question::read($questionId, $this->course);
3466
3467
        if (false === $objQuestionTmp) {
3468
            return false;
3469
        }
3470
3471
        $questionName = $objQuestionTmp->selectTitle();
3472
        $questionWeighting = $objQuestionTmp->selectWeighting();
3473
        $answerType = $objQuestionTmp->selectType();
3474
        $quesId = $objQuestionTmp->getId();
3475
        $extra = $objQuestionTmp->extra;
3476
        $next = 1; //not for now
3477
        $totalWeighting = 0;
3478
        $totalScore = 0;
3479
3480
        // Extra information of the question
3481
        if ((
3482
                MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
3483
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
3484
            )
3485
            && !empty($extra)
3486
        ) {
3487
            $extra = explode(':', $extra);
3488
            // Fixes problems with negatives values using intval
3489
            $true_score = (float) trim($extra[0]);
3490
            $false_score = (float) trim($extra[1]);
3491
            $doubt_score = (float) trim($extra[2]);
3492
        }
3493
3494
        // Construction of the Answer object
3495
        $objAnswerTmp = new Answer($questionId, $course_id);
3496
        $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
3497
3498
        if ($debug) {
3499
            error_log('Count of possible answers: '.$nbrAnswers);
3500
            error_log('$answerType: '.$answerType);
3501
        }
3502
3503
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
3504
            $choiceTmp = $choice;
3505
            $choice = isset($choiceTmp['choice']) ? $choiceTmp['choice'] : '';
3506
            $choiceDegreeCertainty = isset($choiceTmp['choiceDegreeCertainty']) ? $choiceTmp['choiceDegreeCertainty'] : '';
3507
        }
3508
3509
        if (FREE_ANSWER == $answerType ||
3510
            ORAL_EXPRESSION == $answerType ||
3511
            CALCULATED_ANSWER == $answerType ||
3512
            ANNOTATION == $answerType ||
3513
            UPLOAD_ANSWER == $answerType
3514
        ) {
3515
            $nbrAnswers = 1;
3516
        }
3517
3518
        $generatedFilesHtml = '';
3519
        if ($answerType == ORAL_EXPRESSION) {
3520
            $generatedFilesHtml = ExerciseLib::getOralFileAudio($exeId, $questionId);
3521
        }
3522
3523
        $user_answer = '';
3524
        // Get answer list for matching
3525
        $sql = "SELECT iid, answer
3526
                FROM $table_ans
3527
                WHERE question_id = $questionId";
3528
        $res_answer = Database::query($sql);
3529
3530
        $answerMatching = [];
3531
        while ($real_answer = Database::fetch_array($res_answer)) {
3532
            $answerMatching[$real_answer['iid']] = $real_answer['answer'];
3533
        }
3534
3535
        // Get first answer needed for global question, no matter the answer shuffle option;
3536
        $firstAnswer = [];
3537
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
3538
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
3539
        ) {
3540
            $sql = "SELECT *
3541
                    FROM $table_ans
3542
                    WHERE question_id = $questionId
3543
                    ORDER BY position
3544
                    LIMIT 1";
3545
            $result = Database::query($sql);
3546
            if (Database::num_rows($result)) {
3547
                $firstAnswer = Database::fetch_array($result);
3548
            }
3549
        }
3550
3551
        $real_answers = [];
3552
        $quiz_question_options = Question::readQuestionOption($questionId, $course_id);
3553
3554
        $organs_at_risk_hit = 0;
3555
        $questionScore = 0;
3556
        $orderedHotSpots = [];
3557
        if (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION, ANNOTATION], true)) {
3558
            $orderedHotSpots = $em->getRepository(TrackEHotspot::class)->findBy(
3559
                [
3560
                    'hotspotQuestionId' => $questionId,
3561
                    'course' => $course_id,
3562
                    'hotspotExeId' => $exeId,
3563
                ],
3564
                ['hotspotAnswerId' => 'ASC']
3565
            );
3566
        }
3567
3568
        if (in_array($answerType, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION], true)) {
3569
            if (MULTIPLE_ANSWER_DROPDOWN_COMBINATION == $answerType) {
3570
                $questionScore = $questionWeighting;
3571
            }
3572
            // Load student's selected choices
3573
            if ($from_database) {
3574
                $studentChoices = Database::store_result(
3575
                    Database::query(
3576
                        "SELECT answer FROM $TBL_TRACK_ATTEMPT WHERE exe_id = $exeId AND question_id = $questionId"
3577
                    ),
3578
                    'ASSOC'
3579
                );
3580
                $studentChoices = array_column($studentChoices, 'answer');
3581
            } else {
3582
                $studentChoices = array_values((array) $choice);
3583
            }
3584
3585
            // Compute the list of correct choice IDs
3586
            $correctChoices = array_keys(array_filter(
3587
                $answerMatching, // iid => answer text
3588
                function ($answerIdKey) use ($objAnswerTmp) {
3589
                    // Safeguards in case arrays are not initialized
3590
                    $iidList = is_array($objAnswerTmp->iid) ? $objAnswerTmp->iid : [];
3591
                    $pos = array_search($answerIdKey, $iidList, true);
3592
                    return $pos !== false && !empty($objAnswerTmp->correct[$pos]);
3593
                },
3594
                ARRAY_FILTER_USE_KEY
3595
            ));
3596
3597
            // Scoring
3598
            if (MULTIPLE_ANSWER_DROPDOWN_COMBINATION === $answerType) {
3599
                // All-or-nothing scoring
3600
                $questionScore = (float) $questionWeighting;
3601
                if (array_diff($studentChoices, $correctChoices) || array_diff($correctChoices, $studentChoices)) {
3602
                    $questionScore = 0.0;
3603
                }
3604
            } else { // MULTIPLE_ANSWER_DROPDOWN
3605
                // Partial scoring using per-choice weighting
3606
                $questionScore = 0.0;
3607
                $iidList    = is_array($objAnswerTmp->iid) ? $objAnswerTmp->iid : [];
3608
                $correctArr = is_array($objAnswerTmp->correct) ? $objAnswerTmp->correct : [];
3609
                $weights    = is_array($objAnswerTmp->weighting) ? $objAnswerTmp->weighting : [];
3610
3611
                foreach ((array) $studentChoices as $choiceId) {
3612
                    $pos = array_search($choiceId, $iidList, true);
3613
                    if ($pos !== false && !empty($correctArr[$pos])) {
3614
                        $questionScore += (float) ($weights[$pos] ?? 0);
3615
                    }
3616
                }
3617
            }
3618
3619
            // Render (UI)
3620
            if ($show_result) {
3621
                echo ExerciseShowFunctions::displayMultipleAnswerDropdown(
3622
                    $this,
3623
                    $objAnswerTmp,
3624
                    $correctChoices,
3625
                    $studentChoices,
3626
                    $showTotalScoreAndUserChoicesInLastAttempt
3627
                );
3628
            }
3629
        }
3630
3631
        if ($debug) {
3632
            error_log('-- Start answer loop --');
3633
        }
3634
3635
        $answerDestination = null;
3636
        $userAnsweredQuestion = false;
3637
        $correctAnswerId = [];
3638
        $matchingCorrectAnswers = [];
3639
        for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
3640
            $answer = $objAnswerTmp->selectAnswer($answerId);
3641
            $answerComment = $objAnswerTmp->selectComment($answerId);
3642
            $answerCorrect = $objAnswerTmp->isCorrect($answerId);
3643
            $answerWeighting = (float) $objAnswerTmp->selectWeighting($answerId);
3644
            $answerAutoId = $objAnswerTmp->selectAutoId($answerId);
3645
            $answerIid = isset($objAnswerTmp->iid[$answerId]) ? (int) $objAnswerTmp->iid[$answerId] : 0;
3646
3647
            if ($debug) {
3648
                error_log("c_quiz_answer.id_auto: $answerAutoId ");
3649
                error_log("Answer marked as correct in db (0/1)?: $answerCorrect ");
3650
                error_log("answerWeighting: $answerWeighting");
3651
            }
3652
3653
            // Delineation
3654
            $delineation_cord = $objAnswerTmp->selectHotspotCoordinates(1);
3655
            $answer_delineation_destination = $objAnswerTmp->selectDestination(1);
3656
3657
            switch ($answerType) {
3658
                case UNIQUE_ANSWER:
3659
                case UNIQUE_ANSWER_IMAGE:
3660
                case UNIQUE_ANSWER_NO_OPTION:
3661
                case READING_COMPREHENSION:
3662
                    if ($from_database) {
3663
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3664
                                WHERE
3665
                                    exe_id = $exeId AND
3666
                                    question_id = $questionId";
3667
                        $result = Database::query($sql);
3668
                        $choice = Database::result($result, 0, 'answer');
3669
3670
                        if (false === $userAnsweredQuestion) {
3671
                            $userAnsweredQuestion = !empty($choice);
3672
                        }
3673
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3674
                        if ($studentChoice) {
3675
                            $questionScore += $answerWeighting;
3676
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3677
                            $correctAnswerId[] = $answerId;
3678
                        }
3679
                    } else {
3680
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3681
                        if ($studentChoice) {
3682
                            $questionScore += $answerWeighting;
3683
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3684
                            $correctAnswerId[] = $answerId;
3685
                        }
3686
                    }
3687
3688
                    break;
3689
                case MULTIPLE_ANSWER_TRUE_FALSE:
3690
                    if ($from_database) {
3691
                        $choice = [];
3692
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3693
                                WHERE
3694
                                    exe_id = $exeId AND
3695
                                    question_id = ".$questionId;
3696
3697
                        $result = Database::query($sql);
3698
                        while ($row = Database::fetch_array($result)) {
3699
                            $values = explode(':', $row['answer']);
3700
                            $my_answer_id = isset($values[0]) ? $values[0] : '';
3701
                            $option = isset($values[1]) ? $values[1] : '';
3702
                            $choice[$my_answer_id] = $option;
3703
                        }
3704
                        $userAnsweredQuestion = !empty($choice);
3705
                    }
3706
3707
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3708
                    if (!empty($studentChoice)) {
3709
                        $correctAnswerId[] = $answerAutoId;
3710
                        if ($studentChoice == $answerCorrect) {
3711
                            $questionScore += $true_score;
3712
                        } else {
3713
                            if (isset($quiz_question_options[$studentChoice])
3714
                                && in_array($quiz_question_options[$studentChoice]['name'], ["Don't know", 'DoubtScore'])
3715
                            ) {
3716
                                $questionScore += $doubt_score;
3717
                            } else {
3718
                                $questionScore += $false_score;
3719
                            }
3720
                        }
3721
                    } else {
3722
                        // If no result then the user just hit don't know
3723
                        $studentChoice = 3;
3724
                        $questionScore += $doubt_score;
3725
                    }
3726
                    $totalScore = $questionScore;
3727
3728
                    break;
3729
                case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
3730
                    if ($from_database) {
3731
                        $choice = [];
3732
                        $choiceDegreeCertainty = [];
3733
                        $sql = "SELECT answer
3734
                                FROM $TBL_TRACK_ATTEMPT
3735
                                WHERE exe_id = $exeId AND question_id = $questionId";
3736
3737
                        $result = Database::query($sql);
3738
                        while ($row = Database::fetch_array($result)) {
3739
                            $ind = $row['answer'];
3740
                            $values = explode(':', $ind);
3741
                            $myAnswerId = $values[0] ?? null;
3742
                            $option = $values[1] ?? null;
3743
                            $percent = $values[2] ?? null;
3744
                            $choice[$myAnswerId] = $option;
3745
                            $choiceDegreeCertainty[$myAnswerId] = $percent;
3746
                        }
3747
                    }
3748
3749
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3750
                    $studentChoiceDegree = isset($choiceDegreeCertainty[$answerAutoId]) ? $choiceDegreeCertainty[$answerAutoId] : null;
3751
3752
                    // student score update
3753
                    if (!empty($studentChoice)) {
3754
                        if ($studentChoice == $answerCorrect) {
3755
                            // correct answer and student is Unsure or PrettySur
3756
                            if (isset($quiz_question_options[$studentChoiceDegree]) &&
3757
                                $quiz_question_options[$studentChoiceDegree]['position'] >= 3 &&
3758
                                $quiz_question_options[$studentChoiceDegree]['position'] < 9
3759
                            ) {
3760
                                $questionScore += $true_score;
3761
                            } else {
3762
                                // student ignore correct answer
3763
                                $questionScore += $doubt_score;
3764
                            }
3765
                        } else {
3766
                            // false answer and student is Unsure or PrettySur
3767
                            if (isset($quiz_question_options[$studentChoiceDegree]) && $quiz_question_options[$studentChoiceDegree]['position'] >= 3
3768
                                && $quiz_question_options[$studentChoiceDegree]['position'] < 9) {
3769
                                $questionScore += $false_score;
3770
                            } else {
3771
                                // student ignore correct answer
3772
                                $questionScore += $doubt_score;
3773
                            }
3774
                        }
3775
                    }
3776
                    $totalScore = $questionScore;
3777
3778
                    break;
3779
                case MULTIPLE_ANSWER:
3780
                case MULTIPLE_ANSWER_DROPDOWN:
3781
                    if ($from_database) {
3782
                        $choice = [];
3783
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3784
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3785
                        $resultans = Database::query($sql);
3786
                        while ($row = Database::fetch_array($resultans)) {
3787
                            $choice[$row['answer']] = 1;
3788
                        }
3789
3790
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3791
                        $real_answers[$answerId] = (bool) $studentChoice;
3792
3793
                        if ($studentChoice) {
3794
                            $questionScore += $answerWeighting;
3795
                        }
3796
                    } else {
3797
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3798
                        $real_answers[$answerId] = (bool) $studentChoice;
3799
3800
                        if (isset($studentChoice)
3801
                            || (MULTIPLE_ANSWER_DROPDOWN == $answerType && in_array($answerAutoId, $choice))
3802
                        ) {
3803
                            $correctAnswerId[] = $answerAutoId;
3804
                            $questionScore += $answerWeighting;
3805
                        }
3806
                    }
3807
                    $totalScore += $answerWeighting;
3808
3809
                    break;
3810
                case GLOBAL_MULTIPLE_ANSWER:
3811
                    if ($from_database) {
3812
                        $choice = [];
3813
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3814
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3815
                        $resultans = Database::query($sql);
3816
                        while ($row = Database::fetch_array($resultans)) {
3817
                            $choice[$row['answer']] = 1;
3818
                        }
3819
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3820
                        $real_answers[$answerId] = (bool) $studentChoice;
3821
                        if ($studentChoice) {
3822
                            $questionScore += $answerWeighting;
3823
                        }
3824
                    } else {
3825
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3826
                        if (isset($studentChoice)) {
3827
                            $questionScore += $answerWeighting;
3828
                        }
3829
                        $real_answers[$answerId] = (bool) $studentChoice;
3830
                    }
3831
                    $totalScore += $answerWeighting;
3832
                    if ($debug) {
3833
                        error_log("studentChoice: $studentChoice");
3834
                    }
3835
3836
                    break;
3837
                case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
3838
                    if ($from_database) {
3839
                        $choice = [];
3840
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3841
                                WHERE exe_id = $exeId AND question_id = $questionId";
3842
                        $resultans = Database::query($sql);
3843
                        while ($row = Database::fetch_array($resultans)) {
3844
                            $result = explode(':', $row['answer']);
3845
                            if (isset($result[0])) {
3846
                                $my_answer_id = isset($result[0]) ? $result[0] : '';
3847
                                $option = isset($result[1]) ? $result[1] : '';
3848
                                $choice[$my_answer_id] = $option;
3849
                            }
3850
                        }
3851
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3852
3853
                        $real_answers[$answerId] = false;
3854
                        if ($answerCorrect == $studentChoice) {
3855
                            $real_answers[$answerId] = true;
3856
                        }
3857
                    } else {
3858
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3859
                        $real_answers[$answerId] = false;
3860
                        if ($answerCorrect == $studentChoice) {
3861
                            $real_answers[$answerId] = true;
3862
                        }
3863
                    }
3864
3865
                    break;
3866
                case MULTIPLE_ANSWER_COMBINATION:
3867
                    if ($from_database) {
3868
                        $choice = [];
3869
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3870
                                WHERE exe_id = $exeId AND question_id = $questionId";
3871
                        $resultans = Database::query($sql);
3872
                        while ($row = Database::fetch_array($resultans)) {
3873
                            $choice[$row['answer']] = 1;
3874
                        }
3875
3876
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3877
                        if (1 == $answerCorrect) {
3878
                            $real_answers[$answerId] = false;
3879
                            if ($studentChoice) {
3880
                                $real_answers[$answerId] = true;
3881
                            }
3882
                        } else {
3883
                            $real_answers[$answerId] = true;
3884
                            if ($studentChoice) {
3885
                                $real_answers[$answerId] = false;
3886
                            }
3887
                        }
3888
                    } else {
3889
                        $studentChoice = $choice[$answerAutoId] ?? null;
3890
                        if (1 == $answerCorrect) {
3891
                            $real_answers[$answerId] = false;
3892
                            if ($studentChoice) {
3893
                                $real_answers[$answerId] = true;
3894
                            }
3895
                        } else {
3896
                            $real_answers[$answerId] = true;
3897
                            if ($studentChoice) {
3898
                                $real_answers[$answerId] = false;
3899
                            }
3900
                        }
3901
                    }
3902
3903
                    break;
3904
                case FILL_IN_BLANKS:
3905
                case FILL_IN_BLANKS_COMBINATION:
3906
                    $str = '';
3907
                    $answerFromDatabase = '';
3908
                    if ($from_database) {
3909
                        $sql = "SELECT answer
3910
                                FROM $TBL_TRACK_ATTEMPT
3911
                                WHERE
3912
                                    exe_id = $exeId AND
3913
                                    question_id= $questionId ";
3914
                        $result = Database::query($sql);
3915
                        $str = $answerFromDatabase = Database::result($result, 0, 'answer');
3916
                    }
3917
3918
                    // if ($saved_results == false && strpos($answerFromDatabase, 'font color') !== false) {
3919
                    if (false) {
3920
                        // the question is encoded like this
3921
                        // [A] B [C] D [E] F::10,10,10@1
3922
                        // number 1 before the "@" means that is a switchable fill in blank question
3923
                        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
3924
                        // means that is a normal fill blank question
3925
                        // first we explode the "::"
3926
                        $pre_array = explode('::', $answer);
3927
3928
                        // is switchable fill blank or not
3929
                        $last = count($pre_array) - 1;
3930
                        $is_set_switchable = explode('@', $pre_array[$last]);
3931
                        $switchable_answer_set = false;
3932
                        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
3933
                            $switchable_answer_set = true;
3934
                        }
3935
                        $answer = '';
3936
                        for ($k = 0; $k < $last; $k++) {
3937
                            $answer .= $pre_array[$k];
3938
                        }
3939
                        // splits weightings that are joined with a comma
3940
                        $answerWeighting = explode(',', $is_set_switchable[0]);
3941
                        // we save the answer because it will be modified
3942
                        $temp = $answer;
3943
                        $answer = '';
3944
                        $j = 0;
3945
                        //initialise answer tags
3946
                        $user_tags = $correct_tags = $real_text = [];
3947
                        // the loop will stop at the end of the text
3948
                        while (1) {
3949
                            // quits the loop if there are no more blanks (detect '[')
3950
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
3951
                                // adds the end of the text
3952
                                $answer = $temp;
3953
                                $real_text[] = $answer;
3954
3955
                                break; //no more "blanks", quit the loop
3956
                            }
3957
                            // adds the piece of text that is before the blank
3958
                            //and ends with '[' into a general storage array
3959
                            $real_text[] = api_substr($temp, 0, $pos + 1);
3960
                            $answer .= api_substr($temp, 0, $pos + 1);
3961
                            //take the string remaining (after the last "[" we found)
3962
                            $temp = api_substr($temp, $pos + 1);
3963
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
3964
                            if (false === ($pos = api_strpos($temp, ']'))) {
3965
                                // adds the end of the text
3966
                                $answer .= $temp;
3967
3968
                                break;
3969
                            }
3970
                            if ($from_database) {
3971
                                $str = $answerFromDatabase;
3972
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
3973
                                $str = str_replace('\r\n', '', $str);
3974
3975
                                $choice = $arr[1];
3976
                                if (isset($choice[$j])) {
3977
                                    $tmp = api_strrpos($choice[$j], ' / ');
3978
                                    $choice[$j] = api_substr($choice[$j], 0, $tmp);
3979
                                    $choice[$j] = trim($choice[$j]);
3980
                                    // Needed to let characters ' and " to work as part of an answer
3981
                                    $choice[$j] = stripslashes($choice[$j]);
3982
                                } else {
3983
                                    $choice[$j] = null;
3984
                                }
3985
                            } else {
3986
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
3987
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
3988
                            }
3989
3990
                            $user_tags[] = $choice[$j];
3991
                            // Put the contents of the [] answer tag into correct_tags[]
3992
                            $correct_tags[] = api_substr($temp, 0, $pos);
3993
                            $j++;
3994
                            $temp = api_substr($temp, $pos + 1);
3995
                        }
3996
                        $answer = '';
3997
                        $real_correct_tags = $correct_tags;
3998
                        $chosen_list = [];
3999
4000
                        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...
4001
                            if (0 == $i) {
4002
                                $answer .= $real_text[0];
4003
                            }
4004
                            if (!$switchable_answer_set) {
4005
                                // Needed to parse ' and " characters
4006
                                $user_tags[$i] = stripslashes($user_tags[$i]);
4007
                                if ($correct_tags[$i] == $user_tags[$i]) {
4008
                                    // gives the related weighting to the student
4009
                                    $questionScore += $answerWeighting[$i];
4010
                                    // increments total score
4011
                                    $totalScore += $answerWeighting[$i];
4012
                                    // adds the word in green at the end of the string
4013
                                    $answer .= $correct_tags[$i];
4014
                                } elseif (!empty($user_tags[$i])) {
4015
                                    // else if the word entered by the student IS NOT the same as
4016
                                    // the one defined by the professor
4017
                                    // adds the word in red at the end of the string, and strikes it
4018
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4019
                                } else {
4020
                                    // adds a tabulation if no word has been typed by the student
4021
                                    $answer .= ''; // remove &nbsp; that causes issue
4022
                                }
4023
                            } else {
4024
                                // switchable fill in the blanks
4025
                                if (in_array($user_tags[$i], $correct_tags)) {
4026
                                    $chosen_list[] = $user_tags[$i];
4027
                                    $correct_tags = array_diff($correct_tags, $chosen_list);
4028
                                    // gives the related weighting to the student
4029
                                    $questionScore += $answerWeighting[$i];
4030
                                    // increments total score
4031
                                    $totalScore += $answerWeighting[$i];
4032
                                    // adds the word in green at the end of the string
4033
                                    $answer .= $user_tags[$i];
4034
                                } elseif (!empty($user_tags[$i])) {
4035
                                    // else if the word entered by the student IS NOT the same
4036
                                    // as the one defined by the professor
4037
                                    // adds the word in red at the end of the string, and strikes it
4038
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4039
                                } else {
4040
                                    // adds a tabulation if no word has been typed by the student
4041
                                    $answer .= ''; // remove &nbsp; that causes issue
4042
                                }
4043
                            }
4044
4045
                            // adds the correct word, followed by ] to close the blank
4046
                            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4047
                            if (isset($real_text[$i + 1])) {
4048
                                $answer .= $real_text[$i + 1];
4049
                            }
4050
                        }
4051
                    } else {
4052
                        // insert the student result in the track_e_attempt table, field answer
4053
                        // $answer is the answer like in the c_quiz_answer table for the question
4054
                        // student data are choice[]
4055
                        $listCorrectAnswers = FillBlanks::getAnswerInfo($answer);
4056
                        $switchableAnswerSet = $listCorrectAnswers['switchable'];
4057
                        $answerWeighting = $listCorrectAnswers['weighting'];
4058
                        // user choices is an array $choice
4059
4060
                        // get existing user data in n the BDD
4061
                        if ($from_database) {
4062
                            $listStudentResults = FillBlanks::getAnswerInfo(
4063
                                $answerFromDatabase,
4064
                                true
4065
                            );
4066
                            $choice = $listStudentResults['student_answer'];
4067
                        }
4068
4069
                        // loop other all blanks words
4070
                        if (!$switchableAnswerSet) {
4071
                            // not switchable answer, must be in the same place than teacher order
4072
                            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...
4073
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
4074
                                $correctAnswer = $listCorrectAnswers['words'][$i];
4075
4076
                                if ($debug) {
4077
                                    error_log("Student answer: $i");
4078
                                    error_log($studentAnswer);
4079
                                }
4080
4081
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4082
                                // Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
4083
                                // ENT_QUOTES is used in order to transform ' to &#039;
4084
                                if (!$from_database) {
4085
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4086
                                    if ($debug) {
4087
                                        error_log('Student answer cleaned:');
4088
                                        error_log($studentAnswer);
4089
                                    }
4090
                                }
4091
4092
                                $isAnswerCorrect = 0;
4093
                                if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
4094
                                    // gives the related weighting to the student
4095
                                    $questionScore += $answerWeighting[$i];
4096
                                    // increments total score
4097
                                    $totalScore += $answerWeighting[$i];
4098
                                    $isAnswerCorrect = 1;
4099
                                }
4100
                                if ($debug) {
4101
                                    error_log("isAnswerCorrect $i: $isAnswerCorrect");
4102
                                }
4103
4104
                                $studentAnswerToShow = $studentAnswer;
4105
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4106
                                if ($debug) {
4107
                                    error_log("Fill in blank type: $type");
4108
                                }
4109
                                if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4110
                                    $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4111
                                    if ('' != $studentAnswer) {
4112
                                        foreach ($listMenu as $item) {
4113
                                            if (sha1($item) == $studentAnswer) {
4114
                                                $studentAnswerToShow = $item;
4115
                                            }
4116
                                        }
4117
                                    }
4118
                                }
4119
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4120
                                $listCorrectAnswers['student_score'][$i] = $isAnswerCorrect;
4121
                            }
4122
                        } else {
4123
                            // switchable answer
4124
                            $listStudentAnswerTemp = $choice;
4125
                            $listTeacherAnswerTemp = $listCorrectAnswers['words'];
4126
4127
                            // for every teacher answer, check if there is a student answer
4128
                            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...
4129
                                $studentAnswer = trim($listStudentAnswerTemp[$i]);
4130
                                $studentAnswerToShow = $studentAnswer;
4131
4132
                                if (empty($studentAnswer)) {
4133
                                    break;
4134
                                }
4135
4136
                                if ($debug) {
4137
                                    error_log("Student answer: $i");
4138
                                    error_log($studentAnswer);
4139
                                }
4140
4141
                                if (!$from_database) {
4142
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4143
                                    if ($debug) {
4144
                                        error_log("Student answer cleaned:");
4145
                                        error_log($studentAnswer);
4146
                                    }
4147
                                }
4148
4149
                                $found = false;
4150
                                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...
4151
                                    $correctAnswer = $listTeacherAnswerTemp[$j];
4152
4153
                                    if (!$found) {
4154
                                        if (FillBlanks::isStudentAnswerGood(
4155
                                            $studentAnswer,
4156
                                            $correctAnswer,
4157
                                            $from_database
4158
                                        )) {
4159
                                            $questionScore += $answerWeighting[$i];
4160
                                            $totalScore += $answerWeighting[$i];
4161
                                            $listTeacherAnswerTemp[$j] = '';
4162
                                            $found = true;
4163
                                        }
4164
                                    }
4165
4166
                                    $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4167
                                    if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4168
                                        $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4169
                                        if (!empty($studentAnswer)) {
4170
                                            foreach ($listMenu as $key => $item) {
4171
                                                if (sha1($item) === $studentAnswer) {
4172
                                                    $studentAnswerToShow = $item;
4173
                                                    break;
4174
                                                }
4175
                                            }
4176
                                        }
4177
                                    }
4178
                                }
4179
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4180
                                $listCorrectAnswers['student_score'][$i] = $found ? 1 : 0;
4181
                            }
4182
                        }
4183
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
4184
                    }
4185
4186
                    break;
4187
                case CALCULATED_ANSWER:
4188
                    $calculatedAnswerList = Session::read('calculatedAnswerId');
4189
                    if (!empty($calculatedAnswerList)) {
4190
                        $answer = $objAnswerTmp->selectAnswer($calculatedAnswerList[$questionId]);
4191
                        $preArray = explode('@@', $answer);
4192
                        $last = count($preArray) - 1;
4193
                        $answer = '';
4194
                        for ($k = 0; $k < $last; $k++) {
4195
                            $answer .= $preArray[$k];
4196
                        }
4197
                        $answerWeighting = [$answerWeighting];
4198
                        // we save the answer because it will be modified
4199
                        $temp = $answer;
4200
                        $answer = '';
4201
                        $j = 0;
4202
                        // initialise answer tags
4203
                        $userTags = $correctTags = $realText = [];
4204
                        // the loop will stop at the end of the text
4205
                        while (1) {
4206
                            // quits the loop if there are no more blanks (detect '[')
4207
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
4208
                                // adds the end of the text
4209
                                $answer = $temp;
4210
                                $realText[] = $answer;
4211
4212
                                break; //no more "blanks", quit the loop
4213
                            }
4214
                            // adds the piece of text that is before the blank
4215
                            // and ends with '[' into a general storage array
4216
                            $realText[] = api_substr($temp, 0, $pos + 1);
4217
                            $answer .= api_substr($temp, 0, $pos + 1);
4218
                            // take the string remaining (after the last "[" we found)
4219
                            $temp = api_substr($temp, $pos + 1);
4220
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4221
                            if (false === ($pos = api_strpos($temp, ']'))) {
4222
                                // adds the end of the text
4223
                                $answer .= $temp;
4224
4225
                                break;
4226
                            }
4227
4228
                            if ($from_database) {
4229
                                $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4230
                                        WHERE
4231
                                            exe_id = $exeId AND
4232
                                            question_id = $questionId ";
4233
                                $result = Database::query($sql);
4234
                                $str = Database::result($result, 0, 'answer');
4235
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4236
                                $str = str_replace('\r\n', '', $str);
4237
                                $choice = $arr[1];
4238
                                if (isset($choice[$j])) {
4239
                                    $tmp = api_strrpos($choice[$j], ' / ');
4240
                                    if ($tmp) {
4241
                                        $choice[$j] = api_substr($choice[$j], 0, $tmp);
4242
                                    } else {
4243
                                        $tmp = ltrim($tmp, '[');
4244
                                        $tmp = rtrim($tmp, ']');
4245
                                    }
4246
                                    $choice[$j] = trim($choice[$j]);
4247
                                    // Needed to let characters ' and " to work as part of an answer
4248
                                    $choice[$j] = stripslashes($choice[$j]);
4249
                                } else {
4250
                                    $choice[$j] = null;
4251
                                }
4252
                            } else {
4253
                                // This value is the user input not escaped while correct answer is escaped by ckeditor
4254
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4255
                            }
4256
                            $userTags[] = $choice[$j];
4257
                            // put the contents of the [] answer tag into correct_tags[]
4258
                            $correctTags[] = api_substr($temp, 0, $pos);
4259
                            $j++;
4260
                            $temp = api_substr($temp, $pos + 1);
4261
                        }
4262
                        $answer = '';
4263
                        $realCorrectTags = $correctTags;
4264
                        $calculatedStatus = Display::label(get_lang('Incorrect'), 'danger');
4265
                        $expectedAnswer = '';
4266
                        $calculatedChoice = '';
4267
4268
                        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...
4269
                            if (0 == $i) {
4270
                                $answer .= $realText[0];
4271
                            }
4272
                            // Needed to parse ' and " characters
4273
                            $userTags[$i] = stripslashes($userTags[$i]);
4274
                            if ($correctTags[$i] == $userTags[$i]) {
4275
                                // gives the related weighting to the student
4276
                                $questionScore += $answerWeighting[$i];
4277
                                // increments total score
4278
                                $totalScore += $answerWeighting[$i];
4279
                                // adds the word in green at the end of the string
4280
                                $answer .= $correctTags[$i];
4281
                                $calculatedChoice = $correctTags[$i];
4282
                            } elseif (!empty($userTags[$i])) {
4283
                                // else if the word entered by the student IS NOT the same as
4284
                                // the one defined by the professor
4285
                                // adds the word in red at the end of the string, and strikes it
4286
                                $answer .= '<font color="red"><s>'.$userTags[$i].'</s></font>';
4287
                                $calculatedChoice = $userTags[$i];
4288
                            } else {
4289
                                // adds a tabulation if no word has been typed by the student
4290
                                $answer .= ''; // remove &nbsp; that causes issue
4291
                            }
4292
                            // adds the correct word, followed by ] to close the blank
4293
                            if (EXERCISE_FEEDBACK_TYPE_EXAM != $this->results_disabled) {
4294
                                $answer .= ' / <font color="green"><b>'.$realCorrectTags[$i].'</b></font>';
4295
                                $calculatedStatus = Display::label(get_lang('Correct'), 'success');
4296
                                $expectedAnswer = $realCorrectTags[$i];
4297
                            }
4298
                            $answer .= ']';
4299
                            if (isset($realText[$i + 1])) {
4300
                                $answer .= $realText[$i + 1];
4301
                            }
4302
                        }
4303
                    } else {
4304
                        if ($from_database) {
4305
                            $sql = "SELECT *
4306
                                    FROM $TBL_TRACK_ATTEMPT
4307
                                    WHERE
4308
                                        exe_id = $exeId AND
4309
                                        question_id = $questionId ";
4310
                            $result = Database::query($sql);
4311
                            $resultData = Database::fetch_assoc($result);
4312
                            $answer = $resultData['answer'];
4313
                            $questionScore = $resultData['marks'];
4314
                        }
4315
                    }
4316
4317
                    break;
4318
                case UPLOAD_ANSWER:
4319
                case FREE_ANSWER:
4320
                    if ($from_database) {
4321
                        $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
4322
                                 WHERE
4323
                                    exe_id = $exeId AND
4324
                                    question_id= ".$questionId;
4325
                        $result = Database::query($sql);
4326
                        $data = Database::fetch_array($result);
4327
                        $choice = '';
4328
                        $questionScore = 0;
4329
                        if ($data) {
4330
                            $choice = $data['answer'];
4331
                            $questionScore = $data['marks'];
4332
                        }
4333
4334
                        $choice = str_replace('\r\n', '', $choice);
4335
                        $choice = stripslashes($choice);
4336
4337
                        if (-1 == $questionScore) {
4338
                            $totalScore += 0;
4339
                        } else {
4340
                            $totalScore += $questionScore;
4341
                        }
4342
                        if ('' == $questionScore) {
4343
                            $questionScore = 0;
4344
                        }
4345
                        $arrques = $questionName;
4346
                        $arrans = $choice;
4347
                    } else {
4348
                        $studentChoice = $choice;
4349
                        if ($studentChoice) {
4350
                            //Fixing negative puntation see #2193
4351
                            $questionScore = 0;
4352
                            $totalScore += 0;
4353
                        }
4354
                    }
4355
4356
                    break;
4357
                case ORAL_EXPRESSION:
4358
                    if ($from_database) {
4359
                        $query = "SELECT answer, marks
4360
                                  FROM $TBL_TRACK_ATTEMPT
4361
                                  WHERE
4362
                                        exe_id = $exeId AND
4363
                                        question_id = $questionId
4364
                                 ";
4365
                        $resq = Database::query($query);
4366
                        $row = Database::fetch_assoc($resq);
4367
                        $choice = '';
4368
                        $questionScore = 0;
4369
4370
                        if (is_array($row)) {
4371
                            $choice = $row['answer'];
4372
                            $choice = str_replace('\r\n', '', $choice);
4373
                            $choice = stripslashes($choice);
4374
                            $questionScore = $row['marks'];
4375
                        }
4376
4377
                        if (-1 == $questionScore) {
4378
                            $totalScore += 0;
4379
                        } else {
4380
                            $totalScore += $questionScore;
4381
                        }
4382
                        $arrques = $questionName;
4383
                        $arrans = $choice;
4384
                    } else {
4385
                        $studentChoice = $choice;
4386
                        if ($studentChoice) {
4387
                            //Fixing negative puntation see #2193
4388
                            $questionScore = 0;
4389
                            $totalScore += 0;
4390
                        }
4391
                    }
4392
4393
                    break;
4394
                case MULTIPLE_ANSWER_DROPDOWN:
4395
                    $totalScore += $answerWeighting;
4396
                    break;
4397
                case MULTIPLE_ANSWER_DROPDOWN_COMBINATION:
4398
                    break;
4399
                case MATCHING_COMBINATION:
4400
                case MATCHING_DRAGGABLE_COMBINATION:
4401
                case DRAGGABLE:
4402
                case MATCHING_DRAGGABLE:
4403
                case MATCHING:
4404
                    if ($from_database) {
4405
                        $sql = "SELECT iid, answer
4406
                                FROM $table_ans
4407
                                WHERE
4408
                                    question_id = $questionId AND
4409
                                    correct = 0
4410
                                ";
4411
                        $result = Database::query($sql);
4412
                        // Getting the real answer
4413
                        $real_list = [];
4414
                        while ($realAnswer = Database::fetch_array($result)) {
4415
                            $real_list[$realAnswer['iid']] = $realAnswer['answer'];
4416
                        }
4417
4418
                        $orderBy = ' ORDER BY iid ';
4419
                        if (DRAGGABLE == $answerType) {
4420
                            $orderBy = ' ORDER BY correct ';
4421
                        }
4422
4423
                        $sql = "SELECT iid, answer, correct, ponderation
4424
                                FROM $table_ans
4425
                                WHERE
4426
                                    question_id = $questionId AND
4427
                                    correct <> 0
4428
                                $orderBy";
4429
                        $result = Database::query($sql);
4430
                        $options = [];
4431
                        $correctAnswers = [];
4432
                        while ($row = Database::fetch_assoc($result)) {
4433
                            $options[] = $row;
4434
                            $correctAnswers[$row['correct']] = $row['answer'];
4435
                        }
4436
4437
                        $questionScore = 0;
4438
                        $counterAnswer = 1;
4439
                        foreach ($options as $a_answers) {
4440
                            $i_answer_id = $a_answers['iid']; //3
4441
                            $s_answer_label = $a_answers['answer']; // your daddy - your mother
4442
                            $i_answer_correct_answer = $a_answers['correct']; //1 - 2
4443
                            $i_answer_id_auto = $a_answers['iid']; // 3 - 4
4444
4445
                            $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4446
                                    WHERE
4447
                                        exe_id = '$exeId' AND
4448
                                        question_id = '$questionId' AND
4449
                                        position = '$i_answer_id_auto'";
4450
                            $result = Database::query($sql);
4451
                            $s_user_answer = 0;
4452
                            if (Database::num_rows($result) > 0) {
4453
                                //  rich - good looking
4454
                                $s_user_answer = Database::result($result, 0, 0);
4455
                            }
4456
                            $i_answerWeighting = $a_answers['ponderation'];
4457
                            $user_answer = '';
4458
                            $status = Display::label(get_lang('Incorrect'), 'danger');
4459
4460
                            if (!empty($s_user_answer)) {
4461
                                if (DRAGGABLE == $answerType) {
4462
                                    if ($s_user_answer == $i_answer_correct_answer) {
4463
                                        $questionScore += $i_answerWeighting;
4464
                                        $totalScore += $i_answerWeighting;
4465
                                        $user_answer = Display::label(get_lang('Correct'), 'success');
4466
                                        if ($this->showExpectedChoice() && !empty($i_answer_id_auto)) {
4467
                                            $user_answer = $answerMatching[$i_answer_id_auto];
4468
                                        }
4469
                                        $status = Display::label(get_lang('Correct'), 'success');
4470
                                    } else {
4471
                                        $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4472
                                        if ($this->showExpectedChoice() && !empty($s_user_answer)) {
4473
                                            /*$data = $options[$real_list[$s_user_answer] - 1];
4474
                                            $user_answer = $data['answer'];*/
4475
                                            $user_answer = $correctAnswers[$s_user_answer] ?? '';
4476
                                        }
4477
                                    }
4478
                                } else {
4479
                                    if ($s_user_answer == $i_answer_correct_answer) {
4480
                                        $questionScore += $i_answerWeighting;
4481
                                        $totalScore += $i_answerWeighting;
4482
                                        $status = Display::label(get_lang('Correct'), 'success');
4483
4484
                                        // Try with id
4485
                                        if (isset($real_list[$i_answer_id])) {
4486
                                            $user_answer = Display::span(
4487
                                                $real_list[$i_answer_id],
4488
                                                ['style' => 'color: #008000; font-weight: bold;']
4489
                                            );
4490
                                        }
4491
4492
                                        // Try with $i_answer_id_auto
4493
                                        if (empty($user_answer)) {
4494
                                            if (isset($real_list[$i_answer_id_auto])) {
4495
                                                $user_answer = Display::span(
4496
                                                    $real_list[$i_answer_id_auto],
4497
                                                    ['style' => 'color: #008000; font-weight: bold;']
4498
                                                );
4499
                                            }
4500
                                        }
4501
4502
                                        if (isset($real_list[$i_answer_correct_answer])) {
4503
                                            $matchingCorrectAnswers[$questionId]['from_database']['correct'][$i_answer_correct_answer] = $real_list[$i_answer_correct_answer];
4504
                                            $user_answer = Display::span(
4505
                                                $real_list[$i_answer_correct_answer],
4506
                                                ['style' => 'color: #008000; font-weight: bold;']
4507
                                            );
4508
                                        }
4509
                                    } else {
4510
                                        $user_answer = Display::span(
4511
                                            $real_list[$s_user_answer],
4512
                                            ['style' => 'color: #FF0000; text-decoration: line-through;']
4513
                                        );
4514
                                        if ($this->showExpectedChoice()) {
4515
                                            if (isset($real_list[$s_user_answer])) {
4516
                                                $user_answer = Display::span($real_list[$s_user_answer]);
4517
                                            }
4518
                                        }
4519
                                    }
4520
                                }
4521
                            } elseif (DRAGGABLE == $answerType) {
4522
                                $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4523
                                if ($this->showExpectedChoice()) {
4524
                                    $user_answer = '';
4525
                                }
4526
                            } else {
4527
                                $user_answer = Display::span(
4528
                                    get_lang('Incorrect').' &nbsp;',
4529
                                    ['style' => 'color: #FF0000; text-decoration: line-through;']
4530
                                );
4531
                                if ($this->showExpectedChoice()) {
4532
                                    $user_answer = '';
4533
                                }
4534
                            }
4535
4536
                            if ($show_result) {
4537
                                if (false === $this->showExpectedChoice() &&
4538
                                    false === $showTotalScoreAndUserChoicesInLastAttempt
4539
                                ) {
4540
                                    $user_answer = '';
4541
                                }
4542
                                switch ($answerType) {
4543
                                    case MATCHING:
4544
                                    case MATCHING_DRAGGABLE:
4545
                                    case MATCHING_COMBINATION:
4546
                                    case MATCHING_DRAGGABLE_COMBINATION:
4547
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4548
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4549
                                                break;
4550
                                            }
4551
                                        }
4552
                                        echo '<tr>';
4553
                                        if (!in_array(
4554
                                            $this->results_disabled,
4555
                                            [
4556
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4557
                                            ]
4558
                                        )
4559
                                        ) {
4560
                                            echo '<td>'.$s_answer_label.'</td>';
4561
                                            echo '<td>'.$user_answer.'</td>';
4562
                                        } else {
4563
                                            echo '<td>'.$s_answer_label.'</td>';
4564
                                            $status = Display::label(get_lang('Correct'), 'success');
4565
                                        }
4566
4567
                                        if ($this->showExpectedChoice()) {
4568
                                            if ($this->showExpectedChoiceColumn()) {
4569
                                                echo '<td>';
4570
                                                if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
4571
                                                    if (isset($real_list[$i_answer_correct_answer]) &&
4572
                                                        true == $showTotalScoreAndUserChoicesInLastAttempt
4573
                                                    ) {
4574
                                                        echo Display::span(
4575
                                                            $real_list[$i_answer_correct_answer]
4576
                                                        );
4577
                                                    }
4578
                                                }
4579
                                                echo '</td>';
4580
                                            }
4581
                                            echo '<td class="text-center">'.$status.'</td>';
4582
                                        } else {
4583
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
4584
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4585
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4586
                                                ) {
4587
                                                    if ($this->showExpectedChoiceColumn()) {
4588
                                                        echo '<td>';
4589
                                                        echo Display::span(
4590
                                                            $real_list[$i_answer_correct_answer],
4591
                                                            ['style' => 'color: #008000; font-weight: bold;']
4592
                                                        );
4593
                                                        echo '</td>';
4594
                                                    }
4595
                                                }
4596
                                            }
4597
                                        }
4598
                                        echo '</tr>';
4599
4600
                                        break;
4601
                                    case DRAGGABLE:
4602
                                        if (false == $showTotalScoreAndUserChoicesInLastAttempt) {
4603
                                            $s_answer_label = '';
4604
                                        }
4605
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4606
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4607
                                                break;
4608
                                            }
4609
                                        }
4610
                                        echo '<tr>';
4611
                                        if ($this->showExpectedChoice()) {
4612
                                            if (!in_array(
4613
                                                $this->results_disabled,
4614
                                                [
4615
                                                    RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4616
                                                    //RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4617
                                                ]
4618
                                            )
4619
                                            ) {
4620
                                                echo '<td>'.$user_answer.'</td>';
4621
                                            } else {
4622
                                                $status = Display::label(get_lang('Correct'), 'success');
4623
                                            }
4624
                                            echo '<td>'.$s_answer_label.'</td>';
4625
                                            echo '<td class="text-center">'.$status.'</td>';
4626
                                        } else {
4627
                                            echo '<td>'.$s_answer_label.'</td>';
4628
                                            echo '<td>'.$user_answer.'</td>';
4629
                                            echo '<td>';
4630
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
4631
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4632
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4633
                                                ) {
4634
                                                    echo Display::span(
4635
                                                        $real_list[$i_answer_correct_answer],
4636
                                                        ['style' => 'color: #008000; font-weight: bold;']
4637
                                                    );
4638
                                                }
4639
                                            }
4640
                                            echo '</td>';
4641
                                        }
4642
                                        echo '</tr>';
4643
4644
                                        break;
4645
                                }
4646
                            }
4647
                            $counterAnswer++;
4648
                        }
4649
                        $matchingCorrectAnswers[$questionId]['from_database']['count_options'] = count($options);
4650
                        break 2; // break the switch and the "for" condition
4651
                    } else {
4652
                        if ($answerCorrect) {
4653
                            if (isset($choice[$answerAutoId]) &&
4654
                                $answerCorrect == $choice[$answerAutoId]
4655
                            ) {
4656
                                $matchingCorrectAnswers[$questionId]['form_values']['correct'][$answerAutoId] = $choice[$answerAutoId];
4657
                                $correctAnswerId[] = $answerAutoId;
4658
                                $questionScore += $answerWeighting;
4659
                                $totalScore += $answerWeighting;
4660
                                $user_answer = Display::span($answerMatching[$choice[$answerAutoId]]);
4661
                            } else {
4662
                                if (isset($answerMatching[$choice[$answerAutoId]])) {
4663
                                    $user_answer = Display::span(
4664
                                        $answerMatching[$choice[$answerAutoId]],
4665
                                        ['style' => 'color: #FF0000; text-decoration: line-through;']
4666
                                    );
4667
                                }
4668
                            }
4669
                            $matching[$answerAutoId] = $choice[$answerAutoId];
4670
                        }
4671
                        $matchingCorrectAnswers[$questionId]['form_values']['count_options'] = count($choice);
4672
                    }
4673
4674
                    break;
4675
                case HOT_SPOT:
4676
                case HOT_SPOT_COMBINATION:
4677
                    if ($from_database) {
4678
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4679
                        // Check auto id
4680
                        $foundAnswerId = $answerAutoId;
4681
                        $sql = "SELECT hotspot_correct
4682
                                FROM $TBL_TRACK_HOTSPOT
4683
                                WHERE
4684
                                    hotspot_exe_id = $exeId AND
4685
                                    hotspot_question_id= $questionId AND
4686
                                    hotspot_answer_id = $answerAutoId
4687
                                ORDER BY hotspot_id ASC";
4688
                        $result = Database::query($sql);
4689
                        if (Database::num_rows($result)) {
4690
                            $studentChoice = Database::result(
4691
                                $result,
4692
                                0,
4693
                                'hotspot_correct'
4694
                            );
4695
4696
                            if ($studentChoice) {
4697
                                $questionScore += $answerWeighting;
4698
                                $totalScore += $answerWeighting;
4699
                            }
4700
                        } else {
4701
                            // If answer.id is different:
4702
                            $sql = "SELECT hotspot_correct
4703
                                FROM $TBL_TRACK_HOTSPOT
4704
                                WHERE
4705
                                    hotspot_exe_id = $exeId AND
4706
                                    hotspot_question_id= $questionId AND
4707
                                    hotspot_answer_id = ".(int) $answerId.'
4708
                                ORDER BY hotspot_id ASC';
4709
                            $result = Database::query($sql);
4710
4711
                            $foundAnswerId = $answerId;
4712
                            if (Database::num_rows($result)) {
4713
                                $studentChoice = Database::result(
4714
                                    $result,
4715
                                    0,
4716
                                    'hotspot_correct'
4717
                                );
4718
4719
                                if ($studentChoice) {
4720
                                    $questionScore += $answerWeighting;
4721
                                    $totalScore += $answerWeighting;
4722
                                }
4723
                            } else {
4724
                                // check answer.iid
4725
                                if (!empty($answerIid)) {
4726
                                    $sql = "SELECT hotspot_correct
4727
                                            FROM $TBL_TRACK_HOTSPOT
4728
                                            WHERE
4729
                                                hotspot_exe_id = $exeId AND
4730
                                                hotspot_question_id= $questionId AND
4731
                                                hotspot_answer_id = $answerIid
4732
                                            ORDER BY hotspot_id ASC";
4733
                                    $result = Database::query($sql);
4734
4735
                                    $foundAnswerId = $answerIid;
4736
                                    $studentChoice = Database::result(
4737
                                        $result,
4738
                                        0,
4739
                                        'hotspot_correct'
4740
                                    );
4741
4742
                                    if ($studentChoice) {
4743
                                        $questionScore += $answerWeighting;
4744
                                        $totalScore += $answerWeighting;
4745
                                    }
4746
                                }
4747
                            }
4748
                        }
4749
                    } else {
4750
                        if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
4751
                            $choice[$answerAutoId] = 0;
4752
                            $choice[$answerIid] = 0;
4753
                        } else {
4754
                            $studentChoice = $choice[$answerAutoId];
4755
                            if (empty($studentChoice)) {
4756
                                $studentChoice = $choice[$answerIid];
4757
                            }
4758
                            $choiceIsValid = false;
4759
                            if (!empty($studentChoice)) {
4760
                                $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
4761
                                $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
4762
                                $choicePoint = Geometry::decodePoint($studentChoice);
4763
4764
                                switch ($hotspotType) {
4765
                                    case 'square':
4766
                                        $hotspotProperties = Geometry::decodeSquare($hotspotCoordinates);
4767
                                        $choiceIsValid = Geometry::pointIsInSquare($hotspotProperties, $choicePoint);
4768
4769
                                        break;
4770
                                    case 'circle':
4771
                                        $hotspotProperties = Geometry::decodeEllipse($hotspotCoordinates);
4772
                                        $choiceIsValid = Geometry::pointIsInEllipse($hotspotProperties, $choicePoint);
4773
4774
                                        break;
4775
                                    case 'poly':
4776
                                        $hotspotProperties = Geometry::decodePolygon($hotspotCoordinates);
4777
                                        $choiceIsValid = Geometry::pointIsInPolygon($hotspotProperties, $choicePoint);
4778
4779
                                        break;
4780
                                }
4781
                            }
4782
4783
                            $choice[$answerAutoId] = 0;
4784
                            if ($choiceIsValid) {
4785
                                $questionScore += $answerWeighting;
4786
                                $totalScore += $answerWeighting;
4787
                                $choice[$answerAutoId] = 1;
4788
                                $choice[$answerIid] = 1;
4789
                            }
4790
                            $studentChoice = $choiceIsValid ? 1 : 0;
4791
                        }
4792
                    }
4793
4794
                    break;
4795
                case HOT_SPOT_ORDER:
4796
                    // @todo never added to chamilo
4797
                    // for hotspot with fixed order
4798
                    $studentChoice = $choice['order'][$answerId];
4799
                    if ($studentChoice == $answerId) {
4800
                        $questionScore += $answerWeighting;
4801
                        $totalScore += $answerWeighting;
4802
                        $studentChoice = true;
4803
                    } else {
4804
                        $studentChoice = false;
4805
                    }
4806
4807
                    break;
4808
                case HOT_SPOT_DELINEATION:
4809
                    // for hotspot with delineation
4810
                    if ($from_database) {
4811
                        // getting the user answer
4812
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4813
                        $query = "SELECT hotspot_correct, hotspot_coordinate
4814
                                    FROM $TBL_TRACK_HOTSPOT
4815
                                    WHERE
4816
                                        hotspot_exe_id = $exeId AND
4817
                                        hotspot_question_id= $questionId AND
4818
                                        hotspot_answer_id = '1'";
4819
                        // By default we take 1 because it's a delineation
4820
                        $resq = Database::query($query);
4821
                        $row = Database::fetch_assoc($resq);
4822
4823
                        if ($row && isset($row['hotspot_correct'], $row['hotspot_coordinate'])) {
4824
                            $choice = $row['hotspot_correct'];
4825
                            $user_answer = $row['hotspot_coordinate'];
4826
                            $coords = explode('/', $user_answer);
4827
                        } else {
4828
                            $choice = '';
4829
                            $user_answer = '';
4830
                            $coords = [];
4831
                        }
4832
4833
                        $user_array = '';
4834
                        foreach ($coords as $coord) {
4835
                            [$x, $y] = explode(';', $coord);
4836
                            $user_array .= round($x).';'.round($y).'/';
4837
                        }
4838
                        $user_array = substr($user_array, 0, -1) ?: '';
4839
                    } else {
4840
                        if (!empty($studentChoice)) {
4841
                            $correctAnswerId[] = $answerAutoId;
4842
                            $newquestionList[] = $questionId;
4843
                        }
4844
4845
                        if (1 === $answerId && isset($choice[$answerId])) {
4846
                            $studentChoice = $choice[$answerId];
4847
                            $questionScore += $answerWeighting;
4848
                        }
4849
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
4850
                            $user_array = $_SESSION['exerciseResultCoordinates'][$questionId];
4851
                        }
4852
                    }
4853
                    $_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
4854
                    $_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
4855
4856
                    break;
4857
                case FILL_IN_BLANKS_COMBINATION:
4858
                        $answerFromDatabase = '';
4859
                        if ($from_database) {
4860
                            $sql = "SELECT answer
4861
                            FROM $TBL_TRACK_ATTEMPT
4862
                            WHERE exe_id = $exeId AND question_id = $questionId";
4863
                            $result = Database::query($sql);
4864
                            $answerFromDatabase = Database::result($result, 0, 'answer');
4865
                        }
4866
4867
                        // Teacher definition and weights
4868
                        $info = FillBlanks::getAnswerInfo($answer);
4869
                        $listCorrectAnswers = $info; // keep words, weighting, switchable
4870
                        $switchableAnswerSet = $info['switchable'];
4871
                        $answerWeighting = $info['weighting'];
4872
4873
                        // Student choices
4874
                        if ($from_database && $answerFromDatabase !== '') {
4875
                            $studentInfo = FillBlanks::getAnswerInfo($answerFromDatabase, true);
4876
                            $choice = $studentInfo['student_answer'] ?? [];
4877
                        }
4878
4879
                        // Evaluate each blank and fill student_answer/student_score only
4880
                        if (!$switchableAnswerSet) {
4881
                            // Fixed positions
4882
                            $count = count($info['words']);
4883
                            for ($i = 0; $i < $count; $i++) {
4884
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
4885
                                if (!$from_database) {
4886
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4887
                                }
4888
                                $correctAnswer = $info['words'][$i];
4889
4890
                                // Resolve menu placeholders when present (for display)
4891
                                $studentAnswerToShow = $studentAnswer;
4892
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4893
                                if (FillBlanks::FILL_THE_BLANK_MENU == $type && $studentAnswer !== '') {
4894
                                    $menu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4895
                                    foreach ($menu as $opt) {
4896
                                        if (sha1($opt) === $studentAnswer) {
4897
                                            $studentAnswerToShow = $opt;
4898
                                            break;
4899
                                        }
4900
                                    }
4901
                                }
4902
4903
                                $ok = FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database);
4904
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4905
                                $listCorrectAnswers['student_score'][$i]  = $ok ? 1 : 0;
4906
                            }
4907
                        } else {
4908
                            // Switchable answers: any student entry can match any teacher word once
4909
                            $studentTemp = (array) ($choice ?? []);
4910
                            $teacherTemp = $info['words'];
4911
4912
                            $count = max(count($studentTemp), count($teacherTemp));
4913
                            for ($i = 0; $i < $count; $i++) {
4914
                                $studentAnswer = isset($studentTemp[$i]) ? trim($studentTemp[$i]) : '';
4915
                                $studentAnswerToShow = $studentAnswer;
4916
4917
                                if ($studentAnswer !== '') {
4918
                                    if (!$from_database) {
4919
                                        $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4920
                                    }
4921
                                    $found = false;
4922
                                    foreach ($teacherTemp as $j => $correctAnswer) {
4923
                                        if ($found) {
4924
                                            continue;
4925
                                        }
4926
                                        if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
4927
                                            // consume this teacher slot
4928
                                            $teacherTemp[$j] = '';
4929
                                            $found = true;
4930
                                        }
4931
                                    }
4932
4933
                                    // Resolve menu placeholder for display
4934
                                    if ($studentAnswerToShow !== '') {
4935
                                        $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer ?? '');
4936
                                        if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4937
                                            $menu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4938
                                            foreach ($menu as $opt) {
4939
                                                if (sha1($opt) === $studentAnswer) {
4940
                                                    $studentAnswerToShow = $opt;
4941
                                                    break;
4942
                                                }
4943
                                            }
4944
                                        }
4945
                                    }
4946
4947
                                    $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4948
                                    $listCorrectAnswers['student_score'][$i]  = $found ? 1 : 0;
4949
                                } else {
4950
                                    $listCorrectAnswers['student_answer'][$i] = '';
4951
                                    $listCorrectAnswers['student_score'][$i]  = 0;
4952
                                }
4953
                            }
4954
                        }
4955
4956
                        // Build the annotated answer string for display
4957
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
4958
                    break;
4959
                case HOT_SPOT_COMBINATION:
4960
                        if ($from_database) {
4961
                            $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4962
                            // Try by auto id
4963
                            $sql = "SELECT hotspot_correct
4964
                                FROM $TBL_TRACK_HOTSPOT
4965
                                WHERE hotspot_exe_id = $exeId
4966
                                  AND hotspot_question_id = $questionId
4967
                                  AND hotspot_answer_id = $answerAutoId
4968
                                ORDER BY hotspot_id ASC";
4969
                            $result = Database::query($sql);
4970
                            if (Database::num_rows($result)) {
4971
                                $studentChoice = (int) Database::result($result, 0, 'hotspot_correct');
4972
                                $choice[$answerAutoId] = $studentChoice ? 1 : 0;
4973
                            } else {
4974
                                // Fallback to legacy ids
4975
                                $studentChoice = 0;
4976
                                if (!empty($answerIid)) {
4977
                                    $sql = "SELECT hotspot_correct
4978
                                        FROM $TBL_TRACK_HOTSPOT
4979
                                        WHERE hotspot_exe_id = $exeId
4980
                                          AND hotspot_question_id = $questionId
4981
                                          AND hotspot_answer_id = $answerIid
4982
                                        ORDER BY hotspot_id ASC";
4983
                                    $result = Database::query($sql);
4984
                                    if (Database::num_rows($result)) {
4985
                                        $studentChoice = (int) Database::result($result, 0, 'hotspot_correct');
4986
                                    }
4987
                                }
4988
                                $choice[$answerAutoId] = $studentChoice ? 1 : 0;
4989
                                $choice[$answerIid]    = $choice[$answerAutoId];
4990
                            }
4991
                        } else {
4992
                            // Validate coordinate inside the expected shape and mark $choice
4993
                            if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
4994
                                $choice[$answerAutoId] = 0;
4995
                                $choice[$answerIid]    = 0;
4996
                                $studentChoice = 0;
4997
                            } else {
4998
                                $studentChoicePoint = $choice[$answerAutoId] ?? $choice[$answerIid] ?? '';
4999
                                $isValid = false;
5000
5001
                                if (!empty($studentChoicePoint)) {
5002
                                    $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
5003
                                    $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
5004
                                    $choicePoint = Geometry::decodePoint($studentChoicePoint);
5005
5006
                                    switch ($hotspotType) {
5007
                                        case 'square':
5008
                                            $props = Geometry::decodeSquare($hotspotCoordinates);
5009
                                            $isValid = Geometry::pointIsInSquare($props, $choicePoint);
5010
                                            break;
5011
                                        case 'circle':
5012
                                            $props = Geometry::decodeEllipse($hotspotCoordinates);
5013
                                            $isValid = Geometry::pointIsInEllipse($props, $choicePoint);
5014
                                            break;
5015
                                        case 'poly':
5016
                                            $props = Geometry::decodePolygon($hotspotCoordinates);
5017
                                            $isValid = Geometry::pointIsInPolygon($props, $choicePoint);
5018
                                            break;
5019
                                    }
5020
                                }
5021
5022
                                $choice[$answerAutoId] = $isValid ? 1 : 0;
5023
                                $choice[$answerIid]    = $choice[$answerAutoId];
5024
                                $studentChoice         = $choice[$answerAutoId];
5025
                            }
5026
                        }
5027
                    break;
5028
                case ANNOTATION:
5029
                    if ($from_database) {
5030
                        $sql = "SELECT answer, marks
5031
                                FROM $TBL_TRACK_ATTEMPT
5032
                                WHERE
5033
                                  exe_id = $exeId AND
5034
                                  question_id = $questionId ";
5035
                        $resq = Database::query($sql);
5036
                        $data = Database::fetch_array($resq);
5037
5038
                        $questionScore = empty($data['marks']) ? 0 : $data['marks'];
5039
                        $arrques = $questionName;
5040
5041
                        break;
5042
                    }
5043
                    $studentChoice = $choice;
5044
                    if ($studentChoice) {
5045
                        $questionScore = 0;
5046
                    }
5047
5048
                    break;
5049
            }
5050
5051
            if ($show_result) {
5052
                if ('exercise_result' === $from) {
5053
                    // Display answers (if not matching type, or if the answer is correct)
5054
                    if (!in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION]) ||
5055
                        $answerCorrect
5056
                    ) {
5057
                        if (in_array(
5058
                            $answerType,
5059
                            [
5060
                                UNIQUE_ANSWER,
5061
                                UNIQUE_ANSWER_IMAGE,
5062
                                UNIQUE_ANSWER_NO_OPTION,
5063
                                MULTIPLE_ANSWER,
5064
                                MULTIPLE_ANSWER_COMBINATION,
5065
                                GLOBAL_MULTIPLE_ANSWER,
5066
                                READING_COMPREHENSION,
5067
                            ]
5068
                        )) {
5069
                            ExerciseShowFunctions::display_unique_or_multiple_answer(
5070
                                $this,
5071
                                $feedback_type,
5072
                                $answerType,
5073
                                $studentChoice,
5074
                                $answer,
5075
                                $answerComment,
5076
                                $answerCorrect,
5077
                                0,
5078
                                0,
5079
                                0,
5080
                                $results_disabled,
5081
                                $showTotalScoreAndUserChoicesInLastAttempt,
5082
                                $this->export
5083
                            );
5084
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
5085
                            ExerciseShowFunctions::display_multiple_answer_true_false(
5086
                                $this,
5087
                                $feedback_type,
5088
                                $answerType,
5089
                                $studentChoice,
5090
                                $answer,
5091
                                $answerComment,
5092
                                $answerCorrect,
5093
                                0,
5094
                                $questionId,
5095
                                0,
5096
                                $results_disabled,
5097
                                $showTotalScoreAndUserChoicesInLastAttempt
5098
                            );
5099
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
5100
                            ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5101
                                $this,
5102
                                $feedback_type,
5103
                                $studentChoice,
5104
                                $studentChoiceDegree,
5105
                                $answer,
5106
                                $answerComment,
5107
                                $answerCorrect,
5108
                                $questionId,
5109
                                $results_disabled
5110
                            );
5111
                        } elseif (MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType) {
5112
                            ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5113
                                $this,
5114
                                $feedback_type,
5115
                                $answerType,
5116
                                $studentChoice,
5117
                                $answer,
5118
                                $answerComment,
5119
                                $answerCorrect,
5120
                                0,
5121
                                0,
5122
                                0,
5123
                                $results_disabled,
5124
                                $showTotalScoreAndUserChoicesInLastAttempt
5125
                            );
5126
                        } elseif (in_array($answerType, [FILL_IN_BLANKS, FILL_IN_BLANKS_COMBINATION])) {
5127
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5128
                                $this,
5129
                                $feedback_type,
5130
                                $answer,
5131
                                0,
5132
                                0,
5133
                                $results_disabled,
5134
                                $showTotalScoreAndUserChoicesInLastAttempt,
5135
                                ''
5136
                            );
5137
                        } elseif (CALCULATED_ANSWER == $answerType) {
5138
                            ExerciseShowFunctions::display_calculated_answer(
5139
                                $this,
5140
                                $feedback_type,
5141
                                $answer,
5142
                                0,
5143
                                0,
5144
                                $results_disabled,
5145
                                $showTotalScoreAndUserChoicesInLastAttempt,
5146
                                $expectedAnswer,
5147
                                $calculatedChoice,
5148
                                $calculatedStatus
5149
                            );
5150
                        } elseif (FREE_ANSWER == $answerType) {
5151
                            ExerciseShowFunctions::display_free_answer(
5152
                                $feedback_type,
5153
                                $choice,
5154
                                $exeId,
5155
                                $questionId,
5156
                                $questionScore,
5157
                                $results_disabled
5158
                            );
5159
                        } elseif ($answerType == UPLOAD_ANSWER) {
5160
                            ExerciseShowFunctions::displayUploadAnswer(
5161
                                $feedback_type,
5162
                                $choice,
5163
                                $exeId,
5164
                                $questionId,
5165
                                $questionScore,
5166
                                $results_disabled
5167
                            );
5168
                        } elseif (ORAL_EXPRESSION == $answerType) {
5169
                            // to store the details of open questions in an array to be used in mail
5170
                            /** @var OralExpression $objQuestionTmp */
5171
                            ExerciseShowFunctions::display_oral_expression_answer(
5172
                                $feedback_type,
5173
                                $choice,
5174
                                $exeId,
5175
                                $questionId,
5176
                                $results_disabled,
5177
                                $questionScore,
5178
                                true
5179
                            );
5180
                        } elseif (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION], true)) {
5181
                            $correctAnswerId = 0;
5182
                            /** @var TrackEHotspot $hotspot */
5183
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5184
                                if ($hotspot->getHotspotAnswerId() == $answerAutoId) {
5185
                                    break;
5186
                                }
5187
                            }
5188
5189
                            // force to show whether the choice is correct or not
5190
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
5191
                            ExerciseShowFunctions::display_hotspot_answer(
5192
                                $this,
5193
                                $feedback_type,
5194
                                $answerId,
5195
                                $answer,
5196
                                $studentChoice,
5197
                                $answerComment,
5198
                                $results_disabled,
5199
                                $answerId,
5200
                                $showTotalScoreAndUserChoicesInLastAttempt
5201
                            );
5202
                        } elseif (HOT_SPOT_ORDER == $answerType) {
5203
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5204
                                $feedback_type,
5205
                                $answerId,
5206
                                $answer,
5207
                                $studentChoice,
5208
                                $answerComment
5209
                            );*/
5210
                        } elseif (HOT_SPOT_DELINEATION == $answerType && isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
5211
                            $user_answer = $_SESSION['exerciseResultCoordinates'][$questionId];
5212
5213
                            // Round-up the coordinates
5214
                            $coords = explode('/', $user_answer);
5215
                            $coords = array_filter($coords);
5216
                            $user_array = '';
5217
                            foreach ($coords as $coord) {
5218
                                if (!empty($coord)) {
5219
                                    $parts = explode(';', $coord);
5220
                                    if (!empty($parts)) {
5221
                                        $user_array .= round($parts[0]).';'.round($parts[1]).'/';
5222
                                    }
5223
                                }
5224
                            }
5225
                            $user_array = substr($user_array, 0, -1) ?: '';
5226
                            if ($next) {
5227
                                $user_answer = $user_array;
5228
                                // We compare only the delineation not the other points
5229
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5230
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5231
5232
                                // Calculating the area
5233
                                $poly_user = convert_coordinates($user_answer, '/');
5234
                                $poly_answer = convert_coordinates($answer_question, '|');
5235
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5236
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5237
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5238
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5239
5240
                                $overlap = $poly_results['both'];
5241
                                $poly_answer_area = $poly_results['s1'];
5242
                                $poly_user_area = $poly_results['s2'];
5243
                                $missing = $poly_results['s1Only'];
5244
                                $excess = $poly_results['s2Only'];
5245
5246
                                // //this is an area in pixels
5247
                                if ($debug > 0) {
5248
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5249
                                }
5250
5251
                                if ($overlap < 1) {
5252
                                    // Shortcut to avoid complicated calculations
5253
                                    $final_overlap = 0;
5254
                                    $final_missing = 100;
5255
                                    $final_excess = 100;
5256
                                } else {
5257
                                    // the final overlap is the percentage of the initial polygon
5258
                                    // that is overlapped by the user's polygon
5259
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5260
                                    if ($debug > 1) {
5261
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap, 0);
5262
                                    }
5263
                                    // the final missing area is the percentage of the initial polygon
5264
                                    // that is not overlapped by the user's polygon
5265
                                    $final_missing = 100 - $final_overlap;
5266
                                    if ($debug > 1) {
5267
                                        error_log(__LINE__.' - Final missing is '.$final_missing, 0);
5268
                                    }
5269
                                    // the final excess area is the percentage of the initial polygon's size
5270
                                    // that is covered by the user's polygon outside of the initial polygon
5271
                                    $final_excess = round(
5272
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5273
                                    );
5274
                                    if ($debug > 1) {
5275
                                        error_log(__LINE__.' - Final excess is '.$final_excess, 0);
5276
                                    }
5277
                                }
5278
5279
                                // Checking the destination parameters parsing the "@@"
5280
                                $destination_items = explode('@@', $answerDestination);
5281
                                $threadhold_total = $destination_items[0];
5282
                                $threadhold_items = explode(';', $threadhold_total);
5283
                                $threadhold1 = $threadhold_items[0]; // overlap
5284
                                $threadhold2 = $threadhold_items[1]; // excess
5285
                                $threadhold3 = $threadhold_items[2]; // missing
5286
5287
                                // if is delineation
5288
                                if (1 === $answerId) {
5289
                                    //setting colors
5290
                                    if ($final_overlap >= $threadhold1) {
5291
                                        $overlap_color = true;
5292
                                    }
5293
                                    if ($final_excess <= $threadhold2) {
5294
                                        $excess_color = true;
5295
                                    }
5296
                                    if ($final_missing <= $threadhold3) {
5297
                                        $missing_color = true;
5298
                                    }
5299
5300
                                    // if pass
5301
                                    if ($final_overlap >= $threadhold1 &&
5302
                                        $final_missing <= $threadhold3 &&
5303
                                        $final_excess <= $threadhold2
5304
                                    ) {
5305
                                        $next = 1; //go to the oars
5306
                                        $result_comment = get_lang('Acceptable');
5307
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5308
                                    } else {
5309
                                        $next = 0;
5310
                                        $result_comment = get_lang('Unacceptable');
5311
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5312
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5313
                                        // checking the destination parameters parsing the "@@"
5314
                                        $destination_items = explode('@@', $answerDestination);
5315
                                    }
5316
                                } elseif ($answerId > 1) {
5317
                                    if ('noerror' == $objAnswerTmp->selectHotspotType($answerId)) {
5318
                                        if ($debug > 0) {
5319
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5320
                                        }
5321
                                        //type no error shouldn't be treated
5322
                                        $next = 1;
5323
5324
                                        continue;
5325
                                    }
5326
                                    if ($debug > 0) {
5327
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5328
                                    }
5329
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5330
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5331
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5332
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5333
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5334
5335
                                    if (false == $overlap) {
5336
                                        //all good, no overlap
5337
                                        $next = 1;
5338
5339
                                        continue;
5340
                                    } else {
5341
                                        if ($debug > 0) {
5342
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5343
                                        }
5344
                                        $organs_at_risk_hit++;
5345
                                        //show the feedback
5346
                                        $next = 0;
5347
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5348
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5349
5350
                                        $destination_items = explode('@@', $answerDestination);
5351
                                        $try_hotspot = $destination_items[1];
5352
                                        $lp_hotspot = $destination_items[2];
5353
                                        $select_question_hotspot = $destination_items[3];
5354
                                        $url_hotspot = $destination_items[4];
5355
                                    }
5356
                                }
5357
                            } else {
5358
                                // the first delineation feedback
5359
                                if ($debug > 0) {
5360
                                    error_log(__LINE__.' first', 0);
5361
                                }
5362
                            }
5363
                        } elseif (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
5364
                            echo '<tr>';
5365
                            echo Display::tag('td', $answerMatching[$answerId]);
5366
                            echo Display::tag(
5367
                                'td',
5368
                                "$user_answer / ".Display::tag(
5369
                                    'strong',
5370
                                    $answerMatching[$answerCorrect],
5371
                                    ['style' => 'color: #008000; font-weight: bold;']
5372
                                )
5373
                            );
5374
                            echo '</tr>';
5375
                        } elseif (ANNOTATION == $answerType) {
5376
                            ExerciseShowFunctions::displayAnnotationAnswer(
5377
                                $feedback_type,
5378
                                $exeId,
5379
                                $questionId,
5380
                                $questionScore,
5381
                                $results_disabled
5382
                            );
5383
                        }
5384
                    }
5385
                } else {
5386
                    if ($debug) {
5387
                        error_log('Showing questions $from '.$from);
5388
                    }
5389
5390
                    switch ($answerType) {
5391
                        case UNIQUE_ANSWER:
5392
                        case UNIQUE_ANSWER_IMAGE:
5393
                        case UNIQUE_ANSWER_NO_OPTION:
5394
                        case MULTIPLE_ANSWER:
5395
                        case GLOBAL_MULTIPLE_ANSWER:
5396
                        case MULTIPLE_ANSWER_COMBINATION:
5397
                        case READING_COMPREHENSION:
5398
                            if (1 == $answerId) {
5399
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5400
                                    $this,
5401
                                    $feedback_type,
5402
                                    $answerType,
5403
                                    $studentChoice,
5404
                                    $answer,
5405
                                    $answerComment,
5406
                                    $answerCorrect,
5407
                                    $exeId,
5408
                                    $questionId,
5409
                                    $answerId,
5410
                                    $results_disabled,
5411
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5412
                                    $this->export
5413
                                );
5414
                            } else {
5415
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5416
                                    $this,
5417
                                    $feedback_type,
5418
                                    $answerType,
5419
                                    $studentChoice,
5420
                                    $answer,
5421
                                    $answerComment,
5422
                                    $answerCorrect,
5423
                                    $exeId,
5424
                                    $questionId,
5425
                                    '',
5426
                                    $results_disabled,
5427
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5428
                                    $this->export
5429
                                );
5430
                            }
5431
5432
                            break;
5433
                        case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
5434
                            if (1 == $answerId) {
5435
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5436
                                    $this,
5437
                                    $feedback_type,
5438
                                    $answerType,
5439
                                    $studentChoice,
5440
                                    $answer,
5441
                                    $answerComment,
5442
                                    $answerCorrect,
5443
                                    $exeId,
5444
                                    $questionId,
5445
                                    $answerId,
5446
                                    $results_disabled,
5447
                                    $showTotalScoreAndUserChoicesInLastAttempt
5448
                                );
5449
                            } else {
5450
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5451
                                    $this,
5452
                                    $feedback_type,
5453
                                    $answerType,
5454
                                    $studentChoice,
5455
                                    $answer,
5456
                                    $answerComment,
5457
                                    $answerCorrect,
5458
                                    $exeId,
5459
                                    $questionId,
5460
                                    '',
5461
                                    $results_disabled,
5462
                                    $showTotalScoreAndUserChoicesInLastAttempt
5463
                                );
5464
                            }
5465
5466
                            break;
5467
                        case MULTIPLE_ANSWER_TRUE_FALSE:
5468
                            if (1 == $answerId) {
5469
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5470
                                    $this,
5471
                                    $feedback_type,
5472
                                    $answerType,
5473
                                    $studentChoice,
5474
                                    $answer,
5475
                                    $answerComment,
5476
                                    $answerCorrect,
5477
                                    $exeId,
5478
                                    $questionId,
5479
                                    $answerId,
5480
                                    $results_disabled,
5481
                                    $showTotalScoreAndUserChoicesInLastAttempt
5482
                                );
5483
                            } else {
5484
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5485
                                    $this,
5486
                                    $feedback_type,
5487
                                    $answerType,
5488
                                    $studentChoice,
5489
                                    $answer,
5490
                                    $answerComment,
5491
                                    $answerCorrect,
5492
                                    $exeId,
5493
                                    $questionId,
5494
                                    '',
5495
                                    $results_disabled,
5496
                                    $showTotalScoreAndUserChoicesInLastAttempt
5497
                                );
5498
                            }
5499
5500
                            break;
5501
                        case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
5502
                            if (1 == $answerId) {
5503
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5504
                                    $this,
5505
                                    $feedback_type,
5506
                                    $studentChoice,
5507
                                    $studentChoiceDegree,
5508
                                    $answer,
5509
                                    $answerComment,
5510
                                    $answerCorrect,
5511
                                    $questionId,
5512
                                    $results_disabled
5513
                                );
5514
                            } else {
5515
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5516
                                    $this,
5517
                                    $feedback_type,
5518
                                    $studentChoice,
5519
                                    $studentChoiceDegree,
5520
                                    $answer,
5521
                                    $answerComment,
5522
                                    $answerCorrect,
5523
                                    $questionId,
5524
                                    $results_disabled
5525
                                );
5526
                            }
5527
5528
                            break;
5529
                        case FILL_IN_BLANKS:
5530
                        case FILL_IN_BLANKS_COMBINATION:
5531
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5532
                                $this,
5533
                                $feedback_type,
5534
                                $answer,
5535
                                $exeId,
5536
                                $questionId,
5537
                                $results_disabled,
5538
                                $showTotalScoreAndUserChoicesInLastAttempt,
5539
                                $str
5540
                            );
5541
                            break;
5542
                        case CALCULATED_ANSWER:
5543
                            ExerciseShowFunctions::display_calculated_answer(
5544
                                $this,
5545
                                $feedback_type,
5546
                                $answer,
5547
                                $exeId,
5548
                                $questionId,
5549
                                $results_disabled,
5550
                                '',
5551
                                $showTotalScoreAndUserChoicesInLastAttempt
5552
                            );
5553
5554
                            break;
5555
                        case FREE_ANSWER:
5556
                            echo ExerciseShowFunctions::display_free_answer(
5557
                                $feedback_type,
5558
                                $choice,
5559
                                $exeId,
5560
                                $questionId,
5561
                                $questionScore,
5562
                                $results_disabled
5563
                            );
5564
5565
                            break;
5566
                        case UPLOAD_ANSWER:
5567
                            echo ExerciseShowFunctions::displayUploadAnswer(
5568
                                $feedback_type,
5569
                                $choice,
5570
                                $exeId,
5571
                                $questionId,
5572
                                $questionScore,
5573
                                $results_disabled
5574
                            );
5575
                            break;
5576
                        case ORAL_EXPRESSION:
5577
                            /** @var OralExpression $objQuestionTmp */
5578
                            echo '<tr>
5579
                                <td valign="top">'.
5580
                                ExerciseShowFunctions::display_oral_expression_answer(
5581
                                    $feedback_type,
5582
                                    $choice,
5583
                                    $exeId,
5584
                                    $questionId,
5585
                                    $results_disabled,
5586
                                    $questionScore
5587
                                ).'</td>
5588
                                </tr>
5589
                                </table>';
5590
                            break;
5591
                        case HOT_SPOT:
5592
                        case HOT_SPOT_COMBINATION:
5593
                            $correctAnswerId = 0;
5594
5595
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5596
                                if ($hotspot->getHotspotAnswerId() == $foundAnswerId) {
5597
                                    break;
5598
                                }
5599
                            }
5600
                            ExerciseShowFunctions::display_hotspot_answer(
5601
                                $this,
5602
                                $feedback_type,
5603
                                $answerId,
5604
                                $answer,
5605
                                $studentChoice,
5606
                                $answerComment,
5607
                                $results_disabled,
5608
                                $answerId,
5609
                                $showTotalScoreAndUserChoicesInLastAttempt
5610
                            );
5611
5612
                            break;
5613
                        case HOT_SPOT_DELINEATION:
5614
                            $user_answer = $user_array;
5615
                            if ($next) {
5616
                                $user_answer = $user_array;
5617
                                // we compare only the delineation not the other points
5618
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5619
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5620
5621
                                // calculating the area
5622
                                $poly_user = convert_coordinates($user_answer, '/');
5623
                                $poly_answer = convert_coordinates($answer_question, '|');
5624
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5625
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5626
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5627
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5628
5629
                                $overlap = $poly_results['both'];
5630
                                $poly_answer_area = $poly_results['s1'];
5631
                                $poly_user_area = $poly_results['s2'];
5632
                                $missing = $poly_results['s1Only'];
5633
                                $excess = $poly_results['s2Only'];
5634
                                if ($debug > 0) {
5635
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5636
                                }
5637
                                if ($overlap < 1) {
5638
                                    //shortcut to avoid complicated calculations
5639
                                    $final_overlap = 0;
5640
                                    $final_missing = 100;
5641
                                    $final_excess = 100;
5642
                                } else {
5643
                                    // the final overlap is the percentage of the initial polygon
5644
                                    // that is overlapped by the user's polygon
5645
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5646
5647
                                    // the final missing area is the percentage of the initial polygon that
5648
                                    // is not overlapped by the user's polygon
5649
                                    $final_missing = 100 - $final_overlap;
5650
                                    // the final excess area is the percentage of the initial polygon's size that is
5651
                                    // covered by the user's polygon outside of the initial polygon
5652
                                    $final_excess = round(
5653
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5654
                                    );
5655
5656
                                    if ($debug > 1) {
5657
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap);
5658
                                        error_log(__LINE__.' - Final excess is '.$final_excess);
5659
                                        error_log(__LINE__.' - Final missing is '.$final_missing);
5660
                                    }
5661
                                }
5662
5663
                                // Checking the destination parameters parsing the "@@"
5664
                                $destination_items = explode('@@', $answerDestination);
5665
                                $threadhold_total = $destination_items[0];
5666
                                $threadhold_items = explode(';', $threadhold_total);
5667
                                $threadhold1 = $threadhold_items[0]; // overlap
5668
                                $threadhold2 = $threadhold_items[1]; // excess
5669
                                $threadhold3 = $threadhold_items[2]; //missing
5670
                                // if is delineation
5671
                                if (1 === $answerId) {
5672
                                    //setting colors
5673
                                    if ($final_overlap >= $threadhold1) {
5674
                                        $overlap_color = true;
5675
                                    }
5676
                                    if ($final_excess <= $threadhold2) {
5677
                                        $excess_color = true;
5678
                                    }
5679
                                    if ($final_missing <= $threadhold3) {
5680
                                        $missing_color = true;
5681
                                    }
5682
5683
                                    // if pass
5684
                                    if ($final_overlap >= $threadhold1 &&
5685
                                        $final_missing <= $threadhold3 &&
5686
                                        $final_excess <= $threadhold2
5687
                                    ) {
5688
                                        $next = 1; //go to the oars
5689
                                        $result_comment = get_lang('Acceptable');
5690
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5691
                                    } else {
5692
                                        $next = 0;
5693
                                        $result_comment = get_lang('Unacceptable');
5694
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5695
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5696
                                        //checking the destination parameters parsing the "@@"
5697
                                        $destination_items = explode('@@', $answerDestination);
5698
                                    }
5699
                                } elseif ($answerId > 1) {
5700
                                    if ('noerror' === $objAnswerTmp->selectHotspotType($answerId)) {
5701
                                        if ($debug > 0) {
5702
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5703
                                        }
5704
                                        //type no error shouldn't be treated
5705
                                        $next = 1;
5706
5707
                                        break;
5708
                                    }
5709
                                    if ($debug > 0) {
5710
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5711
                                    }
5712
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5713
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5714
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5715
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5716
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5717
5718
                                    if (false == $overlap) {
5719
                                        //all good, no overlap
5720
                                        $next = 1;
5721
5722
                                        break;
5723
                                    } else {
5724
                                        if ($debug > 0) {
5725
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5726
                                        }
5727
                                        $organs_at_risk_hit++;
5728
                                        //show the feedback
5729
                                        $next = 0;
5730
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5731
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5732
5733
                                        $destination_items = explode('@@', $answerDestination);
5734
                                        $try_hotspot = $destination_items[1];
5735
                                        $lp_hotspot = $destination_items[2];
5736
                                        $select_question_hotspot = $destination_items[3];
5737
                                        $url_hotspot = $destination_items[4];
5738
                                    }
5739
                                }
5740
                            }
5741
5742
                            break;
5743
                        case HOT_SPOT_ORDER:
5744
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5745
                                $feedback_type,
5746
                                $answerId,
5747
                                $answer,
5748
                                $studentChoice,
5749
                                $answerComment
5750
                            );*/
5751
5752
                            break;
5753
                        case DRAGGABLE:
5754
                        case MATCHING_DRAGGABLE:
5755
                        case MATCHING:
5756
                        case MATCHING_COMBINATION:
5757
                        case MATCHING_DRAGGABLE_COMBINATION:
5758
                            echo '<tr>';
5759
                            echo Display::tag('td', $answerMatching[$answerId]);
5760
                            echo Display::tag(
5761
                                'td',
5762
                                "$user_answer / ".Display::tag(
5763
                                    'strong',
5764
                                    $answerMatching[$answerCorrect],
5765
                                    ['style' => 'color: #008000; font-weight: bold;']
5766
                                )
5767
                            );
5768
                            echo '</tr>';
5769
5770
                            break;
5771
                        case ANNOTATION:
5772
                            ExerciseShowFunctions::displayAnnotationAnswer(
5773
                                $feedback_type,
5774
                                $exeId,
5775
                                $questionId,
5776
                                $questionScore,
5777
                                $results_disabled
5778
                            );
5779
5780
                            break;
5781
                    }
5782
                }
5783
            }
5784
        } // end for that loops over all answers of the current question
5785
5786
        if ($debug) {
5787
            error_log('-- End answer loop --');
5788
        }
5789
5790
        $final_answer = true;
5791
5792
        foreach ($real_answers as $my_answer) {
5793
            if (!$my_answer) {
5794
                $final_answer = false;
5795
            }
5796
        }
5797
5798
        if (FILL_IN_BLANKS_COMBINATION === $answerType) {
5799
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
5800
                $answerType,
5801
                $listCorrectAnswers,
5802
                $exeId,
5803
                $questionId,
5804
                $questionWeighting
5805
            );
5806
        }
5807
5808
        if (HOT_SPOT_COMBINATION === $answerType) {
5809
            $listCoords = $exerciseResultCoordinates[$questionId] ?? [];
5810
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
5811
                $answerType,
5812
                $listCoords,
5813
                $exeId,
5814
                $questionId,
5815
                $questionWeighting,
5816
                (array) ($choice ?? []),
5817
                $nbrAnswers
5818
            );
5819
        }
5820
5821
        if (in_array($answerType, [MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION], true)) {
5822
            $questionScore = ExerciseLib::getUserQuestionScoreGlobal(
5823
                $answerType,
5824
                $matchingCorrectAnswers[$questionId] ?? [],
5825
                $exeId,
5826
                $questionId,
5827
                $questionWeighting
5828
            );
5829
        }
5830
5831
        //we add the total score after dealing with the answers
5832
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
5833
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
5834
        ) {
5835
            if ($final_answer) {
5836
                //getting only the first score where we save the weight of all the question
5837
                $answerWeighting = $objAnswerTmp->selectWeighting(1);
5838
                if (empty($answerWeighting) && !empty($firstAnswer) && isset($firstAnswer['ponderation'])) {
5839
                    $answerWeighting = $firstAnswer['ponderation'];
5840
                }
5841
                $questionScore += $answerWeighting;
5842
            }
5843
        }
5844
5845
        $extra_data = [
5846
            'final_overlap' => $final_overlap,
5847
            'final_missing' => $final_missing,
5848
            'final_excess' => $final_excess,
5849
            'overlap_color' => $overlap_color,
5850
            'missing_color' => $missing_color,
5851
            'excess_color' => $excess_color,
5852
            'threadhold1' => $threadhold1,
5853
            'threadhold2' => $threadhold2,
5854
            'threadhold3' => $threadhold3,
5855
        ];
5856
5857
        if ('exercise_result' === $from) {
5858
            // if answer is hotspot. To the difference of exercise_show.php,
5859
            //  we use the results from the session (from_db=0)
5860
            // TODO Change this, because it is wrong to show the user
5861
            //  some results that haven't been stored in the database yet
5862
            if (in_array($answerType, [HOT_SPOT, HOT_SPOT_ORDER, HOT_SPOT_DELINEATION, HOT_SPOT_COMBINATION])) {
5863
                if ($debug) {
5864
                    error_log('$from AND this is a hotspot kind of question ');
5865
                }
5866
                if (HOT_SPOT_DELINEATION === $answerType) {
5867
                    if ($showHotSpotDelineationTable) {
5868
                        if (!is_numeric($final_overlap)) {
5869
                            $final_overlap = 0;
5870
                        }
5871
                        if (!is_numeric($final_missing)) {
5872
                            $final_missing = 0;
5873
                        }
5874
                        if (!is_numeric($final_excess)) {
5875
                            $final_excess = 0;
5876
                        }
5877
5878
                        if ($final_overlap > 100) {
5879
                            $final_overlap = 100;
5880
                        }
5881
5882
                        $overlap = 0;
5883
                        if ($final_overlap > 0) {
5884
                            $overlap = (int) $final_overlap;
5885
                        }
5886
5887
                        $excess = 0;
5888
                        if ($final_excess > 0) {
5889
                            $excess = (int) $final_excess;
5890
                        }
5891
5892
                        $missing = 0;
5893
                        if ($final_missing > 0) {
5894
                            $missing = (int) $final_missing;
5895
                        }
5896
5897
                        $table_resume = '<table class="table table-hover table-striped data_table">
5898
                                <tr class="row_odd" >
5899
                                    <td></td>
5900
                                    <td ><b>'.get_lang('Requirements').'</b></td>
5901
                                    <td><b>'.get_lang('Your answer').'</b></td>
5902
                                </tr>
5903
                                <tr class="row_even">
5904
                                    <td><b>'.get_lang('Overlapping area').'</b></td>
5905
                                    <td>'.get_lang('Minimum').' '.$threadhold1.'</td>
5906
                                    <td class="text-right '.($overlap_color ? 'text-success' : 'text-error').'">'
5907
                                    .$overlap.'</td>
5908
                                </tr>
5909
                                <tr>
5910
                                    <td><b>'.get_lang('Excessive area').'</b></td>
5911
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold2.'</td>
5912
                                    <td class="text-right '.($excess_color ? 'text-success' : 'text-error').'">'
5913
                                    .$excess.'</td>
5914
                                </tr>
5915
                                <tr class="row_even">
5916
                                    <td><b>'.get_lang('Missing area').'</b></td>
5917
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold3.'</td>
5918
                                    <td class="text-right '.($missing_color ? 'text-success' : 'text-error').'">'
5919
                                    .$missing.'</td>
5920
                                </tr>
5921
                            </table>';
5922
                        if (0 == $next) {
5923
                        } else {
5924
                            $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
5925
                            $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
5926
                        }
5927
5928
                        $message = '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>
5929
                                    <p style="text-align:center">';
5930
                        $message .= '<p>'.get_lang('Your delineation :').'</p>';
5931
                        $message .= $table_resume;
5932
                        $message .= '<br />'.get_lang('Your result is :').' '.$result_comment.'<br />';
5933
                        if ($organs_at_risk_hit > 0) {
5934
                            $message .= '<p><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
5935
                        }
5936
                        $message .= '<p>'.$comment.'</p>';
5937
                        echo $message;
5938
5939
                        $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][0] = $message;
5940
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
5941
                            $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][1] = $_SESSION['exerciseResultCoordinates'][$questionId];
5942
                        }
5943
                    } else {
5944
                        echo $hotspot_delineation_result[0] ?? '';
5945
                    }
5946
5947
                    // Save the score attempts
5948
                    if (1) {
5949
                        //getting the answer 1 or 0 comes from exercise_submit_modal.php
5950
                        $final_answer = $hotspot_delineation_result[1] ?? '';
5951
                        if (0 == $final_answer) {
5952
                            $questionScore = 0;
5953
                        }
5954
                        // we always insert the answer_id 1 = delineation
5955
                        Event::saveQuestionAttempt($this, $questionScore, 1, $quesId, $exeId, 0);
5956
                        //in delineation mode, get the answer from $hotspot_delineation_result[1]
5957
                        $hotspotValue = isset($hotspot_delineation_result[1]) ? 1 === (int) $hotspot_delineation_result[1] ? 1 : 0 : 0;
5958
                        Event::saveExerciseAttemptHotspot(
5959
                            $this,
5960
                            $exeId,
5961
                            $quesId,
5962
                            1,
5963
                            $hotspotValue,
5964
                            $exerciseResultCoordinates[$quesId] ?? '',
5965
                            false,
5966
                            0,
5967
                            $learnpath_id,
5968
                            $learnpath_item_id
5969
                        );
5970
                    } else {
5971
                        if (0 == $final_answer) {
5972
                            $questionScore = 0;
5973
                            $answer = 0;
5974
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
5975
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5976
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5977
                                    Event::saveExerciseAttemptHotspot(
5978
                                        $this,
5979
                                        $exeId,
5980
                                        $quesId,
5981
                                        $idx,
5982
                                        0,
5983
                                        $val,
5984
                                        false,
5985
                                        0,
5986
                                        $learnpath_id,
5987
                                        $learnpath_item_id
5988
                                    );
5989
                                }
5990
                            }
5991
                        } else {
5992
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
5993
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5994
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5995
                                    $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
5996
                                    Event::saveExerciseAttemptHotspot(
5997
                                        $this,
5998
                                        $exeId,
5999
                                        $quesId,
6000
                                        $idx,
6001
                                        $hotspotValue,
6002
                                        $val,
6003
                                        false,
6004
                                        0,
6005
                                        $learnpath_id,
6006
                                        $learnpath_item_id
6007
                                    );
6008
                                }
6009
                            }
6010
                        }
6011
                    }
6012
                }
6013
            }
6014
6015
            $relPath = api_get_path(WEB_CODE_PATH);
6016
6017
            if ($save_results
6018
                && in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION], true)
6019
                && !empty($exerciseResultCoordinates[$questionId])
6020
            ) {
6021
                Database::delete(
6022
                    Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
6023
                    [
6024
                        'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
6025
                            $exeId, $questionId, api_get_course_int_id(),
6026
                        ],
6027
                    ]
6028
                );
6029
6030
                foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
6031
                    $hotspotValue = !empty($choice[$idx]) ? 1 : 0;
6032
                    Event::saveExerciseAttemptHotspot(
6033
                        $this,
6034
                        $exeId,
6035
                        $quesId,
6036
                        $idx,
6037
                        $hotspotValue,
6038
                        $val,
6039
                        false,
6040
                        $this->id,
6041
                        $learnpath_id,
6042
                        $learnpath_item_id
6043
                    );
6044
                }
6045
                $hotspotsSavedEarly = true;
6046
            }
6047
6048
            if (in_array($answerType, [HOT_SPOT, HOT_SPOT_ORDER, HOT_SPOT_COMBINATION], true)) {
6049
                // We made an extra table for the answers
6050
                if ($show_result) {
6051
                    echo '</table></td></tr>';
6052
                    echo '
6053
                        <tr>
6054
                            <td colspan="2">
6055
                                <p><em>'.get_lang('Image zones')."</em></p>
6056
                                <div id=\"hotspot-solution-$questionId\"></div>
6057
                                <script>
6058
                                    $(function() {
6059
                                        new HotspotQuestion({
6060
                                            questionId: $questionId,
6061
                                            exerciseId: {$this->getId()},
6062
                                            exeId: $exeId,
6063
                                            selector: '#hotspot-solution-$questionId',
6064
                                            for: 'solution',
6065
                                            relPath: '$relPath'
6066
                                        });
6067
                                    });
6068
                                </script>
6069
                            </td>
6070
                        </tr>
6071
                    ";
6072
                }
6073
            } elseif (ANNOTATION == $answerType) {
6074
                if ($show_result) {
6075
                    echo '
6076
                        <p><em>'.get_lang('Annotation').'</em></p>
6077
                        <div id="annotation-canvas-'.$questionId.'"></div>
6078
                        <script>
6079
                            AnnotationQuestion({
6080
                                questionId: parseInt('.$questionId.'),
6081
                                exerciseId: parseInt('.$exeId.'),
6082
                                relPath: \''.$relPath.'\',
6083
                                courseId: parseInt('.$course_id.')
6084
                            });
6085
                        </script>
6086
                    ';
6087
                }
6088
            }
6089
6090
            if ($show_result && ANNOTATION != $answerType) {
6091
                echo '</table>';
6092
            }
6093
        }
6094
        unset($objAnswerTmp);
6095
6096
        $totalWeighting += $questionWeighting;
6097
        // Store results directly in the database
6098
        // For all in one page exercises, the results will be
6099
        // stored by exercise_results.php (using the session)
6100
        if (in_array(
6101
            $objQuestionTmp->type,
6102
            [PAGE_BREAK, MEDIA_QUESTION],
6103
            true
6104
        )) {
6105
            $save_results = false;
6106
        }
6107
        if ($save_results) {
6108
            if ($debug) {
6109
                error_log("Save question results $save_results");
6110
                error_log("Question score: $questionScore");
6111
                error_log('choice: ');
6112
                error_log(print_r($choice, 1));
6113
            }
6114
6115
            if (empty($choice)) {
6116
                $choice = 0;
6117
            }
6118
            // with certainty degree
6119
            if (empty($choiceDegreeCertainty)) {
6120
                $choiceDegreeCertainty = 0;
6121
            }
6122
            if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
6123
                MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType ||
6124
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
6125
            ) {
6126
                if (0 != $choice) {
6127
                    $reply = array_keys($choice);
6128
                    $countReply = count($reply);
6129
                    for ($i = 0; $i < $countReply; $i++) {
6130
                        $chosenAnswer = $reply[$i];
6131
                        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
6132
                            if (0 != $choiceDegreeCertainty) {
6133
                                $replyDegreeCertainty = array_keys($choiceDegreeCertainty);
6134
                                $answerDegreeCertainty = isset($replyDegreeCertainty[$i]) ? $replyDegreeCertainty[$i] : '';
6135
                                $answerValue = isset($choiceDegreeCertainty[$answerDegreeCertainty]) ? $choiceDegreeCertainty[$answerDegreeCertainty] : '';
6136
                                Event::saveQuestionAttempt(
6137
                                    $this,
6138
                                    $questionScore,
6139
                                    $chosenAnswer.':'.$choice[$chosenAnswer].':'.$answerValue,
6140
                                    $quesId,
6141
                                    $exeId,
6142
                                    $i,
6143
                                    $this->getId(),
6144
                                    $updateResults,
6145
                                    $questionDuration
6146
                                );
6147
                            }
6148
                        } else {
6149
                            Event::saveQuestionAttempt(
6150
                                $this,
6151
                                $questionScore,
6152
                                $chosenAnswer.':'.$choice[$chosenAnswer],
6153
                                $quesId,
6154
                                $exeId,
6155
                                $i,
6156
                                $this->getId(),
6157
                                $updateResults,
6158
                                $questionDuration
6159
                            );
6160
                        }
6161
                        if ($debug) {
6162
                            error_log('result =>'.$questionScore.' '.$chosenAnswer.':'.$choice[$chosenAnswer]);
6163
                        }
6164
                    }
6165
                } else {
6166
                    Event::saveQuestionAttempt(
6167
                        $this,
6168
                        $questionScore,
6169
                        0,
6170
                        $quesId,
6171
                        $exeId,
6172
                        0,
6173
                        $this->getId(),
6174
                        false,
6175
                        $questionDuration
6176
                    );
6177
                }
6178
            } elseif (in_array($answerType, [
6179
                MULTIPLE_ANSWER,
6180
                GLOBAL_MULTIPLE_ANSWER,
6181
                MULTIPLE_ANSWER_DROPDOWN,
6182
                MULTIPLE_ANSWER_DROPDOWN_COMBINATION
6183
            ], true)) {
6184
                if (0 != $choice) {
6185
                    $reply = in_array($answerType, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION], true)
6186
                        ? array_values((array) $choice)
6187
                        : array_keys((array) $choice);
6188
                    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...
6189
                        $ans = $reply[$i];
6190
                        Event::saveQuestionAttempt(
6191
                            $this,
6192
                            $questionScore,
6193
                            $ans,
6194
                            $quesId,
6195
                            $exeId,
6196
                            $i,
6197
                            $this->id,
6198
                            false,
6199
                            $questionDuration
6200
                        );
6201
                    }
6202
                } else {
6203
                    Event::saveQuestionAttempt(
6204
                        $this,
6205
                        $questionScore,
6206
                        0,
6207
                        $quesId,
6208
                        $exeId,
6209
                        0,
6210
                        $this->id,
6211
                        false,
6212
                        $questionDuration
6213
                    );
6214
                }
6215
            } elseif (MULTIPLE_ANSWER_COMBINATION == $answerType) {
6216
                if (0 != $choice) {
6217
                    $reply = array_keys($choice);
6218
                    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...
6219
                        $ans = $reply[$i];
6220
                        Event::saveQuestionAttempt(
6221
                            $this,
6222
                            $questionScore,
6223
                            $ans,
6224
                            $quesId,
6225
                            $exeId,
6226
                            $i,
6227
                            $this->id,
6228
                            false,
6229
                            $questionDuration
6230
                        );
6231
                    }
6232
                } else {
6233
                    Event::saveQuestionAttempt(
6234
                        $this,
6235
                        $questionScore,
6236
                        0,
6237
                        $quesId,
6238
                        $exeId,
6239
                        0,
6240
                        $this->id,
6241
                        false,
6242
                        $questionDuration
6243
                    );
6244
                }
6245
            } elseif (in_array($answerType, [
6246
                MATCHING,
6247
                DRAGGABLE,
6248
                MATCHING_DRAGGABLE,
6249
                MATCHING_COMBINATION,
6250
                MATCHING_DRAGGABLE_COMBINATION
6251
            ], true)) {
6252
                if (isset($matching)) {
6253
                    foreach ($matching as $j => $val) {
6254
                        Event::saveQuestionAttempt(
6255
                            $this,
6256
                            $questionScore,
6257
                            $val,
6258
                            $quesId,
6259
                            $exeId,
6260
                            $j,
6261
                            $this->id,
6262
                            false,
6263
                            $questionDuration
6264
                        );
6265
                    }
6266
                }
6267
            } elseif (FREE_ANSWER == $answerType) {
6268
                $answer = $choice;
6269
                Event::saveQuestionAttempt(
6270
                    $this,
6271
                    $questionScore,
6272
                    $answer,
6273
                    $quesId,
6274
                    $exeId,
6275
                    0,
6276
                    $this->id,
6277
                    false,
6278
                    $questionDuration
6279
                );
6280
            } elseif ($answerType == UPLOAD_ANSWER) {
6281
                $answer = '';
6282
                $questionAttemptId = Event::saveQuestionAttempt(
6283
                    $this,
6284
                    0,
6285
                    $answer,
6286
                    $quesId,
6287
                    $exeId,
6288
                    0,
6289
                    $this->id,
6290
                    false,
6291
                    $questionDuration
6292
                );
6293
6294
                if (false !== $questionAttemptId) {
6295
                    $postedAssets = isset($_REQUEST['uploadAsset'][$questionId])
6296
                        ? (array) $_REQUEST['uploadAsset'][$questionId]
6297
                        : [];
6298
                    UploadAnswer::saveAssetInQuestionAttempt($questionAttemptId, $postedAssets);
6299
                }
6300
            } elseif (ORAL_EXPRESSION == $answerType) {
6301
                $answer = $choice;
6302
                /** @var OralExpression $objQuestionTmp */
6303
                $questionAttemptId = Event::saveQuestionAttempt(
6304
                    $this,
6305
                    $questionScore,
6306
                    $answer,
6307
                    $quesId,
6308
                    $exeId,
6309
                    0,
6310
                    $this->id,
6311
                    false,
6312
                    $questionDuration
6313
                );
6314
6315
                if (false !== $questionAttemptId) {
6316
                    OralExpression::saveAssetInQuestionAttempt($questionAttemptId);
6317
                }
6318
            } elseif (
6319
            in_array(
6320
                $answerType,
6321
                [UNIQUE_ANSWER, UNIQUE_ANSWER_IMAGE, UNIQUE_ANSWER_NO_OPTION, READING_COMPREHENSION]
6322
            )
6323
            ) {
6324
                $answer = $choice;
6325
                Event::saveQuestionAttempt(
6326
                    $this,
6327
                    $questionScore,
6328
                    $answer,
6329
                    $quesId,
6330
                    $exeId,
6331
                    0,
6332
                    $this->id,
6333
                    false,
6334
                    $questionDuration
6335
                );
6336
            } elseif (in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION, ANNOTATION], true)) {
6337
                $answer = [];
6338
                if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) {
6339
                    if ($debug) {
6340
                        error_log('Checking result coordinates');
6341
                    }
6342
                    Database::delete(
6343
                        Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
6344
                        [
6345
                            'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
6346
                                $exeId,
6347
                                $questionId,
6348
                                api_get_course_int_id(),
6349
                            ],
6350
                        ]
6351
                    );
6352
6353
                    foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
6354
                        $answer[] = $val;
6355
                        $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
6356
                        if ($debug) {
6357
                            error_log('Hotspot value: '.$hotspotValue);
6358
                        }
6359
                        Event::saveExerciseAttemptHotspot(
6360
                            $this,
6361
                            $exeId,
6362
                            $quesId,
6363
                            $idx,
6364
                            $hotspotValue,
6365
                            $val,
6366
                            false,
6367
                            $this->id,
6368
                            $learnpath_id,
6369
                            $learnpath_item_id
6370
                        );
6371
                    }
6372
                } else {
6373
                    if ($debug) {
6374
                        error_log('Empty: exerciseResultCoordinates');
6375
                    }
6376
                }
6377
                Event::saveQuestionAttempt(
6378
                    $this,
6379
                    $questionScore,
6380
                    implode('|', $answer),
6381
                    $quesId,
6382
                    $exeId,
6383
                    0,
6384
                    $this->id,
6385
                    false,
6386
                    $questionDuration
6387
                );
6388
            } else {
6389
                Event::saveQuestionAttempt(
6390
                    $this,
6391
                    $questionScore,
6392
                    $answer,
6393
                    $quesId,
6394
                    $exeId,
6395
                    0,
6396
                    $this->id,
6397
                    false,
6398
                    $questionDuration
6399
                );
6400
            }
6401
        }
6402
6403
        if (0 == $propagate_neg && $questionScore < 0) {
6404
            $questionScore = 0;
6405
        }
6406
6407
        if ($save_results) {
6408
            $statsTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6409
            $sql = "UPDATE $statsTable SET
6410
                        score = score + ".(float) $questionScore."
6411
                    WHERE exe_id = $exeId";
6412
            Database::query($sql);
6413
        }
6414
6415
        return [
6416
            'score' => $questionScore,
6417
            'weight' => $questionWeighting,
6418
            'extra' => $extra_data,
6419
            'open_question' => $arrques,
6420
            'open_answer' => $arrans,
6421
            'answer_type' => $answerType,
6422
            'generated_oral_file' => $generatedFilesHtml,
6423
            'user_answered' => $userAnsweredQuestion,
6424
            'correct_answer_id' => $correctAnswerId,
6425
            'answer_destination' => $answerDestination,
6426
        ];
6427
    }
6428
6429
    /**
6430
     * Sends a notification when a user ends an examn.
6431
     *
6432
     * @param string $type                  'start' or 'end' of an exercise
6433
     * @param array  $question_list_answers
6434
     * @param string $origin
6435
     * @param int    $exe_id
6436
     * @param float  $score
6437
     * @param float  $weight
6438
     *
6439
     * @return bool
6440
     */
6441
    public function send_mail_notification_for_exam(
6442
        $type,
6443
        $question_list_answers,
6444
        $origin,
6445
        $exe_id,
6446
        $score = null,
6447
        $weight = null
6448
    ) {
6449
        $setting = api_get_course_setting('email_alert_manager_on_new_quiz');
6450
6451
        if ((empty($setting) || !is_array($setting)) && empty($this->getNotifications())) {
6452
            return false;
6453
        }
6454
6455
        $settingFromExercise = $this->getNotifications();
6456
        if (!empty($settingFromExercise)) {
6457
            $setting = $settingFromExercise;
6458
        }
6459
6460
        // Email configuration settings
6461
        $courseCode = api_get_course_id();
6462
        $courseInfo = api_get_course_info($courseCode);
6463
6464
        if (empty($courseInfo)) {
6465
            return false;
6466
        }
6467
6468
        $sessionId = api_get_session_id();
6469
6470
        $sessionData = '';
6471
        if (!empty($sessionId)) {
6472
            $sessionInfo = api_get_session_info($sessionId);
6473
            if (!empty($sessionInfo)) {
6474
                $sessionData = '<tr>'
6475
                    .'<td>'.get_lang('Session name').'</td>'
6476
                    .'<td>'.$sessionInfo['name'].'</td>'
6477
                    .'</tr>';
6478
            }
6479
        }
6480
6481
        $sendStart = false;
6482
        $sendEnd = false;
6483
        $sendEndOpenQuestion = false;
6484
        $sendEndOralQuestion = false;
6485
6486
        foreach ($setting as $option) {
6487
            switch ($option) {
6488
                case 0:
6489
                    return false;
6490
6491
                    break;
6492
                case 1: // End
6493
                    if ('end' == $type) {
6494
                        $sendEnd = true;
6495
                    }
6496
6497
                    break;
6498
                case 2: // start
6499
                    if ('start' == $type) {
6500
                        $sendStart = true;
6501
                    }
6502
6503
                    break;
6504
                case 3: // end + open
6505
                    if ('end' == $type) {
6506
                        $sendEndOpenQuestion = true;
6507
                    }
6508
6509
                    break;
6510
                case 4: // end + oral
6511
                    if ('end' == $type) {
6512
                        $sendEndOralQuestion = true;
6513
                    }
6514
6515
                    break;
6516
            }
6517
        }
6518
6519
        $user_info = api_get_user_info(api_get_user_id());
6520
        $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_show.php?'.
6521
            api_get_cidreq(true, true, 'qualify').'&id='.$exe_id.'&action=qualify';
6522
6523
        if (!empty($sessionId)) {
6524
            $addGeneralCoach = true;
6525
            $setting = ('true' === api_get_setting('exercise.block_quiz_mail_notification_general_coach'));
6526
            if (true === $setting) {
6527
                $addGeneralCoach = false;
6528
            }
6529
            $teachers = CourseManager::get_coach_list_from_course_code(
6530
                $courseCode,
6531
                $sessionId,
6532
                $addGeneralCoach
6533
            );
6534
        } else {
6535
            $teachers = CourseManager::get_teacher_list_from_course_code($courseCode);
6536
        }
6537
6538
        if ($sendEndOpenQuestion) {
6539
            $this->sendNotificationForOpenQuestions(
6540
                $question_list_answers,
6541
                $origin,
6542
                $user_info,
6543
                $url,
6544
                $teachers
6545
            );
6546
        }
6547
6548
        if ($sendEndOralQuestion) {
6549
            $this->sendNotificationForOralQuestions(
6550
                $question_list_answers,
6551
                $origin,
6552
                $exe_id,
6553
                $user_info,
6554
                $url,
6555
                $teachers
6556
            );
6557
        }
6558
6559
        if (!$sendEnd && !$sendStart) {
6560
            return false;
6561
        }
6562
6563
        $scoreLabel = '';
6564
        if ($sendEnd &&
6565
            ('true' === api_get_setting('exercise.send_score_in_exam_notification_mail_to_manager'))
6566
        ) {
6567
            $notificationPercentage = ('true' === api_get_setting('mail.send_notification_score_in_percentage'));
6568
            $scoreLabel = ExerciseLib::show_score($score, $weight, $notificationPercentage, true);
6569
            $scoreLabel = '<tr>
6570
                            <td>'.get_lang('Score')."</td>
6571
                            <td>&nbsp;$scoreLabel</td>
6572
                        </tr>";
6573
        }
6574
6575
        if ($sendEnd) {
6576
            $msg = get_lang('A learner attempted an exercise').'<br /><br />';
6577
        } else {
6578
            $msg = get_lang('Student just started an exercise').'<br /><br />';
6579
        }
6580
6581
        $msg .= get_lang('Attempt details').' : <br /><br />
6582
                    <table>
6583
                        <tr>
6584
                            <td>'.get_lang('Course name').'</td>
6585
                            <td>#course#</td>
6586
                        </tr>
6587
                        '.$sessionData.'
6588
                        <tr>
6589
                            <td>'.get_lang('Test').'</td>
6590
                            <td>&nbsp;#exercise#</td>
6591
                        </tr>
6592
                        <tr>
6593
                            <td>'.get_lang('Learner name').'</td>
6594
                            <td>&nbsp;#student_complete_name#</td>
6595
                        </tr>
6596
                        <tr>
6597
                            <td>'.get_lang('Learner e-mail').'</td>
6598
                            <td>&nbsp;#email#</td>
6599
                        </tr>
6600
                        '.$scoreLabel.'
6601
                    </table>';
6602
6603
        $variables = [
6604
            '#email#' => $user_info['email'],
6605
            '#exercise#' => $this->exercise,
6606
            '#student_complete_name#' => $user_info['complete_name'],
6607
            '#course#' => Display::url(
6608
                $courseInfo['title'],
6609
                $courseInfo['course_public_url'].'?sid='.$sessionId
6610
            ),
6611
        ];
6612
6613
        if ($sendEnd) {
6614
            $msg .= '<br /><a href="#url#">'.get_lang(
6615
                    'Click this link to check the answer and/or give feedback'
6616
                ).'</a>';
6617
            $variables['#url#'] = $url;
6618
        }
6619
6620
        $content = str_replace(array_keys($variables), array_values($variables), $msg);
6621
6622
        if ($sendEnd) {
6623
            $subject = get_lang('A learner attempted an exercise');
6624
        } else {
6625
            $subject = get_lang('Student just started an exercise');
6626
        }
6627
6628
        if (!empty($teachers)) {
6629
            foreach ($teachers as $user_id => $teacher_data) {
6630
                MessageManager::send_message_simple(
6631
                    $user_id,
6632
                    $subject,
6633
                    $content
6634
                );
6635
            }
6636
        }
6637
    }
6638
6639
    /**
6640
     * @param array $user_data         result of api_get_user_info()
6641
     * @param array $trackExerciseInfo result of get_stat_track_exercise_info
6642
     * @param bool  $saveUserResult
6643
     * @param bool  $allowSignature
6644
     * @param bool  $allowExportPdf
6645
     *
6646
     * @return string
6647
     */
6648
    public function showExerciseResultHeader(
6649
        $user_data,
6650
        $trackExerciseInfo,
6651
        $saveUserResult,
6652
        $allowSignature = false,
6653
        $allowExportPdf = false
6654
    ) {
6655
        if ('true' === api_get_setting('exercise.hide_user_info_in_quiz_result')) {
6656
            return '';
6657
        }
6658
6659
        $start_date = null;
6660
        if (isset($trackExerciseInfo['start_date'])) {
6661
            $start_date = api_convert_and_format_date($trackExerciseInfo['start_date']);
6662
        }
6663
        $duration = isset($trackExerciseInfo['duration_formatted']) ? $trackExerciseInfo['duration_formatted'] : null;
6664
        $ip = isset($trackExerciseInfo['user_ip']) ? $trackExerciseInfo['user_ip'] : null;
6665
6666
        if (!empty($user_data)) {
6667
            $userFullName = $user_data['complete_name'];
6668
            if (api_is_teacher() || api_is_platform_admin(true, true)) {
6669
                $userFullName = '<a href="'.$user_data['profile_url'].'" title="'.get_lang('Go to learner details').'">'.
6670
                    $user_data['complete_name'].'</a>';
6671
            }
6672
6673
            $data = [
6674
                'name_url' => $userFullName,
6675
                'complete_name' => $user_data['complete_name'],
6676
                'username' => $user_data['username'],
6677
                'avatar' => $user_data['avatar_medium'],
6678
                'url' => $user_data['profile_url'],
6679
            ];
6680
6681
            if (!empty($user_data['official_code'])) {
6682
                $data['code'] = $user_data['official_code'];
6683
            }
6684
        }
6685
        // Description can be very long and is generally meant to explain
6686
        //   rules *before* the exam. Leaving here to make display easier if
6687
        //   necessary
6688
        /*
6689
        if (!empty($this->description)) {
6690
            $array[] = array('title' => get_lang("Description"), 'content' => $this->description);
6691
        }
6692
        */
6693
6694
        $data['start_date'] = $start_date;
6695
        $data['duration'] = $duration;
6696
        $data['ip'] = $ip;
6697
6698
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
6699
            $data['title'] = $this->get_formated_title().get_lang('Result');
6700
        } else {
6701
            $data['title'] = PHP_EOL.$this->exercise.' : '.get_lang('Result');
6702
        }
6703
6704
        $questionsCount = count(explode(',', $trackExerciseInfo['data_tracking']));
6705
        $savedAnswersCount = $this->countUserAnswersSavedInExercise($trackExerciseInfo['exe_id']);
6706
6707
        $data['number_of_answers'] = $questionsCount;
6708
        $data['number_of_answers_saved'] = $savedAnswersCount;
6709
        $exeId = $trackExerciseInfo['exe_id'];
6710
6711
        if ('true' === api_get_setting('exercise.quiz_confirm_saved_answers')) {
6712
            $em = Database::getManager();
6713
6714
            if ($saveUserResult) {
6715
                $trackConfirmation = new TrackEExerciseConfirmation();
6716
                $trackConfirmation
6717
                    ->setUser(api_get_user_entity($trackExerciseInfo['exe_user_id']))
6718
                    ->setQuizId($trackExerciseInfo['exe_exo_id'])
6719
                    ->setAttemptId($trackExerciseInfo['exe_id'])
6720
                    ->setQuestionsCount($questionsCount)
6721
                    ->setSavedAnswersCount($savedAnswersCount)
6722
                    ->setCourseId($trackExerciseInfo['c_id'])
6723
                    ->setSessionId($trackExerciseInfo['session_id'])
6724
                    ->setCreatedAt(api_get_utc_datetime(null, false, true));
6725
6726
                $em->persist($trackConfirmation);
6727
                $em->flush();
6728
            } else {
6729
                $trackConfirmation = $em
6730
                    ->getRepository(TrackEExerciseConfirmation::class)
6731
                    ->findOneBy(
6732
                        [
6733
                            'attemptId' => $trackExerciseInfo['exe_id'],
6734
                            'quizId' => $trackExerciseInfo['exe_exo_id'],
6735
                            'courseId' => $trackExerciseInfo['c_id'],
6736
                            'sessionId' => $trackExerciseInfo['session_id'],
6737
                        ]
6738
                    );
6739
            }
6740
6741
            $data['track_confirmation'] = $trackConfirmation;
6742
        }
6743
6744
        $signature = '';
6745
        if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($this)) {
6746
            $signature = ExerciseSignaturePlugin::getSignature($trackExerciseInfo['exe_user_id'], $trackExerciseInfo);
6747
        }
6748
        $tpl = new Template(null, false, false, false, false, false, false);
6749
        $tpl->assign('data', $data);
6750
        $tpl->assign('allow_signature', $allowSignature);
6751
        $tpl->assign('signature', $signature);
6752
        $tpl->assign('allow_export_pdf', $allowExportPdf);
6753
        $tpl->assign(
6754
            'export_url',
6755
            api_get_path(WEB_CODE_PATH).'exercise/result.php?action=export&id='.$exeId.'&'.api_get_cidreq()
6756
        );
6757
        $layoutTemplate = $tpl->get_template('exercise/partials/result_exercise.tpl');
6758
6759
        return $tpl->fetch($layoutTemplate);
6760
    }
6761
6762
    /**
6763
     * Returns the exercise result.
6764
     *
6765
     * @param int        attempt id
6766
     *
6767
     * @return array
6768
     */
6769
    public function get_exercise_result($exe_id)
6770
    {
6771
        $result = [];
6772
        $track_exercise_info = ExerciseLib::get_exercise_track_exercise_info($exe_id);
6773
6774
        if (!empty($track_exercise_info)) {
6775
            $totalScore = 0;
6776
            $objExercise = new self();
6777
            $objExercise->read($track_exercise_info['exe_exo_id']);
6778
            if (!empty($track_exercise_info['data_tracking'])) {
6779
                $question_list = explode(',', $track_exercise_info['data_tracking']);
6780
            }
6781
            foreach ($question_list as $questionId) {
6782
                $question_result = $objExercise->manage_answer(
6783
                    $exe_id,
6784
                    $questionId,
6785
                    '',
6786
                    'exercise_show',
6787
                    [],
6788
                    false,
6789
                    true,
6790
                    false,
6791
                    $objExercise->selectPropagateNeg()
6792
                );
6793
                $totalScore += $question_result['score'];
6794
            }
6795
6796
            if (0 == $objExercise->selectPropagateNeg() && $totalScore < 0) {
6797
                $totalScore = 0;
6798
            }
6799
            $result = [
6800
                'score' => $totalScore,
6801
                'weight' => $track_exercise_info['max_score'],
6802
            ];
6803
        }
6804
6805
        return $result;
6806
    }
6807
6808
    /**
6809
     * Checks if the exercise is visible due a lot of conditions
6810
     * visibility, time limits, student attempts
6811
     * Return associative array
6812
     * value : true if exercise visible
6813
     * message : HTML formatted message
6814
     * rawMessage : text message.
6815
     *
6816
     * @param int  $lpId
6817
     * @param int  $lpItemId
6818
     * @param int  $lpItemViewId
6819
     * @param bool $filterByAdmin
6820
     *
6821
     * @return array
6822
     */
6823
    public function is_visible(
6824
        $lpId = 0,
6825
        $lpItemId = 0,
6826
        $lpItemViewId = 0,
6827
        $filterByAdmin = true
6828
    ) {
6829
        // 1. By default the exercise is visible
6830
        $isVisible = true;
6831
        $message = null;
6832
6833
        // 1.1 Admins and teachers can access to the exercise
6834
        if ($filterByAdmin) {
6835
            if (api_is_platform_admin() || api_is_course_admin() || api_is_course_tutor()) {
6836
                return ['value' => true, 'message' => ''];
6837
            }
6838
        }
6839
6840
        // Deleted exercise.
6841
        if (-1 == $this->active) {
6842
            return [
6843
                'value' => false,
6844
                'message' => Display::return_message(
6845
                    get_lang('Test not found or not visible'),
6846
                    'warning',
6847
                    false
6848
                ),
6849
                'rawMessage' => get_lang('Test not found or not visible'),
6850
            ];
6851
        }
6852
6853
        $repo = Container::getQuizRepository();
6854
        $exercise = $repo->find($this->iId);
6855
6856
        if (null === $exercise) {
6857
            return [];
6858
        }
6859
6860
        $course = api_get_course_entity($this->course_id);
6861
        $link = $exercise->getFirstResourceLinkFromCourseSession($course);
6862
6863
        if ($link && $link->isDraft()) {
6864
            $this->active = 0;
6865
        }
6866
6867
        // 2. If the exercise is not active.
6868
        if (empty($lpId)) {
6869
            // 2.1 LP is OFF
6870
            if (0 == $this->active) {
6871
                return [
6872
                    'value' => false,
6873
                    'message' => Display::return_message(
6874
                        get_lang('Test not found or not visible'),
6875
                        'warning',
6876
                        false
6877
                    ),
6878
                    'rawMessage' => get_lang('Test not found or not visible'),
6879
                ];
6880
            }
6881
        } else {
6882
            $lp = Container::getLpRepository()->find($lpId);
6883
            // 2.1 LP is loaded
6884
            if ($lp && 0 == $this->active &&
6885
                !learnpath::is_lp_visible_for_student($lp, api_get_user_id(), $course)
6886
            ) {
6887
                return [
6888
                    'value' => false,
6889
                    'message' => Display::return_message(
6890
                        get_lang('Test not found or not visible'),
6891
                        'warning',
6892
                        false
6893
                    ),
6894
                    'rawMessage' => get_lang('Test not found or not visible'),
6895
                ];
6896
            }
6897
        }
6898
6899
        // 3. We check if the time limits are on
6900
        $limitTimeExists = false;
6901
        if (!empty($this->start_time) || !empty($this->end_time)) {
6902
            $limitTimeExists = true;
6903
        }
6904
6905
        if ($limitTimeExists) {
6906
            $timeNow = time();
6907
            $existsStartDate = false;
6908
            $nowIsAfterStartDate = true;
6909
            $existsEndDate = false;
6910
            $nowIsBeforeEndDate = true;
6911
6912
            if (!empty($this->start_time)) {
6913
                $existsStartDate = true;
6914
            }
6915
6916
            if (!empty($this->end_time)) {
6917
                $existsEndDate = true;
6918
            }
6919
6920
            // check if we are before-or-after end-or-start date
6921
            if ($existsStartDate && $timeNow < api_strtotime($this->start_time, 'UTC')) {
6922
                $nowIsAfterStartDate = false;
6923
            }
6924
6925
            if ($existsEndDate & $timeNow >= api_strtotime($this->end_time, 'UTC')) {
6926
                $nowIsBeforeEndDate = false;
6927
            }
6928
6929
            // lets check all cases
6930
            if ($existsStartDate && !$existsEndDate) {
6931
                // exists start date and dont exists end date
6932
                if ($nowIsAfterStartDate) {
6933
                    // after start date, no end date
6934
                    $isVisible = true;
6935
                    $message = sprintf(
6936
                        get_lang('Exercise available since %s'),
6937
                        api_convert_and_format_date($this->start_time)
6938
                    );
6939
                } else {
6940
                    // before start date, no end date
6941
                    $isVisible = false;
6942
                    $message = sprintf(
6943
                        get_lang('Exercise available from %s'),
6944
                        api_convert_and_format_date($this->start_time)
6945
                    );
6946
                }
6947
            } elseif (!$existsStartDate && $existsEndDate) {
6948
                // doesnt exist start date, exists end date
6949
                if ($nowIsBeforeEndDate) {
6950
                    // before end date, no start date
6951
                    $isVisible = true;
6952
                    $message = sprintf(
6953
                        get_lang('Exercise available until %s'),
6954
                        api_convert_and_format_date($this->end_time)
6955
                    );
6956
                } else {
6957
                    // after end date, no start date
6958
                    $isVisible = false;
6959
                    $message = sprintf(
6960
                        get_lang('Exercise available until %s'),
6961
                        api_convert_and_format_date($this->end_time)
6962
                    );
6963
                }
6964
            } elseif ($existsStartDate && $existsEndDate) {
6965
                // exists start date and end date
6966
                if ($nowIsAfterStartDate) {
6967
                    if ($nowIsBeforeEndDate) {
6968
                        // after start date and before end date
6969
                        $isVisible = true;
6970
                        $message = sprintf(
6971
                            get_lang('Exercise was activated from %s to %s'),
6972
                            api_convert_and_format_date($this->start_time),
6973
                            api_convert_and_format_date($this->end_time)
6974
                        );
6975
                    } else {
6976
                        // after start date and after end date
6977
                        $isVisible = false;
6978
                        $message = sprintf(
6979
                            get_lang('Exercise was activated from %s to %s'),
6980
                            api_convert_and_format_date($this->start_time),
6981
                            api_convert_and_format_date($this->end_time)
6982
                        );
6983
                    }
6984
                } else {
6985
                    if ($nowIsBeforeEndDate) {
6986
                        // before start date and before end date
6987
                        $isVisible = false;
6988
                        $message = sprintf(
6989
                            get_lang('Exercise will be activated from %s to %s'),
6990
                            api_convert_and_format_date($this->start_time),
6991
                            api_convert_and_format_date($this->end_time)
6992
                        );
6993
                    }
6994
                    // case before start date and after end date is impossible
6995
                }
6996
            } elseif (!$existsStartDate && !$existsEndDate) {
6997
                // doesnt exist start date nor end date
6998
                $isVisible = true;
6999
                $message = '';
7000
            }
7001
        }
7002
7003
        // 4. We check if the student have attempts
7004
        if ($isVisible) {
7005
            $exerciseAttempts = $this->selectAttempts();
7006
7007
            if ($exerciseAttempts > 0) {
7008
                $attemptCount = Event::get_attempt_count(
7009
                    api_get_user_id(),
7010
                    $this->getId(),
7011
                    (int) $lpId,
7012
                    (int) $lpItemId,
7013
                    (int) $lpItemViewId
7014
                );
7015
7016
                if ($attemptCount >= $exerciseAttempts) {
7017
                    $message = sprintf(
7018
                        get_lang('You cannot take test <b>%s</b> because you have already reached the maximum of %s attempts.'),
7019
                        $this->name,
7020
                        $exerciseAttempts
7021
                    );
7022
                    $isVisible = false;
7023
                } else {
7024
                    // Check blocking exercise.
7025
                    $extraFieldValue = new ExtraFieldValue('exercise');
7026
                    $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
7027
                        $this->iId,
7028
                        'blocking_percentage'
7029
                    );
7030
                    if ($blockExercise && isset($blockExercise['value']) && !empty($blockExercise['value'])) {
7031
                        $blockPercentage = (int) $blockExercise['value'];
7032
                        $userAttempts = Event::getExerciseResultsByUser(
7033
                            api_get_user_id(),
7034
                            $this->iId,
7035
                            $this->course_id,
7036
                            $this->sessionId,
7037
                            $lpId,
7038
                            $lpItemId
7039
                        );
7040
7041
                        if (!empty($userAttempts)) {
7042
                            $currentAttempt = current($userAttempts);
7043
                            if ($currentAttempt['total_percentage'] <= $blockPercentage) {
7044
                                $message = sprintf(
7045
                                    get_lang('All attempts blocked because you did not reach the minimum score of %s % at one of your attempts.'),
7046
                                    $blockPercentage
7047
                                );
7048
                                $isVisible = false;
7049
                            }
7050
                        }
7051
                    }
7052
                }
7053
            }
7054
        }
7055
7056
        $rawMessage = '';
7057
        if (!empty($message)) {
7058
            $rawMessage = $message;
7059
            $message = Display::return_message($message, 'warning', false);
7060
        }
7061
7062
        return [
7063
            'value' => $isVisible,
7064
            'message' => $message,
7065
            'rawMessage' => $rawMessage,
7066
        ];
7067
    }
7068
7069
    /**
7070
     * @return bool
7071
     */
7072
    public function added_in_lp()
7073
    {
7074
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
7075
        $sql = "SELECT max_score FROM $TBL_LP_ITEM
7076
                WHERE
7077
                    item_type = '".TOOL_QUIZ."' AND
7078
                    path = '{$this->getId()}'";
7079
        $result = Database::query($sql);
7080
        if (Database::num_rows($result) > 0) {
7081
            return true;
7082
        }
7083
7084
        return false;
7085
    }
7086
7087
    /**
7088
     * Returns an array with this form.
7089
     *
7090
     * @return array
7091
     *
7092
     * @example
7093
     * <code>
7094
     * array (size=3)
7095
     * 999 =>
7096
     * array (size=3)
7097
     * 0 => int 3422
7098
     * 1 => int 3423
7099
     * 2 => int 3424
7100
     * 100 =>
7101
     * array (size=2)
7102
     * 0 => int 3469
7103
     * 1 => int 3470
7104
     * 101 =>
7105
     * array (size=1)
7106
     * 0 => int 3482
7107
     * </code>
7108
     * The array inside the key 999 means the question list that belongs to the media id = 999,
7109
     * this case is special because 999 means "no media".
7110
     */
7111
    public function getMediaList()
7112
    {
7113
        return $this->mediaList;
7114
    }
7115
7116
    /**
7117
     * Is media question activated?
7118
     *
7119
     * @return bool
7120
     */
7121
    public function mediaIsActivated()
7122
    {
7123
        $mediaQuestions = $this->getMediaList();
7124
        $active = false;
7125
        if (isset($mediaQuestions) && !empty($mediaQuestions)) {
7126
            $media_count = count($mediaQuestions);
7127
            if ($media_count > 1) {
7128
                return true;
7129
            } elseif (1 == $media_count) {
7130
                if (isset($mediaQuestions[999])) {
7131
                    return false;
7132
                } else {
7133
                    return true;
7134
                }
7135
            }
7136
        }
7137
7138
        return $active;
7139
    }
7140
7141
    /**
7142
     * Gets question list from the exercise.
7143
     *
7144
     * @return array
7145
     */
7146
    public function getQuestionList()
7147
    {
7148
        return $this->questionList;
7149
    }
7150
7151
    /**
7152
     * Question list with medias compressed like this.
7153
     *
7154
     * @return array
7155
     *
7156
     * @example
7157
     *      <code>
7158
     *      array(
7159
     *      question_id_1,
7160
     *      question_id_2,
7161
     *      media_id, <- this media id contains question ids
7162
     *      question_id_3,
7163
     *      )
7164
     *      </code>
7165
     */
7166
    public function getQuestionListWithMediasCompressed()
7167
    {
7168
        return $this->questionList;
7169
    }
7170
7171
    /**
7172
     * Question list with medias uncompressed like this.
7173
     *
7174
     * @return array
7175
     *
7176
     * @example
7177
     *      <code>
7178
     *      array(
7179
     *      question_id,
7180
     *      question_id,
7181
     *      question_id, <- belongs to a media id
7182
     *      question_id, <- belongs to a media id
7183
     *      question_id,
7184
     *      )
7185
     *      </code>
7186
     */
7187
    public function getQuestionListWithMediasUncompressed()
7188
    {
7189
        return $this->questionListUncompressed;
7190
    }
7191
7192
    /**
7193
     * Sets the question list when the exercise->read() is executed.
7194
     *
7195
     * @param bool $adminView Whether to view the set the list of *all* questions or just the normal student view
7196
     */
7197
    public function setQuestionList($adminView = false)
7198
    {
7199
        // Getting question list.
7200
        $questionList = $this->selectQuestionList(true, $adminView);
7201
        $this->setMediaList($questionList);
7202
        $this->questionList = $this->transformQuestionListWithMedias($questionList, false);
7203
        $this->questionListUncompressed = $this->transformQuestionListWithMedias(
7204
            $questionList,
7205
            true
7206
        );
7207
    }
7208
7209
    /**
7210
     * @params array question list
7211
     * @params bool expand or not question list (true show all questions,
7212
     * false show media question id instead of the question ids)
7213
     */
7214
    public function transformQuestionListWithMedias(
7215
        $question_list,
7216
        $expand_media_questions = false
7217
    ) {
7218
        $new_question_list = [];
7219
        if (!empty($question_list)) {
7220
            $media_questions = $this->getMediaList();
7221
            $media_active = $this->mediaIsActivated($media_questions);
7222
7223
            if ($media_active) {
7224
                $counter = 1;
7225
                foreach ($question_list as $question_id) {
7226
                    $add_question = true;
7227
                    foreach ($media_questions as $media_id => $question_list_in_media) {
7228
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
7229
                            $add_question = false;
7230
                            if (!in_array($media_id, $new_question_list)) {
7231
                                $new_question_list[$counter] = $media_id;
7232
                                $counter++;
7233
                            }
7234
7235
                            break;
7236
                        }
7237
                    }
7238
                    if ($add_question) {
7239
                        $new_question_list[$counter] = $question_id;
7240
                        $counter++;
7241
                    }
7242
                }
7243
                if ($expand_media_questions) {
7244
                    $media_key_list = array_keys($media_questions);
7245
                    foreach ($new_question_list as &$question_id) {
7246
                        if (in_array($question_id, $media_key_list)) {
7247
                            $question_id = $media_questions[$question_id];
7248
                        }
7249
                    }
7250
                    $new_question_list = array_flatten($new_question_list);
7251
                }
7252
            } else {
7253
                $new_question_list = $question_list;
7254
            }
7255
        }
7256
7257
        return $new_question_list;
7258
    }
7259
7260
    /**
7261
     * Get question list depend on the random settings.
7262
     *
7263
     * @return array
7264
     */
7265
    public function get_validated_question_list()
7266
    {
7267
        $isRandomByCategory = $this->isRandomByCat();
7268
        if (0 == $isRandomByCategory) {
7269
            if ($this->isRandom()) {
7270
                return $this->getRandomList();
7271
            }
7272
7273
            return $this->selectQuestionList();
7274
        }
7275
7276
        if ($this->isRandom()) {
7277
            // USE question categories
7278
            // get questions by category for this exercise
7279
            // we have to choice $objExercise->random question in each array values of $tabCategoryQuestions
7280
            // key of $tabCategoryQuestions are the categopy id (0 for not in a category)
7281
            // value is the array of question id of this category
7282
            $questionList = [];
7283
            $categoryQuestions = TestCategory::getQuestionsByCat($this->id);
7284
            $isRandomByCategory = $this->getRandomByCategory();
7285
            // We sort categories based on the term between [] in the head
7286
            // of the category's description
7287
            /* examples of categories :
7288
             * [biologie] Maitriser les mecanismes de base de la genetique
7289
             * [biologie] Relier les moyens de depenses et les agents infectieux
7290
             * [biologie] Savoir ou est produite l'enrgie dans les cellules et sous quelle forme
7291
             * [chimie] Classer les molles suivant leur pouvoir oxydant ou reacteur
7292
             * [chimie] Connaître la denition de la theoie acide/base selon Brönsted
7293
             * [chimie] Connaître les charges des particules
7294
             * We want that in the order of the groups defined by the term
7295
             * between brackets at the beginning of the category title
7296
            */
7297
            // If test option is Grouped By Categories
7298
            if (2 == $isRandomByCategory) {
7299
                $categoryQuestions = TestCategory::sortTabByBracketLabel($categoryQuestions);
7300
            }
7301
            foreach ($categoryQuestions as $question) {
7302
                $number_of_random_question = $this->random;
7303
                if (-1 == $this->random) {
7304
                    $number_of_random_question = count($this->questionList);
7305
                }
7306
                $questionList = array_merge(
7307
                    $questionList,
7308
                    TestCategory::getNElementsFromArray(
7309
                        $question,
7310
                        $number_of_random_question
7311
                    )
7312
                );
7313
            }
7314
            // shuffle the question list if test is not grouped by categories
7315
            if (1 == $isRandomByCategory) {
7316
                shuffle($questionList); // or not
7317
            }
7318
7319
            return $questionList;
7320
        }
7321
7322
        // Problem, random by category has been selected and
7323
        // we have no $this->isRandom number of question selected
7324
        // Should not happened
7325
7326
        return [];
7327
    }
7328
7329
    public function get_question_list($expand_media_questions = false)
7330
    {
7331
        $question_list = $this->get_validated_question_list();
7332
        $question_list = $this->transform_question_list_with_medias($question_list, $expand_media_questions);
7333
7334
        return $question_list;
7335
    }
7336
7337
    public function transform_question_list_with_medias($question_list, $expand_media_questions = false)
7338
    {
7339
        $new_question_list = [];
7340
        if (!empty($question_list)) {
7341
            $media_questions = $this->getMediaList();
7342
            $media_active = $this->mediaIsActivated($media_questions);
7343
7344
            if ($media_active) {
7345
                $counter = 1;
7346
                foreach ($question_list as $question_id) {
7347
                    $add_question = true;
7348
                    foreach ($media_questions as $media_id => $question_list_in_media) {
7349
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
7350
                            $add_question = false;
7351
                            if (!in_array($media_id, $new_question_list)) {
7352
                                $new_question_list[$counter] = $media_id;
7353
                                $counter++;
7354
                            }
7355
7356
                            break;
7357
                        }
7358
                    }
7359
                    if ($add_question) {
7360
                        $new_question_list[$counter] = $question_id;
7361
                        $counter++;
7362
                    }
7363
                }
7364
                if ($expand_media_questions) {
7365
                    $media_key_list = array_keys($media_questions);
7366
                    foreach ($new_question_list as &$question_id) {
7367
                        if (in_array($question_id, $media_key_list)) {
7368
                            $question_id = $media_questions[$question_id];
7369
                        }
7370
                    }
7371
                    $new_question_list = array_flatten($new_question_list);
7372
                }
7373
            } else {
7374
                $new_question_list = $question_list;
7375
            }
7376
        }
7377
7378
        return $new_question_list;
7379
    }
7380
7381
    /**
7382
     * @param int $exe_id
7383
     *
7384
     * @return array
7385
     */
7386
    public function get_stat_track_exercise_info_by_exe_id($exe_id)
7387
    {
7388
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7389
        $exe_id = (int) $exe_id;
7390
        $sql_track = "SELECT * FROM $table WHERE exe_id = $exe_id ";
7391
        $result = Database::query($sql_track);
7392
        $new_array = [];
7393
        if (Database::num_rows($result) > 0) {
7394
            $new_array = Database::fetch_assoc($result);
7395
            $start_date = api_get_utc_datetime($new_array['start_date'], true);
7396
            $end_date = api_get_utc_datetime($new_array['exe_date'], true);
7397
            $new_array['duration_formatted'] = '';
7398
            if (!empty($new_array['exe_duration']) && !empty($start_date) && !empty($end_date)) {
7399
                $time = api_format_time($new_array['exe_duration'], 'js');
7400
                $new_array['duration_formatted'] = $time;
7401
            }
7402
        }
7403
7404
        return $new_array;
7405
    }
7406
7407
    /**
7408
     * @param int $exeId
7409
     *
7410
     * @return bool
7411
     */
7412
    public function removeAllQuestionToRemind($exeId)
7413
    {
7414
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7415
        $exeId = (int) $exeId;
7416
        if (empty($exeId)) {
7417
            return false;
7418
        }
7419
        $sql = "UPDATE $table
7420
                SET questions_to_check = ''
7421
                WHERE exe_id = $exeId ";
7422
        Database::query($sql);
7423
7424
        return true;
7425
    }
7426
7427
    /**
7428
     * @param int   $exeId
7429
     * @param array $questionList
7430
     *
7431
     * @return bool
7432
     */
7433
    public function addAllQuestionToRemind($exeId, $questionList = [])
7434
    {
7435
        $exeId = (int) $exeId;
7436
        if (empty($questionList)) {
7437
            return false;
7438
        }
7439
7440
        $questionListToString = implode(',', $questionList);
7441
        $questionListToString = Database::escape_string($questionListToString);
7442
7443
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7444
        $sql = "UPDATE $table
7445
                SET questions_to_check = '$questionListToString'
7446
                WHERE exe_id = $exeId";
7447
        Database::query($sql);
7448
7449
        return true;
7450
    }
7451
7452
    /**
7453
     * @param int    $exeId
7454
     * @param int    $questionId
7455
     * @param string $action
7456
     */
7457
    public function editQuestionToRemind($exeId, $questionId, $action = 'add')
7458
    {
7459
        $exercise_info = self::get_stat_track_exercise_info_by_exe_id($exeId);
7460
        $questionId = (int) $questionId;
7461
        $exeId = (int) $exeId;
7462
7463
        if ($exercise_info) {
7464
            $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7465
            if (empty($exercise_info['questions_to_check'])) {
7466
                if ('add' == $action) {
7467
                    $sql = "UPDATE $track_exercises
7468
                            SET questions_to_check = '$questionId'
7469
                            WHERE exe_id = $exeId ";
7470
                    Database::query($sql);
7471
                }
7472
            } else {
7473
                $remind_list = explode(',', $exercise_info['questions_to_check']);
7474
                $remind_list_string = '';
7475
                if ('add' === $action) {
7476
                    if (!in_array($questionId, $remind_list)) {
7477
                        $newRemindList = [];
7478
                        $remind_list[] = $questionId;
7479
                        $questionListInSession = Session::read('questionList');
7480
                        if (!empty($questionListInSession)) {
7481
                            foreach ($questionListInSession as $originalQuestionId) {
7482
                                if (in_array($originalQuestionId, $remind_list)) {
7483
                                    $newRemindList[] = $originalQuestionId;
7484
                                }
7485
                            }
7486
                        }
7487
                        $remind_list_string = implode(',', $newRemindList);
7488
                    }
7489
                } elseif ('delete' == $action) {
7490
                    if (!empty($remind_list)) {
7491
                        if (in_array($questionId, $remind_list)) {
7492
                            $remind_list = array_flip($remind_list);
7493
                            unset($remind_list[$questionId]);
7494
                            $remind_list = array_flip($remind_list);
7495
7496
                            if (!empty($remind_list)) {
7497
                                sort($remind_list);
7498
                                array_filter($remind_list);
7499
                                $remind_list_string = implode(',', $remind_list);
7500
                            }
7501
                        }
7502
                    }
7503
                }
7504
                $value = Database::escape_string($remind_list_string);
7505
                $sql = "UPDATE $track_exercises
7506
                        SET questions_to_check = '$value'
7507
                        WHERE exe_id = $exeId ";
7508
                Database::query($sql);
7509
            }
7510
        }
7511
    }
7512
7513
    /**
7514
     * @param string $answer
7515
     */
7516
    public function fill_in_blank_answer_to_array($answer)
7517
    {
7518
        $list = null;
7519
        api_preg_match_all('/\[[^]]+\]/', $answer, $list);
7520
7521
        if (empty($list)) {
7522
            return '';
7523
        }
7524
7525
        return $list[0];
7526
    }
7527
7528
    /**
7529
     * @param string $answer
7530
     *
7531
     * @return string
7532
     */
7533
    public function fill_in_blank_answer_to_string($answer)
7534
    {
7535
        $teacher_answer_list = $this->fill_in_blank_answer_to_array($answer);
7536
        $result = '';
7537
        if (!empty($teacher_answer_list)) {
7538
            foreach ($teacher_answer_list as $teacher_item) {
7539
                //Cleaning student answer list
7540
                $value = strip_tags($teacher_item);
7541
                $value = api_substr($value, 1, api_strlen($value) - 2);
7542
                $value = explode('/', $value);
7543
                if (!empty($value[0])) {
7544
                    $value = trim($value[0]);
7545
                    $value = str_replace('&nbsp;', '', $value);
7546
                    $result .= $value;
7547
                }
7548
            }
7549
        }
7550
7551
        return $result;
7552
    }
7553
7554
    /**
7555
     * @return string
7556
     */
7557
    public function returnTimeLeftDiv()
7558
    {
7559
        $html = '<div id="clock_warning" style="display:none">';
7560
        $html .= Display::return_message(
7561
            get_lang('Time limit reached'),
7562
            'warning'
7563
        );
7564
        $html .= ' ';
7565
        $html .= sprintf(
7566
            get_lang('Just a moment, please. You will be redirected in %s seconds...'),
7567
            '<span id="counter_to_redirect" class="red_alert"></span>'
7568
        );
7569
        $html .= '</div>';
7570
        $icon = Display::getMdiIcon('clock-outline', 'ch-tool-icon');
7571
        $html .= '<div class="count_down">
7572
                    '.get_lang('Remaining time to finish exercise').'
7573
                    '.$icon.'<span id="exercise_clock_warning"></span>
7574
                </div>';
7575
7576
        return $html;
7577
    }
7578
7579
    /**
7580
     * Get categories added in the exercise--category matrix.
7581
     *
7582
     * @return array
7583
     */
7584
    public function getCategoriesInExercise()
7585
    {
7586
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7587
        if (!empty($this->getId())) {
7588
            $sql = "SELECT * FROM $table
7589
                    WHERE exercise_id = {$this->getId()} ";
7590
            $result = Database::query($sql);
7591
            $list = [];
7592
            if (Database::num_rows($result)) {
7593
                while ($row = Database::fetch_assoc($result)) {
7594
                    $list[$row['category_id']] = $row;
7595
                }
7596
7597
                return $list;
7598
            }
7599
        }
7600
7601
        return [];
7602
    }
7603
7604
    /**
7605
     * Get total number of question that will be parsed when using the category/exercise.
7606
     *
7607
     * @return int
7608
     */
7609
    public function getNumberQuestionExerciseCategory()
7610
    {
7611
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7612
        if (!empty($this->getId())) {
7613
            $sql = "SELECT SUM(count_questions) count_questions
7614
                    FROM $table
7615
                    WHERE exercise_id = {$this->getId()}";
7616
            $result = Database::query($sql);
7617
            if (Database::num_rows($result)) {
7618
                $row = Database::fetch_array($result);
7619
7620
                return (int) $row['count_questions'];
7621
            }
7622
        }
7623
7624
        return 0;
7625
    }
7626
7627
    /**
7628
     * Save categories in the TABLE_QUIZ_REL_CATEGORY table
7629
     *
7630
     * @param array $categories
7631
     */
7632
    public function saveCategoriesInExercise($categories)
7633
    {
7634
        if (!empty($categories) && !empty($this->getId())) {
7635
            $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7636
            $sql = "DELETE FROM $table
7637
                    WHERE exercise_id = {$this->getId()}";
7638
            Database::query($sql);
7639
            foreach ($categories as $categoryId => $countQuestions) {
7640
                if ($categoryId !== 0) {
7641
                    $params = [
7642
                        'exercise_id' => $this->getId(),
7643
                        'category_id' => $categoryId,
7644
                        'count_questions' => $countQuestions,
7645
                    ];
7646
                    Database::insert($table, $params);
7647
                }
7648
            }
7649
        }
7650
    }
7651
7652
    /**
7653
     * @param array  $questionList
7654
     * @param int    $currentQuestion
7655
     * @param array  $conditions
7656
     * @param string $link
7657
     *
7658
     * @return string
7659
     */
7660
    public function progressExercisePaginationBar(
7661
        $questionList,
7662
        $currentQuestion,
7663
        $conditions,
7664
        $link
7665
    ) {
7666
        $mediaQuestions = $this->getMediaList();
7667
7668
        $html = '<div class="exercise_pagination pagination pagination-mini"><ul>';
7669
        $counter = 0;
7670
        $nextValue = 0;
7671
        $wasMedia = false;
7672
        $before = 0;
7673
        $counterNoMedias = 0;
7674
        foreach ($questionList as $questionId) {
7675
            $isCurrent = $currentQuestion == $counterNoMedias + 1 ? true : false;
7676
7677
            if (!empty($nextValue)) {
7678
                if ($wasMedia) {
7679
                    $nextValue = $nextValue - $before + 1;
7680
                }
7681
            }
7682
7683
            if (isset($mediaQuestions) && isset($mediaQuestions[$questionId])) {
7684
                $fixedValue = $counterNoMedias;
7685
7686
                $html .= Display::progressPaginationBar(
7687
                    $nextValue,
7688
                    $mediaQuestions[$questionId],
7689
                    $currentQuestion,
7690
                    $fixedValue,
7691
                    $conditions,
7692
                    $link,
7693
                    true,
7694
                    true
7695
                );
7696
7697
                $counter += count($mediaQuestions[$questionId]) - 1;
7698
                $before = count($questionList);
7699
                $wasMedia = true;
7700
                $nextValue += count($questionList);
7701
            } else {
7702
                $html .= Display::parsePaginationItem(
7703
                    $questionId,
7704
                    $isCurrent,
7705
                    $conditions,
7706
                    $link,
7707
                    $counter
7708
                );
7709
                $counter++;
7710
                $nextValue++;
7711
                $wasMedia = false;
7712
            }
7713
            $counterNoMedias++;
7714
        }
7715
        $html .= '</ul></div>';
7716
7717
        return $html;
7718
    }
7719
7720
    /**
7721
     *  Shows a list of numbers that represents the question to answer in a exercise.
7722
     *
7723
     * @param array  $categories
7724
     * @param int    $current
7725
     * @param array  $conditions
7726
     * @param string $link
7727
     *
7728
     * @return string
7729
     */
7730
    public function progressExercisePaginationBarWithCategories(
7731
        $categories,
7732
        $current,
7733
        $conditions = [],
7734
        $link = null
7735
    ) {
7736
        $html = null;
7737
        $counterNoMedias = 0;
7738
        $nextValue = 0;
7739
        $wasMedia = false;
7740
        $before = 0;
7741
7742
        if (!empty($categories)) {
7743
            $selectionType = $this->getQuestionSelectionType();
7744
            $useRootAsCategoryTitle = false;
7745
7746
            // Grouping questions per parent category see BT#6540
7747
            if (in_array(
7748
                $selectionType,
7749
                [
7750
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED,
7751
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM,
7752
                ]
7753
            )) {
7754
                $useRootAsCategoryTitle = true;
7755
            }
7756
7757
            // If the exercise is set to only show the titles of the categories
7758
            // at the root of the tree, then pre-order the categories tree by
7759
            // removing children and summing their questions into the parent
7760
            // categories
7761
            if ($useRootAsCategoryTitle) {
7762
                // The new categories list starts empty
7763
                $newCategoryList = [];
7764
                foreach ($categories as $category) {
7765
                    $rootElement = $category['root'];
7766
7767
                    if (isset($category['parent_info'])) {
7768
                        $rootElement = $category['parent_info']['id'];
7769
                    }
7770
7771
                    //$rootElement = $category['id'];
7772
                    // If the current category's ancestor was never seen
7773
                    // before, then declare it and assign the current
7774
                    // category to it.
7775
                    if (!isset($newCategoryList[$rootElement])) {
7776
                        $newCategoryList[$rootElement] = $category;
7777
                    } else {
7778
                        // If it was already seen, then merge the previous with
7779
                        // the current category
7780
                        $oldQuestionList = $newCategoryList[$rootElement]['question_list'];
7781
                        $category['question_list'] = array_merge($oldQuestionList, $category['question_list']);
7782
                        $newCategoryList[$rootElement] = $category;
7783
                    }
7784
                }
7785
                // Now use the newly built categories list, with only parents
7786
                $categories = $newCategoryList;
7787
            }
7788
7789
            foreach ($categories as $category) {
7790
                $questionList = $category['question_list'];
7791
                // Check if in this category there questions added in a media
7792
                $mediaQuestionId = $category['media_question'];
7793
                $isMedia = false;
7794
                $fixedValue = null;
7795
7796
                // Media exists!
7797
                if (999 != $mediaQuestionId) {
7798
                    $isMedia = true;
7799
                    $fixedValue = $counterNoMedias;
7800
                }
7801
7802
                //$categoryName = $category['path']; << show the path
7803
                $categoryName = $category['name'];
7804
7805
                if ($useRootAsCategoryTitle) {
7806
                    if (isset($category['parent_info'])) {
7807
                        $categoryName = $category['parent_info']['title'];
7808
                    }
7809
                }
7810
                $html .= '<div class="row">';
7811
                $html .= '<div class="span2">'.$categoryName.'</div>';
7812
                $html .= '<div class="span8">';
7813
7814
                if (!empty($nextValue)) {
7815
                    if ($wasMedia) {
7816
                        $nextValue = $nextValue - $before + 1;
7817
                    }
7818
                }
7819
                $html .= Display::progressPaginationBar(
7820
                    $nextValue,
7821
                    $questionList,
7822
                    $current,
7823
                    $fixedValue,
7824
                    $conditions,
7825
                    $link,
7826
                    $isMedia,
7827
                    true
7828
                );
7829
                $html .= '</div>';
7830
                $html .= '</div>';
7831
7832
                if (999 == $mediaQuestionId) {
7833
                    $counterNoMedias += count($questionList);
7834
                } else {
7835
                    $counterNoMedias++;
7836
                }
7837
7838
                $nextValue += count($questionList);
7839
                $before = count($questionList);
7840
7841
                if (999 != $mediaQuestionId) {
7842
                    $wasMedia = true;
7843
                } else {
7844
                    $wasMedia = false;
7845
                }
7846
            }
7847
        }
7848
7849
        return $html;
7850
    }
7851
7852
    /**
7853
     * Renders a question list.
7854
     *
7855
     * @param array $questionList    (with media questions compressed)
7856
     * @param int   $currentQuestion
7857
     * @param array $exerciseResult
7858
     * @param array $attemptList
7859
     * @param array $remindList
7860
     */
7861
    public function renderQuestionList(
7862
        $questionList,
7863
        $currentQuestion,
7864
        $exerciseResult,
7865
        $attemptList,
7866
        $remindList
7867
    ) {
7868
        $mediaQuestions = $this->getMediaList();
7869
        $i = 0;
7870
7871
        // Normal question list render (medias compressed)
7872
        foreach ($questionList as $questionId) {
7873
            $i++;
7874
            // For sequential exercises
7875
7876
            if (ONE_PER_PAGE == $this->type) {
7877
                // If it is not the right question, goes to the next loop iteration
7878
                if ($currentQuestion != $i) {
7879
                    continue;
7880
                } else {
7881
                    if (!in_array(
7882
                        $this->getFeedbackType(),
7883
                        [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
7884
                    )) {
7885
                        // if the user has already answered this question
7886
                        if (isset($exerciseResult[$questionId])) {
7887
                            echo Display::return_message(
7888
                                get_lang('You already answered the question'),
7889
                                'normal'
7890
                            );
7891
7892
                            break;
7893
                        }
7894
                    }
7895
                }
7896
            }
7897
7898
            // The $questionList contains the media id we check
7899
            // if this questionId is a media question type
7900
            if (isset($mediaQuestions[$questionId]) &&
7901
                999 != $mediaQuestions[$questionId]
7902
            ) {
7903
                // The question belongs to a media
7904
                $mediaQuestionList = $mediaQuestions[$questionId];
7905
                $objQuestionTmp = Question::read($questionId);
7906
7907
                $counter = 1;
7908
                if (MEDIA_QUESTION == $objQuestionTmp->type) {
7909
                    echo $objQuestionTmp->show_media_content();
7910
7911
                    $countQuestionsInsideMedia = count($mediaQuestionList);
7912
7913
                    // Show questions that belongs to a media
7914
                    if (!empty($mediaQuestionList)) {
7915
                        // In order to parse media questions we use letters a, b, c, etc.
7916
                        $letterCounter = 97;
7917
                        foreach ($mediaQuestionList as $questionIdInsideMedia) {
7918
                            $isLastQuestionInMedia = false;
7919
                            if ($counter == $countQuestionsInsideMedia) {
7920
                                $isLastQuestionInMedia = true;
7921
                            }
7922
                            $this->renderQuestion(
7923
                                $questionIdInsideMedia,
7924
                                $attemptList,
7925
                                $remindList,
7926
                                chr($letterCounter),
7927
                                $currentQuestion,
7928
                                $mediaQuestionList,
7929
                                $isLastQuestionInMedia,
7930
                                $questionList
7931
                            );
7932
                            $letterCounter++;
7933
                            $counter++;
7934
                        }
7935
                    }
7936
                } else {
7937
                    $this->renderQuestion(
7938
                        $questionId,
7939
                        $attemptList,
7940
                        $remindList,
7941
                        $i,
7942
                        $currentQuestion,
7943
                        null,
7944
                        null,
7945
                        $questionList
7946
                    );
7947
                    $i++;
7948
                }
7949
            } else {
7950
                // Normal question render.
7951
                $this->renderQuestion(
7952
                    $questionId,
7953
                    $attemptList,
7954
                    $remindList,
7955
                    $i,
7956
                    $currentQuestion,
7957
                    null,
7958
                    null,
7959
                    $questionList
7960
                );
7961
            }
7962
7963
            // For sequential exercises.
7964
            if (ONE_PER_PAGE == $this->type) {
7965
                // quits the loop
7966
                break;
7967
            }
7968
        }
7969
        // end foreach()
7970
7971
        if (ALL_ON_ONE_PAGE == $this->type) {
7972
            $exercise_actions = $this->show_button($questionId, $currentQuestion);
7973
            echo Display::div($exercise_actions, ['class' => 'exercise_actions']);
7974
        }
7975
    }
7976
7977
    /**
7978
     * Not implemented in 1.11.x.
7979
     *
7980
     * @param int   $questionId
7981
     * @param array $attemptList
7982
     * @param array $remindList
7983
     * @param int   $i
7984
     * @param int   $current_question
7985
     * @param array $questions_in_media
7986
     * @param bool  $last_question_in_media
7987
     * @param array $realQuestionList
7988
     * @param bool  $generateJS
7989
     */
7990
    public function renderQuestion(
7991
        $questionId,
7992
        $attemptList,
7993
        $remindList,
7994
        $i,
7995
        $current_question,
7996
        $questions_in_media = [],
7997
        $last_question_in_media = false,
7998
        $realQuestionList = [],
7999
        $generateJS = true
8000
    ) {
8001
        // With this option on the question is loaded via AJAX
8002
        //$generateJS = true;
8003
        //$this->loadQuestionAJAX = true;
8004
8005
        if ($generateJS && $this->loadQuestionAJAX) {
8006
            $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?a=get_question&id='.$questionId.'&'.api_get_cidreq();
8007
            $params = [
8008
                'questionId' => $questionId,
8009
                'attemptList' => $attemptList,
8010
                'remindList' => $remindList,
8011
                'i' => $i,
8012
                'current_question' => $current_question,
8013
                'questions_in_media' => $questions_in_media,
8014
                'last_question_in_media' => $last_question_in_media,
8015
            ];
8016
            $params = json_encode($params);
8017
8018
            $script = '<script>
8019
            $(function(){
8020
                var params = '.$params.';
8021
                $.ajax({
8022
                    type: "GET",
8023
                    data: params,
8024
                    url: "'.$url.'",
8025
                    success: function(return_value) {
8026
                        $("#ajaxquestiondiv'.$questionId.'").html(return_value);
8027
                    }
8028
                });
8029
            });
8030
            </script>
8031
            <div id="ajaxquestiondiv'.$questionId.'"></div>';
8032
            echo $script;
8033
        } else {
8034
            $origin = api_get_origin();
8035
            $question_obj = Question::read($questionId);
8036
            $user_choice = isset($attemptList[$questionId]) ? $attemptList[$questionId] : null;
8037
            $remind_highlight = null;
8038
8039
            // Hides questions when reviewing a ALL_ON_ONE_PAGE exercise
8040
            // see #4542 no_remind_highlight class hide with jquery
8041
            if (ALL_ON_ONE_PAGE == $this->type && isset($_GET['reminder']) && 2 == $_GET['reminder']) {
8042
                $remind_highlight = 'no_remind_highlight';
8043
                // @todo not implemented in 1.11.x
8044
                /*if (in_array($question_obj->type, Question::question_type_no_review())) {
8045
                    return null;
8046
                }*/
8047
            }
8048
8049
            $attributes = ['id' => 'remind_list['.$questionId.']'];
8050
8051
            // Showing the question
8052
            $exercise_actions = null;
8053
            echo '<a id="questionanchor'.$questionId.'"></a><br />';
8054
            echo '<div id="question_div_'.$questionId.'" class="main_question '.$remind_highlight.'" >';
8055
8056
            // Shows the question + possible answers
8057
            $showTitle = 1 == $this->getHideQuestionTitle() ? false : true;
8058
            // @todo not implemented in 1.11.x
8059
            /*echo $this->showQuestion(
8060
                $question_obj,
8061
                false,
8062
                $origin,
8063
                $i,
8064
                $showTitle,
8065
                false,
8066
                $user_choice,
8067
                false,
8068
                null,
8069
                false,
8070
                $this->getModelType(),
8071
                $this->categoryMinusOne
8072
            );*/
8073
8074
            // Button save and continue
8075
            switch ($this->type) {
8076
                case ONE_PER_PAGE:
8077
                    $exercise_actions .= $this->show_button(
8078
                        $questionId,
8079
                        $current_question,
8080
                        null,
8081
                        $remindList
8082
                    );
8083
8084
                    break;
8085
                case ALL_ON_ONE_PAGE:
8086
                    if (api_is_allowed_to_session_edit()) {
8087
                        $button = [
8088
                            Display::button(
8089
                                'save_now',
8090
                                get_lang('Save and continue'),
8091
                                ['type' => 'button', 'class' => 'btn btn--primary', 'data-question' => $questionId]
8092
                            ),
8093
                            '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>',
8094
                        ];
8095
                        $exercise_actions .= Display::div(
8096
                            implode(PHP_EOL, $button),
8097
                            ['class' => 'exercise_save_now_button mb-4']
8098
                        );
8099
                    }
8100
8101
                    break;
8102
            }
8103
8104
            if (!empty($questions_in_media)) {
8105
                $count_of_questions_inside_media = count($questions_in_media);
8106
                if ($count_of_questions_inside_media > 1 && api_is_allowed_to_session_edit()) {
8107
                    $button = [
8108
                        Display::button(
8109
                            'save_now',
8110
                            get_lang('Save and continue'),
8111
                            ['type' => 'button', 'class' => 'btn btn--primary', 'data-question' => $questionId]
8112
                        ),
8113
                        '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>&nbsp;',
8114
                    ];
8115
                    $exercise_actions = Display::div(
8116
                        implode(PHP_EOL, $button),
8117
                        ['class' => 'exercise_save_now_button mb-4']
8118
                    );
8119
                }
8120
8121
                if ($last_question_in_media && ONE_PER_PAGE == $this->type) {
8122
                    $exercise_actions = $this->show_button($questionId, $current_question, $questions_in_media);
8123
                }
8124
            }
8125
8126
            // Checkbox review answers. Not implemented.
8127
            /*if ($this->review_answers &&
8128
                !in_array($question_obj->type, Question::question_type_no_review())
8129
            ) {
8130
                $remind_question_div = Display::tag(
8131
                    'label',
8132
                    Display::input(
8133
                        'checkbox',
8134
                        'remind_list['.$questionId.']',
8135
                        '',
8136
                        $attributes
8137
                    ).get_lang('Revise question later'),
8138
                    [
8139
                        'class' => 'checkbox',
8140
                        'for' => 'remind_list['.$questionId.']',
8141
                    ]
8142
                );
8143
                $exercise_actions .= Display::div(
8144
                    $remind_question_div,
8145
                    ['class' => 'exercise_save_now_button']
8146
                );
8147
            }*/
8148
8149
            echo Display::div(' ', ['class' => 'clear']);
8150
8151
            $paginationCounter = null;
8152
            if (ONE_PER_PAGE == $this->type) {
8153
                if (empty($questions_in_media)) {
8154
                    $paginationCounter = Display::paginationIndicator(
8155
                        $current_question,
8156
                        count($realQuestionList)
8157
                    );
8158
                } else {
8159
                    if ($last_question_in_media) {
8160
                        $paginationCounter = Display::paginationIndicator(
8161
                            $current_question,
8162
                            count($realQuestionList)
8163
                        );
8164
                    }
8165
                }
8166
            }
8167
8168
            echo '<div class="row"><div class="pull-right">'.$paginationCounter.'</div></div>';
8169
            echo Display::div($exercise_actions, ['class' => 'exercise_actions']);
8170
            echo '</div>';
8171
        }
8172
    }
8173
8174
    /**
8175
     * Returns an array of categories details for the questions of the current
8176
     * exercise.
8177
     *
8178
     * @return array
8179
     */
8180
    public function getQuestionWithCategories()
8181
    {
8182
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
8183
        $categoryRelTable = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
8184
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8185
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
8186
        $sql = "SELECT DISTINCT cat.*
8187
                FROM $TBL_EXERCICE_QUESTION e
8188
                INNER JOIN $TBL_QUESTIONS q
8189
                ON (e.question_id = q.iid)
8190
                INNER JOIN $categoryRelTable catRel
8191
                ON (catRel.question_id = e.question_id)
8192
                INNER JOIN $categoryTable cat
8193
                ON (cat.iid = catRel.category_id)
8194
                WHERE
8195
                  e.quiz_id	= ".(int) ($this->getId());
8196
8197
        $result = Database::query($sql);
8198
        $categoriesInExercise = [];
8199
        if (Database::num_rows($result)) {
8200
            $categoriesInExercise = Database::store_result($result, 'ASSOC');
8201
        }
8202
8203
        return $categoriesInExercise;
8204
    }
8205
8206
    /**
8207
     * Calculate the max_score of the quiz, depending of question inside, and quiz advanced option.
8208
     */
8209
    public function getMaxScore()
8210
    {
8211
        $outMaxScore = 0;
8212
        // list of question's id !!! the array key start at 1 !!!
8213
        $questionList = $this->selectQuestionList(true);
8214
8215
        if ($this->random > 0 && $this->randomByCat > 0) {
8216
            // test is random by category
8217
            // get the $numberRandomQuestions best score question of each category
8218
            $numberRandomQuestions = $this->random;
8219
            $tabCategoriesScores = [];
8220
            foreach ($questionList as $questionId) {
8221
                $questionCategoryId = TestCategory::getCategoryForQuestion($questionId);
8222
                if (!is_array($tabCategoriesScores[$questionCategoryId])) {
8223
                    $tabCategoriesScores[$questionCategoryId] = [];
8224
                }
8225
                $tmpObjQuestion = Question::read($questionId);
8226
                if (is_object($tmpObjQuestion)) {
8227
                    $tabCategoriesScores[$questionCategoryId][] = $tmpObjQuestion->weighting;
8228
                }
8229
            }
8230
8231
            // here we've got an array with first key, the category_id, second key, score of question for this cat
8232
            foreach ($tabCategoriesScores as $tabScores) {
8233
                rsort($tabScores);
8234
                $tabScoresCount = count($tabScores);
8235
                for ($i = 0; $i < min($numberRandomQuestions, $tabScoresCount); $i++) {
8236
                    $outMaxScore += $tabScores[$i];
8237
                }
8238
            }
8239
8240
            return $outMaxScore;
8241
        }
8242
8243
        // standard test, just add each question score
8244
        foreach ($questionList as $questionId) {
8245
            $question = Question::read($questionId, $this->course);
8246
            $outMaxScore += $question->weighting;
8247
        }
8248
8249
        return $outMaxScore;
8250
    }
8251
8252
    /**
8253
     * @return string
8254
     */
8255
    public function get_formated_title()
8256
    {
8257
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
8258
        }
8259
8260
        return api_html_entity_decode($this->selectTitle());
8261
    }
8262
8263
    /**
8264
     * @param string $title
8265
     *
8266
     * @return string
8267
     */
8268
    public static function get_formated_title_variable($title)
8269
    {
8270
        return api_html_entity_decode($title);
8271
    }
8272
8273
    /**
8274
     * @return string
8275
     */
8276
    public function format_title()
8277
    {
8278
        return api_htmlentities($this->title);
8279
    }
8280
8281
    /**
8282
     * @param string $title
8283
     *
8284
     * @return string
8285
     */
8286
    public static function format_title_variable($title)
8287
    {
8288
        return api_htmlentities($title);
8289
    }
8290
8291
    /**
8292
     * @param int $courseId
8293
     * @param int $sessionId
8294
     *
8295
     * @return array exercises
8296
     */
8297
    public function getExercisesByCourseSession($courseId, $sessionId)
8298
    {
8299
        $courseId = (int) $courseId;
8300
        $sessionId = (int) $sessionId;
8301
8302
        $tbl_quiz = Database::get_course_table(TABLE_QUIZ_TEST);
8303
        $sql = "SELECT * FROM $tbl_quiz cq
8304
                WHERE
8305
                    cq.c_id = %s AND
8306
                    (cq.session_id = %s OR cq.session_id = 0)
8307
                ORDER BY cq.iid";
8308
        $sql = sprintf($sql, $courseId, $sessionId);
8309
8310
        $result = Database::query($sql);
8311
8312
        $rows = [];
8313
        while ($row = Database::fetch_assoc($result)) {
8314
            $rows[] = $row;
8315
        }
8316
8317
        return $rows;
8318
    }
8319
8320
    /**
8321
     * @param int   $courseId
8322
     * @param int   $sessionId
8323
     * @param array $quizId
8324
     *
8325
     * @return array exercises
8326
     */
8327
    public function getExerciseAndResult($courseId, $sessionId, $quizId = [])
8328
    {
8329
        if (empty($quizId)) {
8330
            return [];
8331
        }
8332
8333
        $sessionId = (int) $sessionId;
8334
        $courseId = (int) $courseId;
8335
8336
        $ids = is_array($quizId) ? $quizId : [$quizId];
8337
        $ids = array_map('intval', $ids);
8338
        $ids = implode(',', $ids);
8339
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8340
        if (0 != $sessionId) {
8341
            $sql = "SELECT * FROM $track_exercises te
8342
              INNER JOIN c_quiz cq
8343
              ON cq.iid = te.exe_exo_id
8344
              WHERE
8345
              te.c_id = %d AND
8346
              te.session_id = %s AND
8347
              cq.iid IN (%s)
8348
              ORDER BY cq.iid";
8349
8350
            $sql = sprintf($sql, $courseId, $sessionId, $ids);
8351
        } else {
8352
            $sql = "SELECT * FROM $track_exercises te
8353
              INNER JOIN c_quiz cq ON cq.iid = te.exe_exo_id
8354
              WHERE
8355
              te.c_id = %d AND
8356
              cq.iid IN (%s)
8357
              ORDER BY cq.iid";
8358
            $sql = sprintf($sql, $courseId, $ids);
8359
        }
8360
        $result = Database::query($sql);
8361
        $rows = [];
8362
        while ($row = Database::fetch_assoc($result)) {
8363
            $rows[] = $row;
8364
        }
8365
8366
        return $rows;
8367
    }
8368
8369
    /**
8370
     * @param $exeId
8371
     * @param $exercise_stat_info
8372
     * @param $remindList
8373
     * @param $currentQuestion
8374
     *
8375
     * @return int|null
8376
     */
8377
    public static function getNextQuestionId(
8378
        $exeId,
8379
        $exercise_stat_info,
8380
        $remindList,
8381
        $currentQuestion
8382
    ) {
8383
        $result = Event::get_exercise_results_by_attempt($exeId, 'incomplete');
8384
8385
        if (isset($result[$exeId])) {
8386
            $result = $result[$exeId];
8387
        } else {
8388
            return null;
8389
        }
8390
8391
        $data_tracking = $exercise_stat_info['data_tracking'];
8392
        $data_tracking = explode(',', $data_tracking);
8393
8394
        // if this is the final question do nothing.
8395
        if ($currentQuestion == count($data_tracking)) {
8396
            return null;
8397
        }
8398
8399
        $currentQuestion--;
8400
8401
        if (!empty($result['question_list'])) {
8402
            $answeredQuestions = [];
8403
            foreach ($result['question_list'] as $question) {
8404
                if (!empty($question['answer'])) {
8405
                    $answeredQuestions[] = $question['question_id'];
8406
                }
8407
            }
8408
8409
            // Checking answered questions
8410
            $counterAnsweredQuestions = 0;
8411
            foreach ($data_tracking as $questionId) {
8412
                if (!in_array($questionId, $answeredQuestions)) {
8413
                    if ($currentQuestion != $counterAnsweredQuestions) {
8414
                        break;
8415
                    }
8416
                }
8417
                $counterAnsweredQuestions++;
8418
            }
8419
8420
            $counterRemindListQuestions = 0;
8421
            // Checking questions saved in the reminder list
8422
            if (!empty($remindList)) {
8423
                foreach ($data_tracking as $questionId) {
8424
                    if (in_array($questionId, $remindList)) {
8425
                        // Skip the current question
8426
                        if ($currentQuestion != $counterRemindListQuestions) {
8427
                            break;
8428
                        }
8429
                    }
8430
                    $counterRemindListQuestions++;
8431
                }
8432
8433
                if ($counterRemindListQuestions < $currentQuestion) {
8434
                    return null;
8435
                }
8436
8437
                if (!empty($counterRemindListQuestions)) {
8438
                    if ($counterRemindListQuestions > $counterAnsweredQuestions) {
8439
                        return $counterAnsweredQuestions;
8440
                    } else {
8441
                        return $counterRemindListQuestions;
8442
                    }
8443
                }
8444
            }
8445
8446
            return $counterAnsweredQuestions;
8447
        }
8448
    }
8449
8450
    /**
8451
     * Gets the position of a questionId in the question list.
8452
     *
8453
     * @param $questionId
8454
     *
8455
     * @return int
8456
     */
8457
    public function getPositionInCompressedQuestionList($questionId)
8458
    {
8459
        $questionList = $this->getQuestionListWithMediasCompressed();
8460
        $mediaQuestions = $this->getMediaList();
8461
        $position = 1;
8462
        foreach ($questionList as $id) {
8463
            if (isset($mediaQuestions[$id]) && in_array($questionId, $mediaQuestions[$id])) {
8464
                $mediaQuestionList = $mediaQuestions[$id];
8465
                if (in_array($questionId, $mediaQuestionList)) {
8466
                    return $position;
8467
                } else {
8468
                    $position++;
8469
                }
8470
            } else {
8471
                if ($id == $questionId) {
8472
                    return $position;
8473
                } else {
8474
                    $position++;
8475
                }
8476
            }
8477
        }
8478
8479
        return 1;
8480
    }
8481
8482
    /**
8483
     * Get the correct answers in all attempts.
8484
     *
8485
     * @param int  $learnPathId
8486
     * @param int  $learnPathItemId
8487
     * @param bool $onlyCorrect
8488
     *
8489
     * @return array
8490
     */
8491
    public function getAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0, $onlyCorrect = true)
8492
    {
8493
        $attempts = Event::getExerciseResultsByUser(
8494
            api_get_user_id(),
8495
            $this->getId(),
8496
            api_get_course_int_id(),
8497
            api_get_session_id(),
8498
            $learnPathId,
8499
            $learnPathItemId,
8500
            'DESC'
8501
        );
8502
8503
        $list = [];
8504
        foreach ($attempts as $attempt) {
8505
            foreach ($attempt['question_list'] as $answers) {
8506
                foreach ($answers as $answer) {
8507
                    $objAnswer = new Answer($answer['question_id']);
8508
                    if ($onlyCorrect) {
8509
                        switch ($objAnswer->getQuestionType()) {
8510
                            case FILL_IN_BLANKS:
8511
                            case FILL_IN_BLANKS_COMBINATION:
8512
                                $isCorrect = FillBlanks::isCorrect($answer['answer']);
8513
8514
                                break;
8515
                            case MATCHING:
8516
                            case MATCHING_COMBINATION:
8517
                            case DRAGGABLE:
8518
                            case MATCHING_DRAGGABLE:
8519
                            case MATCHING_DRAGGABLE_COMBINATION:
8520
                                $isCorrect = Matching::isCorrect(
8521
                                    $answer['position'],
8522
                                    $answer['answer'],
8523
                                    $answer['question_id']
8524
                                );
8525
8526
                                break;
8527
                            case ORAL_EXPRESSION:
8528
                                $isCorrect = false;
8529
8530
                                break;
8531
                            default:
8532
                                $isCorrect = $objAnswer->isCorrectByAutoId($answer['answer']);
8533
                        }
8534
                        if ($isCorrect) {
8535
                            $list[$answer['question_id']][] = $answer;
8536
                        }
8537
                    } else {
8538
                        $list[$answer['question_id']][] = $answer;
8539
                    }
8540
                }
8541
            }
8542
8543
            if (false === $onlyCorrect) {
8544
                // Only take latest attempt
8545
                break;
8546
            }
8547
        }
8548
8549
        return $list;
8550
    }
8551
8552
    /**
8553
     * Get the correct answers in all attempts.
8554
     *
8555
     * @param int $learnPathId
8556
     * @param int $learnPathItemId
8557
     *
8558
     * @return array
8559
     */
8560
    public function getCorrectAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0)
8561
    {
8562
        return $this->getAnswersInAllAttempts($learnPathId, $learnPathItemId);
8563
    }
8564
8565
    /**
8566
     * @return bool
8567
     */
8568
    public function showPreviousButton()
8569
    {
8570
        $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
8571
        if (false === $allow) {
8572
            return true;
8573
        }
8574
8575
        return $this->showPreviousButton;
8576
    }
8577
8578
    public function getPreventBackwards()
8579
    {
8580
        return (int) $this->preventBackwards;
8581
    }
8582
8583
    /**
8584
     * @return int
8585
     */
8586
    public function getQuizCategoryId(): ?int
8587
    {
8588
        if (empty($this->quizCategoryId)) {
8589
            return null;
8590
        }
8591
8592
        return (int) $this->quizCategoryId;
8593
    }
8594
8595
    /**
8596
     * @param int $value
8597
     */
8598
    public function setQuizCategoryId($value): void
8599
    {
8600
        if (!empty($value)) {
8601
            $this->quizCategoryId = (int) $value;
8602
        }
8603
    }
8604
8605
    /**
8606
     * Set the value to 1 to hide the question number.
8607
     *
8608
     * @param int $value
8609
     */
8610
    public function setHideQuestionNumber($value = 0)
8611
    {
8612
        $this->hideQuestionNumber = (int) $value;
8613
    }
8614
8615
    /**
8616
     * Gets the value to hide or show the question number. If it does not exist, it is set to 0.
8617
     *
8618
     * @return int 1 if the question number must be hidden
8619
     */
8620
    public function getHideQuestionNumber()
8621
    {
8622
        return (int) $this->hideQuestionNumber;
8623
    }
8624
8625
    public function setPageResultConfiguration(array $values)
8626
    {
8627
        $pageConfig = ('true' === api_get_setting('exercise.allow_quiz_results_page_config'));
8628
        if ($pageConfig) {
8629
            $params = [
8630
                'hide_expected_answer' => $values['hide_expected_answer'] ?? '',
8631
                'hide_question_score' => $values['hide_question_score'] ?? '',
8632
                'hide_total_score' => $values['hide_total_score'] ?? '',
8633
                'hide_category_table' => $values['hide_category_table'] ?? '',
8634
                'hide_correct_answered_questions' => $values['hide_correct_answered_questions'] ?? '',
8635
            ];
8636
            $this->pageResultConfiguration = $params;
8637
        }
8638
    }
8639
8640
    /**
8641
     * @param array $defaults
8642
     */
8643
    public function setPageResultConfigurationDefaults(&$defaults)
8644
    {
8645
        $configuration = $this->getPageResultConfiguration();
8646
        if (!empty($configuration) && !empty($defaults)) {
8647
            $defaults = array_merge($defaults, $configuration);
8648
        }
8649
    }
8650
8651
    /**
8652
     * @return array
8653
     */
8654
    public function getPageResultConfiguration()
8655
    {
8656
        $pageConfig = ('true' === api_get_setting('exercise.allow_quiz_results_page_config'));
8657
        if ($pageConfig) {
8658
            return $this->pageResultConfiguration;
8659
        }
8660
8661
        return [];
8662
    }
8663
8664
    /**
8665
     * @param string $attribute
8666
     *
8667
     * @return mixed|null
8668
     */
8669
    public function getPageConfigurationAttribute($attribute)
8670
    {
8671
        $result = $this->getPageResultConfiguration();
8672
8673
        if (!empty($result)) {
8674
            return $result[$attribute] ?? null;
8675
        }
8676
8677
        return null;
8678
    }
8679
8680
    /**
8681
     * @param bool $showPreviousButton
8682
     *
8683
     * @return Exercise
8684
     */
8685
    public function setShowPreviousButton($showPreviousButton)
8686
    {
8687
        $this->showPreviousButton = $showPreviousButton;
8688
8689
        return $this;
8690
    }
8691
8692
    /**
8693
     * @param array $notifications
8694
     */
8695
    public function setNotifications($notifications)
8696
    {
8697
        $this->notifications = $notifications;
8698
    }
8699
8700
    /**
8701
     * @return array
8702
     */
8703
    public function getNotifications()
8704
    {
8705
        return $this->notifications;
8706
    }
8707
8708
    /**
8709
     * @return bool
8710
     */
8711
    public function showExpectedChoice()
8712
    {
8713
        return ('true' === api_get_setting('exercise.show_exercise_expected_choice'));
8714
    }
8715
8716
    /**
8717
     * @return bool
8718
     */
8719
    public function showExpectedChoiceColumn()
8720
    {
8721
        if (true === $this->forceShowExpectedChoiceColumn) {
8722
            return true;
8723
        }
8724
        if ($this->hideExpectedAnswer) {
8725
            return false;
8726
        }
8727
        if (!in_array(
8728
            $this->results_disabled,
8729
            [
8730
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
8731
            ]
8732
        )
8733
        ) {
8734
            $hide = (int) $this->getPageConfigurationAttribute('hide_expected_answer');
8735
            if (1 === $hide) {
8736
                return false;
8737
            }
8738
8739
            return true;
8740
        }
8741
8742
        return false;
8743
    }
8744
8745
    public function getQuestionRibbon(string $class, string $scoreLabel, ?string $result, array $array): string
8746
    {
8747
        $hide = (int) $this->getPageConfigurationAttribute('hide_question_score');
8748
        if (1 === $hide) {
8749
            return '';
8750
        }
8751
8752
        $ribbon = '<div class="question-answer-result__header-ribbon-title question-answer-result__header-ribbon-title--'.$class.'">'.$scoreLabel.'</div>';
8753
        if (!empty($result)) {
8754
            $ribbon .= '<div class="question-answer-result__header-ribbon-detail">'
8755
                .get_lang('Score').': '.$result
8756
                .'</div>';
8757
        }
8758
8759
        $ribbonClassModifier = '';
8760
8761
        if ($this->showExpectedChoice()) {
8762
            $hideLabel = ('true' === api_get_setting('exercise.exercise_hide_label'));
8763
            if (true === $hideLabel) {
8764
                $ribbonClassModifier = 'question-answer-result__header-ribbon--no-ribbon';
8765
                $html = '';
8766
                $answerUsed = (int) $array['used'];
8767
                $answerMissing = (int) $array['missing'] - $answerUsed;
8768
                for ($i = 1; $i <= $answerUsed; $i++) {
8769
                    $html .= Display::getMdiIcon(StateIcon::COMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL);
8770
                }
8771
                for ($i = 1; $i <= $answerMissing; $i++) {
8772
                    $html .= Display::getMdiIcon(StateIcon::INCOMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL);
8773
                }
8774
                $ribbon = '<div class="question-answer-result__header-ribbon-title hide-label-title">'
8775
                    .get_lang('Correct answers').': '.$result.'</div>'
8776
                    .'<div class="question-answer-result__header-ribbon-detail">'.$html.'</div>';
8777
            }
8778
        }
8779
8780
        return Display::div(
8781
            $ribbon,
8782
            ['class' => "question-answer-result__header-ribbon $ribbonClassModifier"]
8783
        );
8784
    }
8785
8786
    /**
8787
     * @return int
8788
     */
8789
    public function getAutoLaunch()
8790
    {
8791
        return $this->autolaunch;
8792
    }
8793
8794
    /**
8795
     * Clean auto launch settings for all exercise in course/course-session.
8796
     */
8797
    public function enableAutoLaunch()
8798
    {
8799
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8800
        $sql = "UPDATE $table SET autolaunch = 1
8801
                WHERE iid = ".$this->iId;
8802
        Database::query($sql);
8803
    }
8804
8805
    /**
8806
     * Clean auto launch settings for all exercise in course/course-session.
8807
     */
8808
    public function cleanCourseLaunchSettings()
8809
    {
8810
        $em = Database::getManager();
8811
8812
        $repo = Container::getQuizRepository();
8813
8814
        $session = api_get_session_entity();
8815
        $course = api_get_course_entity();
8816
8817
        $qb = $repo->getResourcesByCourse($course, $session);
8818
        $quizzes = $qb->getQuery()->getResult();
8819
8820
        foreach ($quizzes as $quiz) {
8821
            $quiz->setAutoLaunch(false);
8822
            $em->persist($quiz);
8823
        }
8824
8825
        $em->flush();
8826
    }
8827
8828
    /**
8829
     * Get the title without HTML tags.
8830
     *
8831
     * @return string
8832
     */
8833
    public function getUnformattedTitle()
8834
    {
8835
        return strip_tags(api_html_entity_decode($this->title));
8836
    }
8837
8838
    /**
8839
     * Get the question IDs from quiz_rel_question for the current quiz,
8840
     * using the parameters as the arguments to the SQL's LIMIT clause.
8841
     * Because the exercise_id is known, it also comes with a filter on
8842
     * the session, so sessions are not specified here.
8843
     *
8844
     * @param int $start  At which question do we want to start the list
8845
     * @param int $length Up to how many results we want
8846
     *
8847
     * @return array A list of question IDs
8848
     */
8849
    public function getQuestionForTeacher($start = 0, $length = 10)
8850
    {
8851
        $start = (int) $start;
8852
        if ($start < 0) {
8853
            $start = 0;
8854
        }
8855
8856
        $length = (int) $length;
8857
8858
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8859
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
8860
        $sql = "SELECT DISTINCT e.question_id
8861
                FROM $quizRelQuestion e
8862
                INNER JOIN $question q
8863
                ON (e.question_id = q.iid)
8864
                WHERE
8865
8866
                    e.quiz_id = '".$this->getId()."'
8867
                ORDER BY question_order
8868
                LIMIT $start, $length
8869
            ";
8870
        $result = Database::query($sql);
8871
        $questionList = [];
8872
        while ($object = Database::fetch_object($result)) {
8873
            $questionList[] = $object->question_id;
8874
        }
8875
8876
        return $questionList;
8877
    }
8878
8879
    /**
8880
     * @param int   $exerciseId
8881
     * @param array $courseInfo
8882
     * @param int   $sessionId
8883
     *
8884
     * @return bool
8885
     */
8886
    public function generateStats($exerciseId, $courseInfo, $sessionId)
8887
    {
8888
        $allowStats = ('true' === api_get_setting('gradebook.allow_gradebook_stats'));
8889
        if (!$allowStats) {
8890
            return false;
8891
        }
8892
8893
        if (empty($courseInfo)) {
8894
            return false;
8895
        }
8896
8897
        $courseId = $courseInfo['real_id'];
8898
8899
        $sessionId = (int) $sessionId;
8900
        $exerciseId = (int) $exerciseId;
8901
8902
        $result = $this->read($exerciseId);
8903
8904
        if (empty($result)) {
8905
            api_not_allowed(true);
8906
        }
8907
8908
        $statusToFilter = empty($sessionId) ? STUDENT : 0;
8909
8910
        $studentList = CourseManager::get_user_list_from_course_code(
8911
            $courseInfo['code'],
8912
            $sessionId,
8913
            null,
8914
            null,
8915
            $statusToFilter
8916
        );
8917
8918
        if (empty($studentList)) {
8919
            Display::addFlash(Display::return_message(get_lang('No users in course')));
8920
            header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
8921
            exit;
8922
        }
8923
8924
        $tblStats = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8925
8926
        $studentIdList = [];
8927
        if (!empty($studentList)) {
8928
            $studentIdList = array_column($studentList, 'user_id');
8929
        }
8930
8931
        $sessionCondition = api_get_session_condition($sessionId);
8932
        if (false == $this->exercise_was_added_in_lp) {
8933
            $sql = "SELECT * FROM $tblStats
8934
                        WHERE
8935
                            exe_exo_id = $exerciseId AND
8936
                            orig_lp_id = 0 AND
8937
                            orig_lp_item_id = 0 AND
8938
                            status <> 'incomplete' AND
8939
                            c_id = $courseId
8940
                            $sessionCondition
8941
                        ";
8942
        } else {
8943
            $lpId = null;
8944
            if (!empty($this->lpList)) {
8945
                // Taking only the first LP
8946
                $lpId = $this->getLpBySession($sessionId);
8947
                $lpId = $lpId['lp_id'];
8948
            }
8949
8950
            $sql = "SELECT *
8951
                        FROM $tblStats
8952
                        WHERE
8953
                            exe_exo_id = $exerciseId AND
8954
                            orig_lp_id = $lpId AND
8955
                            status <> 'incomplete' AND
8956
                            session_id = $sessionId AND
8957
                            c_id = $courseId ";
8958
        }
8959
8960
        $sql .= ' ORDER BY exe_id DESC';
8961
8962
        $studentCount = 0;
8963
        $sum = 0;
8964
        $bestResult = 0;
8965
        $sumResult = 0;
8966
        $result = Database::query($sql);
8967
        while ($data = Database::fetch_assoc($result)) {
8968
            // Only take into account users in the current student list.
8969
            if (!empty($studentIdList)) {
8970
                if (!in_array($data['exe_user_id'], $studentIdList)) {
8971
                    continue;
8972
                }
8973
            }
8974
8975
            if (!isset($students[$data['exe_user_id']])) {
8976
                if (0 != $data['max_score']) {
8977
                    $students[$data['exe_user_id']] = $data['score'];
8978
                    if ($data['score'] > $bestResult) {
8979
                        $bestResult = $data['score'];
8980
                    }
8981
                    $sumResult += $data['score'];
8982
                }
8983
            }
8984
        }
8985
8986
        $count = count($studentList);
8987
        $average = $sumResult / $count;
8988
        $em = Database::getManager();
8989
8990
        $links = AbstractLink::getGradebookLinksFromItem(
8991
            $this->getId(),
8992
            LINK_EXERCISE,
8993
            $courseInfo['real_id'],
8994
            $sessionId
8995
        );
8996
8997
        if (empty($links)) {
8998
            $links = AbstractLink::getGradebookLinksFromItem(
8999
                $this->iId,
9000
                LINK_EXERCISE,
9001
                $courseInfo['real_id'],
9002
                $sessionId
9003
            );
9004
        }
9005
9006
        if (!empty($links)) {
9007
            $repo = $em->getRepository(GradebookLink::class);
9008
9009
            foreach ($links as $link) {
9010
                $linkId = $link['id'];
9011
                /** @var GradebookLink $exerciseLink */
9012
                $exerciseLink = $repo->find($linkId);
9013
                if ($exerciseLink) {
9014
                    $exerciseLink
9015
                        ->setUserScoreList($students)
9016
                        ->setBestScore($bestResult)
9017
                        ->setAverageScore($average)
9018
                        ->setScoreWeight($this->getMaxScore());
9019
                    $em->persist($exerciseLink);
9020
                    $em->flush();
9021
                }
9022
            }
9023
        }
9024
    }
9025
9026
    /**
9027
     * Return an HTML table of exercises for on-screen printing, including
9028
     * action icons. If no exercise is present and the user can edit the
9029
     * course, show a "create test" button.
9030
     *
9031
     * @param int    $categoryId
9032
     * @param string $keyword
9033
     * @param int    $userId
9034
     * @param int    $courseId
9035
     * @param int    $sessionId
9036
     * @param bool   $returnData
9037
     * @param int    $minCategoriesInExercise
9038
     * @param int    $filterByResultDisabled
9039
     * @param int    $filterByAttempt
9040
     *
9041
     * @return string|SortableTableFromArrayConfig
9042
     */
9043
    public static function exerciseGridResource(
9044
        $categoryId,
9045
        $keyword = '',
9046
        $userId = 0,
9047
        $courseId = 0,
9048
        $sessionId = 0,
9049
        $returnData = false,
9050
        $minCategoriesInExercise = 0,
9051
        $filterByResultDisabled = 0,
9052
        $filterByAttempt = 0,
9053
        $myActions = null,
9054
        $returnTable = false
9055
    ) {
9056
        $is_allowedToEdit = api_is_allowed_to_edit(null, true);
9057
        $courseId = $courseId ? (int) $courseId : api_get_course_int_id();
9058
        $sessionId = $sessionId ? (int) $sessionId : api_get_session_id();
9059
9060
        $course = api_get_course_entity($courseId);
9061
        $session = api_get_session_entity($sessionId);
9062
9063
        $userId = $userId ? (int) $userId : api_get_user_id();
9064
        $user = api_get_user_entity($userId);
9065
9066
        $repo = Container::getQuizRepository();
9067
9068
        $trackEExerciseRepo = Container::getTrackEExerciseRepository();
9069
        $pendingCorrections = $trackEExerciseRepo->getPendingCorrectionsByExercise($courseId);
9070
        $pendingAttempts = [];
9071
        foreach ($pendingCorrections as $correction) {
9072
            $pendingAttempts[$correction['exerciseId']] = $correction['pendingCount'];
9073
        }
9074
9075
        // 2. Get query builder from repo.
9076
        $qb = $repo->getResourcesByCourse($course, $session);
9077
9078
        if (!empty($sessionId)) {
9079
            $qb->andWhere('(links.session = :sess OR links.session IS NULL)')
9080
                ->setParameter('sess', $session);
9081
        } else {
9082
            $qb->andWhere('links.session IS NULL');
9083
        }
9084
9085
        if (!empty($categoryId)) {
9086
            $qb->andWhere($qb->expr()->eq('resource.quizCategory', $categoryId));
9087
        } else {
9088
            $qb->andWhere($qb->expr()->isNull('resource.quizCategory'));
9089
        }
9090
9091
        $allowDelete = self::allowAction('delete');
9092
        $allowClean = self::allowAction('clean_results');
9093
9094
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
9095
9096
        $categoryId = (int) $categoryId;
9097
        $keyword = Database::escape_string($keyword);
9098
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
9099
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
9100
9101
        $courseId = $course->getId();
9102
        $tableRows = [];
9103
        $origin = api_get_origin();
9104
        $charset = 'utf-8';
9105
        $token = Security::get_token();
9106
        $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh($userId, ['real_id' => $courseId]);
9107
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
9108
        $content = '';
9109
        $column = 0;
9110
        if ($is_allowedToEdit) {
9111
            $column = 1;
9112
        }
9113
9114
        $table = new SortableTableFromArrayConfig(
9115
            [],
9116
            $column,
9117
            self::PAGINATION_ITEMS_PER_PAGE,
9118
            'exercises_cat_'.$categoryId.'_'.api_get_course_int_id().'_'.api_get_session_id()
9119
        );
9120
9121
        $limit = $table->per_page;
9122
        $page = $table->page_nr;
9123
        $from = $limit * ($page - 1);
9124
9125
        $categoryCondition = '';
9126
        if ('true' === api_get_setting('exercise.allow_exercise_categories')) {
9127
            if (!empty($categoryId)) {
9128
                $categoryCondition = " AND quiz_category_id = $categoryId ";
9129
            } else {
9130
                $categoryCondition = ' AND quiz_category_id IS NULL ';
9131
            }
9132
        }
9133
9134
        if (!empty($keyword)) {
9135
            $qb->andWhere($qb->expr()->like('resource.title', ':keyword'));
9136
            $qb->setParameter('keyword', '%'.$keyword.'%');
9137
        }
9138
9139
        if (!$is_allowedToEdit) {
9140
            $qb->andWhere('links.visibility = :published')
9141
                ->setParameter('published', ResourceLink::VISIBILITY_PUBLISHED);
9142
        }
9143
9144
        $qb->setFirstResult($from);
9145
        $qb->setMaxResults($limit);
9146
9147
        $filterByResultDisabledCondition = '';
9148
        $filterByResultDisabled = (int) $filterByResultDisabled;
9149
        if (!empty($filterByResultDisabled)) {
9150
            $filterByResultDisabledCondition = ' AND e.results_disabled = '.$filterByResultDisabled;
9151
        }
9152
        $filterByAttemptCondition = '';
9153
        $filterByAttempt = (int) $filterByAttempt;
9154
        if (!empty($filterByAttempt)) {
9155
            $filterByAttemptCondition = ' AND e.max_attempt = '.$filterByAttempt;
9156
        }
9157
9158
        $exerciseList = $qb->getQuery()->getResult();
9159
9160
        $total = $repo->getCount($qb);
9161
9162
        $webPath = api_get_path(WEB_CODE_PATH);
9163
        if (!empty($exerciseList)) {
9164
            $visibilitySetting = ('true' === api_get_setting('lp.show_hidden_exercise_added_to_lp'));
9165
            //avoid sending empty parameters
9166
            $mylpid = empty($learnpath_id) ? '' : '&learnpath_id='.$learnpath_id;
9167
            $mylpitemid = empty($learnpath_item_id) ? '' : '&learnpath_item_id='.$learnpath_item_id;
9168
9169
            /** @var CQuiz $exerciseEntity */
9170
            foreach ($exerciseList as $exerciseEntity) {
9171
                $currentRow = [];
9172
                $exerciseId = $exerciseEntity->getIid();
9173
                $actions = '';
9174
                $attempt_text = '';
9175
                $exercise = new Exercise($courseId);
9176
                $exercise->read($exerciseId, false);
9177
9178
                if (empty($exercise->iId)) {
9179
                    continue;
9180
                }
9181
9182
                $sessionId = api_get_session_id();
9183
                $allowToEditBaseCourse = true;
9184
                $visibleBase = $exerciseEntity->isVisible($course);
9185
                $visibleSess = !empty($sessionId) ? $exerciseEntity->isVisible($course, $session) : null;
9186
                if ($visibleSess === true) {
9187
                    $visibleForStudent = true;
9188
                } elseif ($visibleSess === false) {
9189
                    $visibleForStudent = false;
9190
                } else {
9191
                    $visibleForStudent = $visibleBase;
9192
                }
9193
9194
                $visibleForTeacher = true;
9195
9196
                if (!$is_allowedToEdit && !empty($sessionId)) {
9197
                    $visibilitySetting = ('true' === api_get_setting('lp.show_hidden_exercise_added_to_lp'));
9198
                    if (!$visibleBase && !$visibilitySetting && $exercise->exercise_was_added_in_lp) {
9199
                        continue;
9200
                    }
9201
                }
9202
9203
                $isBaseCourseExercise = $visibleBase && ($visibleSess === null || $visibleSess === true);
9204
                if (!empty($sessionId) && $isBaseCourseExercise) {
9205
                    $allowToEditBaseCourse = false;
9206
                }
9207
9208
                $resourceLink = $exerciseEntity->getFirstResourceLink();
9209
                if ($resourceLink && !$sessionId && $resourceLink->getSession() === null) {
9210
                    $allowToEditBaseCourse = true;
9211
                }
9212
9213
                $allowToEditSession = ($resourceLink && $resourceLink->getSession() && $resourceLink->getSession()->getId() === $sessionId);
9214
                $sessionStar = null;
9215
                if ($allowToEditSession) {
9216
                    $sessionStar = api_get_session_image($sessionId, $user);
9217
                }
9218
9219
                $locked = $exercise->is_gradebook_locked;
9220
9221
                $startTime = $exerciseEntity->getStartTime();
9222
                $endTime = $exerciseEntity->getEndTime();
9223
                $time_limits = false;
9224
                if (!empty($startTime) || !empty($endTime)) {
9225
                    $time_limits = true;
9226
                }
9227
9228
                $is_actived_time = false;
9229
                if ($time_limits) {
9230
                    // check if start time
9231
                    $start_time = false;
9232
                    if (!empty($startTime)) {
9233
                        $start_time = api_strtotime($startTime->format('Y-m-d H:i:s'), 'UTC');
9234
                    }
9235
                    $end_time = false;
9236
                    if (!empty($endTime)) {
9237
                        $end_time = api_strtotime($endTime->format('Y-m-d H:i:s'), 'UTC');
9238
                    }
9239
                    $now = time();
9240
                    //If both "clocks" are enable
9241
                    if ($start_time && $end_time) {
9242
                        if ($now > $start_time && $end_time > $now) {
9243
                            $is_actived_time = true;
9244
                        }
9245
                    } else {
9246
                        //we check the start and end
9247
                        if ($start_time) {
9248
                            if ($now > $start_time) {
9249
                                $is_actived_time = true;
9250
                            }
9251
                        }
9252
                        if ($end_time) {
9253
                            if ($end_time > $now) {
9254
                                $is_actived_time = true;
9255
                            }
9256
                        }
9257
                    }
9258
                }
9259
9260
                $cut_title = $exercise->getCutTitle();
9261
                $alt_title = '';
9262
                if ($cut_title != $exerciseEntity->getTitle()) {
9263
                    $alt_title = ' title = "'.$exercise->getUnformattedTitle().'" ';
9264
                }
9265
9266
                // Teacher only.
9267
                if ($is_allowedToEdit) {
9268
                    $lp_blocked = null;
9269
                    if (true == $exercise->exercise_was_added_in_lp) {
9270
                        $lp_blocked = Display::div(
9271
                            get_lang(
9272
                                '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.'
9273
                            ),
9274
                            ['class' => 'lp_content_type_label']
9275
                        );
9276
                    }
9277
9278
                    $style = $visibleForStudent ? '' : 'color:grey';
9279
9280
                    $title = $cut_title;
9281
9282
                    $url = '<a
9283
                        '.$alt_title.'
9284
                        id="tooltip_'.$exerciseId.'"
9285
                        href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$exerciseId.'"
9286
                        style = "'.$style.';float:left;"
9287
                        >
9288
                         '.Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, ICON_SIZE_SMALL, $title).$title.
9289
                        '</a>'.$sessionStar;
9290
9291
                    if (ExerciseLib::isQuizEmbeddable($exerciseEntity)) {
9292
                        $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'));
9293
                        $url .= Display::div($embeddableIcon, ['class' => 'pull-right']);
9294
                    }
9295
9296
                    $pendingCount = $pendingAttempts[$exerciseId] ?? 0;
9297
                    if ($pendingCount > 0) {
9298
                        $pendingIcon = Display::getMdiIcon(
9299
                            ActionIcon::ALERT->value,
9300
                            'ch-tool-icon',
9301
                            null,
9302
                            ICON_SIZE_SMALL,
9303
                            get_lang('Pending attempts') . ": $pendingCount"
9304
                        );
9305
                        $url .= " $pendingIcon";
9306
                    }
9307
9308
                    $currentRow['title'] = $url.$lp_blocked;
9309
                    $rowi = $exerciseEntity->getQuestions()->count();
9310
                    if ($allowToEditBaseCourse || $allowToEditSession) {
9311
                        // Questions list
9312
                        $actions = Display::url(
9313
                            Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit')),
9314
                            'admin.php?'.api_get_cidreq().'&exerciseId='.$exerciseId
9315
                        );
9316
9317
                        // Test settings
9318
                        $settings = Display::url(
9319
                            Display::getMdiIcon(ToolIcon::SETTINGS, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Configure')),
9320
                            'exercise_admin.php?'.api_get_cidreq().'&exerciseId='.$exerciseId
9321
                        );
9322
9323
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9324
                            $settings = '';
9325
                        }
9326
                        $actions .= $settings;
9327
9328
                        // Exercise results
9329
                        $resultsLink = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9330
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).'</a>';
9331
9332
                        if ($limitTeacherAccess) {
9333
                            if (api_is_platform_admin()) {
9334
                                $actions .= $resultsLink;
9335
                            }
9336
                        } else {
9337
                            // Exercise results
9338
                            $actions .= $resultsLink;
9339
                        }
9340
9341
                        // Auto launch
9342
                        $autoLaunch = $exercise->getAutoLaunch();
9343
                        if (empty($autoLaunch)) {
9344
                            $actions .= Display::url(
9345
                                Display::getMdiIcon('rocket-launch', 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang('Enable')),
9346
                                'exercise.php?'.api_get_cidreq(
9347
                                ).'&action=enable_launch&sec_token='.$token.'&exerciseId='.$exerciseId
9348
                            );
9349
                        } else {
9350
                            $actions .= Display::url(
9351
                                Display::getMdiIcon('rocket-launch', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Disable')),
9352
                                'exercise.php?'.api_get_cidreq(
9353
                                ).'&action=disable_launch&sec_token='.$token.'&exerciseId='.$exerciseId
9354
                            );
9355
                        }
9356
9357
                        // Export
9358
                        $actions .= Display::url(
9359
                            Display::getMdiIcon('disc', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Copy this exercise as a new one')),
9360
                            '',
9361
                            [
9362
                                'onclick' => "javascript:if(!confirm('".addslashes(
9363
                                        api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES)
9364
                                    )." ".addslashes($title)."?"."')) return false;",
9365
                                'href' => 'exercise.php?'.api_get_cidreq(
9366
                                    ).'&action=copy_exercise&sec_token='.$token.'&exerciseId='.$exerciseId,
9367
                            ]
9368
                        );
9369
9370
                        // Clean exercise
9371
                        $clean = '';
9372
                        if (true === $allowClean) {
9373
                            if (!$locked) {
9374
                                $clean = Display::url(
9375
                                    Display::getMdiIcon(ActionIcon::RESET, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Clear all learners results for this exercise')
9376
                                    ),
9377
                                    '',
9378
                                    [
9379
                                        'onclick' => "javascript:if(!confirm('".
9380
                                            addslashes(
9381
                                                api_htmlentities(
9382
                                                    get_lang('Are you sure to delete results'),
9383
                                                    ENT_QUOTES
9384
                                                )
9385
                                            )." ".addslashes($title)."?"."')) return false;",
9386
                                        'href' => 'exercise.php?'.api_get_cidreq(
9387
                                            ).'&action=clean_results&sec_token='.$token.'&exerciseId='.$exerciseId,
9388
                                    ]
9389
                                );
9390
                            } else {
9391
                                $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.')
9392
                                );
9393
                            }
9394
                        }
9395
9396
                        $actions .= $clean;
9397
                        // Visible / invisible
9398
                        // Check if this exercise was added in a LP
9399
                        $visibility = '';
9400
                        if (api_is_platform_admin()) {
9401
                            if ($exercise->exercise_was_added_in_lp) {
9402
                                $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.')
9403
                                );
9404
                            } else {
9405
                                if (!$exerciseEntity->isVisible($course, $session)) {
9406
                                    $visibility = Display::url(
9407
                                        Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Activate')
9408
                                        ),
9409
                                        'exercise.php?' . api_get_cidreq() . '&action=enable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9410
                                    );
9411
                                } else {
9412
                                    // else if not active
9413
                                    $visibility = Display::url(
9414
                                        Display::getMdiIcon(StateIcon::PUBLIC_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Deactivate')
9415
                                        ),
9416
                                        'exercise.php?' . api_get_cidreq() . '&action=disable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9417
                                    );
9418
                                }
9419
                            }
9420
                        }
9421
9422
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9423
                            $visibility = '';
9424
                        }
9425
9426
                        $actions .= $visibility;
9427
9428
                        // Export qti ...
9429
                        $export = Display::url(
9430
                            Display::getMdiIcon(
9431
                                'database',
9432
                                'ch-tool-icon',
9433
                                null,
9434
                                ICON_SIZE_SMALL,
9435
                                'IMS/QTI'
9436
                            ),
9437
                            'exercise.php?action=exportqti2&exerciseId='.$exerciseId.'&'.api_get_cidreq()
9438
                        );
9439
9440
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9441
                            $export = '';
9442
                        }
9443
9444
                        $actions .= $export;
9445
                    } else {
9446
                        // not session
9447
                        $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")
9448
                        );
9449
9450
                        // Check if this exercise was added in a LP
9451
                        $visibility = '';
9452
                        if (api_is_platform_admin()) {
9453
                            if ($exercise->exercise_was_added_in_lp) {
9454
                                $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.')
9455
                                );
9456
                            } else {
9457
                                if (!$visibility) {
9458
                                    $visibility = Display::url(
9459
                                        Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Activate')
9460
                                        ),
9461
                                        'exercise.php?' . api_get_cidreq() . '&action=enable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9462
                                    );
9463
                                } else {
9464
                                    // else if not active
9465
                                    $visibility = Display::url(
9466
                                        Display::getMdiIcon(StateIcon::PUBLIC_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Deactivate')
9467
                                        ),
9468
                                        'exercise.php?' . api_get_cidreq() . '&action=disable&sec_token=' . $token . '&exerciseId=' . $exerciseId
9469
                                    );
9470
                                }
9471
                            }
9472
                        }
9473
9474
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9475
                            $visibility = '';
9476
                        }
9477
9478
                        $actions .= $visibility;
9479
                        $actions .= '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9480
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).'</a>';
9481
                        $actions .= Display::url(
9482
                            Display::getMdiIcon('disc', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Copy this exercise as a new one')),
9483
                            '',
9484
                            [
9485
                                'onclick' => "javascript:if(!confirm('".addslashes(
9486
                                        api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES)
9487
                                    )." ".addslashes($title)."?"."')) return false;",
9488
                                'href' => 'exercise.php?'.api_get_cidreq(
9489
                                    ).'&action=copy_exercise&sec_token='.$token.'&exerciseId='.$exerciseId,
9490
                            ]
9491
                        );
9492
                    }
9493
9494
                    // Delete
9495
                    $delete = '';
9496
                    if ($repo->isGranted('DELETE', $exerciseEntity) && $allowToEditBaseCourse) {
9497
                        if (!$locked) {
9498
                            $deleteUrl = 'exercise.php?'.api_get_cidreq().'&action=delete&sec_token='.$token.'&exerciseId='.$exerciseId;
9499
                            $delete = Display::url(
9500
                                Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete')),
9501
                                '',
9502
                                [
9503
                                    'onclick' => "javascript:if(!confirm('".
9504
                                        addslashes(api_htmlentities(get_lang('Are you sure you want to delete')))." ".
9505
                                        addslashes($exercise->getUnformattedTitle())."?"."')) return false;",
9506
                                    'href' => $deleteUrl,
9507
                                ]
9508
                            );
9509
                        } else {
9510
                            $delete = Display::getMdiIcon(
9511
                                ActionIcon::DELETE,
9512
                                'ch-tool-icon-disabled',
9513
                                null,
9514
                                ICON_SIZE_SMALL,
9515
                                get_lang(
9516
                                    '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.'
9517
                                )
9518
                            );
9519
                        }
9520
                    }
9521
9522
                    if ($limitTeacherAccess && !api_is_platform_admin()) {
9523
                        $delete = '';
9524
                    }
9525
9526
                    if (!empty($minCategoriesInExercise)) {
9527
                        $cats = TestCategory::getListOfCategoriesForTest($exercise);
9528
                        if (!(count($cats) >= $minCategoriesInExercise)) {
9529
                            continue;
9530
                        }
9531
                    }
9532
                    $actions .= $delete;
9533
9534
                    // Number of questions.
9535
                    $random = $exerciseEntity->getRandom();
9536
                    if ($random > 0 || -1 == $random) {
9537
                        // if random == -1 means use random questions with all questions
9538
                        $random_number_of_question = $random;
9539
                        if (-1 == $random_number_of_question) {
9540
                            $random_number_of_question = $rowi;
9541
                        }
9542
                        if ($exerciseEntity->getRandomByCategory() > 0) {
9543
                            $nbQuestionsTotal = TestCategory::getNumberOfQuestionRandomByCategory(
9544
                                $exerciseId,
9545
                                $random_number_of_question
9546
                            );
9547
                            $number_of_questions = $nbQuestionsTotal.' ';
9548
                            $number_of_questions .= ($nbQuestionsTotal > 1) ? get_lang('questions') : get_lang(
9549
                                'Question lower case'
9550
                            );
9551
                            $number_of_questions .= ' - ';
9552
                            $number_of_questions .= min(
9553
                                    TestCategory::getNumberMaxQuestionByCat($exerciseId),
9554
                                    $random_number_of_question
9555
                                ).' '.get_lang('Question by category');
9556
                        } else {
9557
                            $random_label = ' ('.get_lang('Random').') ';
9558
                            $number_of_questions = $random_number_of_question.' '.$random_label.' / '.$rowi;
9559
                            // Bug if we set a random value bigger than the real number of questions
9560
                            if ($random_number_of_question > $rowi) {
9561
                                $number_of_questions = $rowi.' '.$random_label;
9562
                            }
9563
                        }
9564
                    } else {
9565
                        $number_of_questions = $rowi;
9566
                    }
9567
9568
                    $currentRow['count_questions'] = $number_of_questions;
9569
                } else {
9570
                    // Student only.
9571
                    if (!$visibleForStudent) {
9572
                        continue;
9573
                    }
9574
9575
                    $url = '<a '.$alt_title.'
9576
                        href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$exerciseId.'">'.
9577
                        $cut_title.'</a>';
9578
9579
                    // Link of the exercise.
9580
                    $currentRow['title'] = $url.' '.$sessionStar;
9581
                    // This query might be improved later on by ordering by the new "tms" field rather than by exe_id
9582
                    if ($returnData) {
9583
                        $currentRow['title'] = $exercise->getUnformattedTitle();
9584
                    }
9585
9586
                    $sessionCondition = api_get_session_condition(api_get_session_id());
9587
                    // Don't remove this marker: note-query-exe-results
9588
                    $sql = "SELECT * FROM $TBL_TRACK_EXERCISES
9589
                            WHERE
9590
                                exe_exo_id = ".$exerciseId." AND
9591
                                exe_user_id = $userId AND
9592
                                c_id = ".api_get_course_int_id()." AND
9593
                                status <> 'incomplete' AND
9594
                                orig_lp_id = 0 AND
9595
                                orig_lp_item_id = 0
9596
                                $sessionCondition
9597
                            ORDER BY exe_id DESC";
9598
9599
                    $qryres = Database::query($sql);
9600
                    $num = Database:: num_rows($qryres);
9601
9602
                    // Hide the results.
9603
                    $my_result_disabled = $exerciseEntity->getResultsDisabled();
9604
                    $attempt_text = '-';
9605
                    // Time limits are on
9606
                    if ($time_limits) {
9607
                        // Exam is ready to be taken
9608
                        if ($is_actived_time) {
9609
                            // Show results
9610
                            if (
9611
                            in_array(
9612
                                $my_result_disabled,
9613
                                [
9614
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9615
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9616
                                    RESULT_DISABLE_SHOW_SCORE_ONLY,
9617
                                    RESULT_DISABLE_RANKING,
9618
                                ]
9619
                            )
9620
                            ) {
9621
                                // More than one attempt
9622
                                if ($num > 0) {
9623
                                    $row_track = Database:: fetch_array($qryres);
9624
                                    $attempt_text = get_lang('Latest attempt').' : ';
9625
                                    $attempt_text .= ExerciseLib::show_score(
9626
                                        $row_track['score'],
9627
                                        $row_track['max_score']
9628
                                    );
9629
                                } else {
9630
                                    //No attempts
9631
                                    $attempt_text = get_lang('Not attempted');
9632
                                }
9633
                            } else {
9634
                                $attempt_text = '-';
9635
                            }
9636
                        } else {
9637
                            // Quiz not ready due to time limits
9638
                            //@todo use the is_visible function
9639
                            if (!empty($startTime) && !empty($endTime)) {
9640
                                $today = time();
9641
                                if ($today < $start_time) {
9642
                                    $attempt_text = sprintf(
9643
                                        get_lang('Exercise will be activated from %s to %s'),
9644
                                        api_convert_and_format_date($start_time),
9645
                                        api_convert_and_format_date($end_time)
9646
                                    );
9647
                                } else {
9648
                                    if ($today > $end_time) {
9649
                                        $attempt_text = sprintf(
9650
                                            get_lang('Exercise was activated from %s to %s'),
9651
                                            api_convert_and_format_date($start_time),
9652
                                            api_convert_and_format_date($end_time)
9653
                                        );
9654
                                    }
9655
                                }
9656
                            } else {
9657
                                if (!empty($startTime)) {
9658
                                    $attempt_text = sprintf(
9659
                                        get_lang('Exercise available from %s'),
9660
                                        api_convert_and_format_date($start_time)
9661
                                    );
9662
                                }
9663
                                if (!empty($endTime)) {
9664
                                    $attempt_text = sprintf(
9665
                                        get_lang('Exercise available until %s'),
9666
                                        api_convert_and_format_date($end_time)
9667
                                    );
9668
                                }
9669
                            }
9670
                        }
9671
                    } else {
9672
                        // Normal behaviour.
9673
                        // Show results.
9674
                        if (
9675
                        in_array(
9676
                            $my_result_disabled,
9677
                            [
9678
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9679
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9680
                                RESULT_DISABLE_SHOW_SCORE_ONLY,
9681
                                RESULT_DISABLE_RANKING,
9682
                                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
9683
                            ]
9684
                        )
9685
                        ) {
9686
                            if ($num > 0) {
9687
                                $row_track = Database::fetch_array($qryres);
9688
                                $attempt_text = get_lang('Latest attempt').' : ';
9689
                                $attempt_text .= ExerciseLib::show_score(
9690
                                    $row_track['score'],
9691
                                    $row_track['max_score']
9692
                                );
9693
                            } else {
9694
                                $attempt_text = get_lang('Not attempted');
9695
                            }
9696
                        }
9697
                    }
9698
                    if ($returnData) {
9699
                        $attempt_text = $num;
9700
                    }
9701
                }
9702
9703
                $currentRow['attempt'] = $attempt_text;
9704
                $currentRow['iid'] = $exerciseId;
9705
9706
                if ($is_allowedToEdit) {
9707
                    $additionalActions = ExerciseLib::getAdditionalTeacherActions($exerciseId);
9708
9709
                    if (!empty($additionalActions)) {
9710
                        $actions .= $additionalActions.PHP_EOL;
9711
                    }
9712
9713
                    if (!empty($myActions) && is_callable($myActions)) {
9714
                        $actions = $myActions($currentRow);
9715
                    }
9716
                    $currentRow = [
9717
                        $exerciseId,
9718
                        $currentRow['title'],
9719
                        $currentRow['count_questions'],
9720
                        $actions,
9721
                    ];
9722
                } else {
9723
                    $currentRow = [
9724
                        $currentRow['title'],
9725
                        $currentRow['attempt'],
9726
                    ];
9727
9728
                    if ($isDrhOfCourse) {
9729
                        $currentRow[] = '<a
9730
                            href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9731
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).
9732
                            '</a>';
9733
                    }
9734
                    if ($returnData) {
9735
                        $currentRow['id'] = $exercise->id;
9736
                        $currentRow['url'] = $webPath.'exercise/overview.php?'
9737
                            .api_get_cidreq().'&'
9738
                            ."$mylpid$mylpitemid&exerciseId={$exercise->id}";
9739
                        $currentRow['name'] = $currentRow[0];
9740
                    }
9741
                }
9742
                $tableRows[] = $currentRow;
9743
            }
9744
        }
9745
9746
        if (empty($tableRows) && empty($categoryId)) {
9747
            if ($is_allowedToEdit && 'learnpath' !== $origin) {
9748
                if (!empty($_GET['keyword'])) {
9749
                    $content .= Display::return_message(
9750
                        sprintf(get_lang('No result for keyword %s'),Security::remove_XSS($_GET['keyword'])),
9751
                        'warning'
9752
                    );
9753
                } else {
9754
                    $content .= Display::noDataView(
9755
                        get_lang('Test'),
9756
                        Display::getMdiIcon(ToolIcon::QUIZ, 'ch-tool-icon', null, ICON_SIZE_BIG),
9757
                        get_lang('Create a new test'),
9758
                        'exercise_admin.php?'.api_get_cidreq()
9759
                    );
9760
                }
9761
            }
9762
        } else {
9763
            if (empty($tableRows)) {
9764
                return '';
9765
            }
9766
            $table->setTableData($tableRows);
9767
            $table->setTotalNumberOfItems($total);
9768
            $table->set_additional_parameters(
9769
                [
9770
                    'cid' => api_get_course_int_id(),
9771
                    'sid' => api_get_session_id(),
9772
                    'category_id' => $categoryId,
9773
                    'sec_token'   => Security::get_token(),
9774
                ]
9775
            );
9776
9777
            if ($is_allowedToEdit) {
9778
                $formActions = [];
9779
                $formActions['visible'] = get_lang('Activate');
9780
                $formActions['invisible'] = get_lang('Deactivate');
9781
                $formActions['delete'] = get_lang('Delete');
9782
                $table->set_form_actions($formActions);
9783
            }
9784
9785
            $i = 0;
9786
            if ($is_allowedToEdit) {
9787
                $table->set_header($i++, '', false, 'width="18px"');
9788
            }
9789
            $table->set_header($i++, get_lang('Test name'), false);
9790
9791
            if ($is_allowedToEdit) {
9792
                $table->set_header($i++, get_lang('Questions'), false, [], ['class' => 'text-center']);
9793
                $table->set_header($i++, get_lang('Actions'), false, [], ['class' => 'text-center']);
9794
            } else {
9795
                $table->set_header($i++, get_lang('Status'), false);
9796
                if ($isDrhOfCourse) {
9797
                    $table->set_header($i++, get_lang('Actions'), false, [], ['class' => 'text-center']);
9798
                }
9799
            }
9800
9801
            if ($returnTable) {
9802
                return $table;
9803
            }
9804
            $content .= $table->return_table();
9805
        }
9806
9807
        return $content;
9808
    }
9809
9810
    /**
9811
     * @return int value in minutes
9812
     */
9813
    public function getResultAccess()
9814
    {
9815
        $extraFieldValue = new ExtraFieldValue('exercise');
9816
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9817
            $this->iId,
9818
            'results_available_for_x_minutes'
9819
        );
9820
9821
        if (!empty($value) && isset($value['value'])) {
9822
            return (int) $value['value'];
9823
        }
9824
9825
        return 0;
9826
    }
9827
9828
    /**
9829
     * @param array $exerciseResultInfo
9830
     *
9831
     * @return bool
9832
     */
9833
    public function getResultAccessTimeDiff($exerciseResultInfo)
9834
    {
9835
        $value = $this->getResultAccess();
9836
        if (!empty($value)) {
9837
            $endDate = new DateTime($exerciseResultInfo['exe_date'], new DateTimeZone('UTC'));
9838
            $endDate->add(new DateInterval('PT'.$value.'M'));
9839
            $now = time();
9840
            if ($endDate->getTimestamp() > $now) {
9841
                return (int) $endDate->getTimestamp() - $now;
9842
            }
9843
        }
9844
9845
        return 0;
9846
    }
9847
9848
    /**
9849
     * @param array $exerciseResultInfo
9850
     *
9851
     * @return bool
9852
     */
9853
    public function hasResultsAccess($exerciseResultInfo)
9854
    {
9855
        $diff = $this->getResultAccessTimeDiff($exerciseResultInfo);
9856
        if (0 === $diff) {
9857
            return false;
9858
        }
9859
9860
        return true;
9861
    }
9862
9863
    /**
9864
     * @return int
9865
     */
9866
    public function getResultsAccess()
9867
    {
9868
        $extraFieldValue = new ExtraFieldValue('exercise');
9869
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9870
            $this->iId,
9871
            'results_available_for_x_minutes'
9872
        );
9873
        if (!empty($value)) {
9874
            return (int) $value;
9875
        }
9876
9877
        return 0;
9878
    }
9879
9880
    /**
9881
     * @param int   $questionId
9882
     * @param bool  $show_results
9883
     * @param array $question_result
9884
     */
9885
    public function getDelineationResult(Question $objQuestionTmp, $questionId, $show_results, $question_result)
9886
    {
9887
        $id = (int) $objQuestionTmp->id;
9888
        $questionId = (int) $questionId;
9889
9890
        $final_overlap = $question_result['extra']['final_overlap'];
9891
        $final_missing = $question_result['extra']['final_missing'];
9892
        $final_excess = $question_result['extra']['final_excess'];
9893
9894
        $overlap_color = $question_result['extra']['overlap_color'];
9895
        $missing_color = $question_result['extra']['missing_color'];
9896
        $excess_color = $question_result['extra']['excess_color'];
9897
9898
        $threadhold1 = $question_result['extra']['threadhold1'];
9899
        $threadhold2 = $question_result['extra']['threadhold2'];
9900
        $threadhold3 = $question_result['extra']['threadhold3'];
9901
9902
        if ($show_results) {
9903
            if ($overlap_color) {
9904
                $overlap_color = 'green';
9905
            } else {
9906
                $overlap_color = 'red';
9907
            }
9908
9909
            if ($missing_color) {
9910
                $missing_color = 'green';
9911
            } else {
9912
                $missing_color = 'red';
9913
            }
9914
            if ($excess_color) {
9915
                $excess_color = 'green';
9916
            } else {
9917
                $excess_color = 'red';
9918
            }
9919
9920
            if (!is_numeric($final_overlap)) {
9921
                $final_overlap = 0;
9922
            }
9923
9924
            if (!is_numeric($final_missing)) {
9925
                $final_missing = 0;
9926
            }
9927
            if (!is_numeric($final_excess)) {
9928
                $final_excess = 0;
9929
            }
9930
9931
            if ($final_excess > 100) {
9932
                $final_excess = 100;
9933
            }
9934
9935
            $table_resume = '
9936
                    <table class="table table-hover table-striped data_table">
9937
                        <tr class="row_odd" >
9938
                            <td>&nbsp;</td>
9939
                            <td><b>'.get_lang('Requirements').'</b></td>
9940
                            <td><b>'.get_lang('Your answer').'</b></td>
9941
                        </tr>
9942
                        <tr class="row_even">
9943
                            <td><b>'.get_lang('Overlapping area').'</b></td>
9944
                            <td>'.get_lang('Minimum overlap').' '.$threadhold1.'</td>
9945
                            <td>
9946
                                <div style="color:'.$overlap_color.'">
9947
                                    '.(($final_overlap < 0) ? 0 : intval($final_overlap)).'
9948
                                </div>
9949
                            </td>
9950
                        </tr>
9951
                        <tr>
9952
                            <td><b>'.get_lang('Excessive area').'</b></td>
9953
                            <td>'.get_lang('Maximum excess').' '.$threadhold2.'</td>
9954
                            <td>
9955
                                <div style="color:'.$excess_color.'">
9956
                                    '.(($final_excess < 0) ? 0 : intval($final_excess)).'
9957
                                </div>
9958
                            </td>
9959
                        </tr>
9960
                        <tr class="row_even">
9961
                            <td><b>'.get_lang('Missing area').'</b></td>
9962
                            <td>'.get_lang('Maximum missing').' '.$threadhold3.'</td>
9963
                            <td>
9964
                                <div style="color:'.$missing_color.'">
9965
                                    '.(($final_missing < 0) ? 0 : intval($final_missing)).'
9966
                                </div>
9967
                            </td>
9968
                        </tr>
9969
                    </table>
9970
                ';
9971
9972
            $answerType = $objQuestionTmp->selectType();
9973
            /*if ($next == 0) {
9974
                $try = $try_hotspot;
9975
                $lp = $lp_hotspot;
9976
                $destinationid = $select_question_hotspot;
9977
                $url = $url_hotspot;
9978
            } else {
9979
                //show if no error
9980
                $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
9981
                $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
9982
            }
9983
            echo '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>';
9984
            if ($organs_at_risk_hit > 0) {
9985
                $message = '<br />'.get_lang('Your result is :').' <b>'.$result_comment.'</b><br />';
9986
                $message .= '<p style="color:#DC0A0A;"><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
9987
            } else {
9988
                $message = '<p>'.get_lang('Your delineation :').'</p>';
9989
                $message .= $table_resume;
9990
                $message .= '<br />'.get_lang('Your result is :').' <b>'.$result_comment.'</b><br />';
9991
            }
9992
            $message .= '<p>'.$comment.'</p>';
9993
            echo $message;*/
9994
9995
            // Showing the score
9996
            /*$queryfree = "SELECT marks FROM $TBL_TRACK_ATTEMPT
9997
                          WHERE exe_id = $id AND question_id =  $questionId";
9998
            $resfree = Database::query($queryfree);
9999
            $questionScore = Database::result($resfree, 0, 'marks');
10000
            $totalScore += $questionScore;*/
10001
            $relPath = api_get_path(REL_CODE_PATH);
10002
            echo '</table></td></tr>';
10003
            echo "
10004
                        <tr>
10005
                            <td colspan=\"2\">
10006
                                <div id=\"hotspot-solution\"></div>
10007
                                <script>
10008
                                    $(function() {
10009
                                        new HotspotQuestion({
10010
                                            questionId: $questionId,
10011
                                            exerciseId: {$this->id},
10012
                                            exeId: $id,
10013
                                            selector: '#hotspot-solution',
10014
                                            for: 'solution',
10015
                                            relPath: '$relPath'
10016
                                        });
10017
                                    });
10018
                                </script>
10019
                            </td>
10020
                        </tr>
10021
                    </table>
10022
                ";
10023
        }
10024
    }
10025
10026
    /**
10027
     * Clean exercise session variables.
10028
     */
10029
    public static function cleanSessionVariables()
10030
    {
10031
        Session::erase('objExercise');
10032
        Session::erase('exe_id');
10033
        Session::erase('calculatedAnswerId');
10034
        Session::erase('duration_time_previous');
10035
        Session::erase('duration_time');
10036
        Session::erase('objQuestion');
10037
        Session::erase('objAnswer');
10038
        Session::erase('questionList');
10039
        Session::erase('categoryList');
10040
        Session::erase('exerciseResult');
10041
        Session::erase('firstTime');
10042
10043
        Session::erase('time_per_question');
10044
        Session::erase('question_start');
10045
        Session::erase('exerciseResultCoordinates');
10046
        Session::erase('hotspot_coord');
10047
        Session::erase('hotspot_dest');
10048
        Session::erase('hotspot_delineation_result');
10049
    }
10050
10051
    /**
10052
     * Get the first LP found matching the session ID.
10053
     *
10054
     * @param int $sessionId
10055
     *
10056
     * @return array
10057
     */
10058
    public function getLpBySession($sessionId)
10059
    {
10060
        if (!empty($this->lpList)) {
10061
            $sessionId = (int) $sessionId;
10062
10063
            foreach ($this->lpList as $lp) {
10064
                if (isset($lp['session_id']) && (int) $lp['session_id'] == $sessionId) {
10065
                    return $lp;
10066
                }
10067
            }
10068
10069
            return current($this->lpList);
10070
        }
10071
10072
        return [
10073
            'lp_id' => 0,
10074
            'max_score' => 0,
10075
            'session_id' => 0,
10076
        ];
10077
    }
10078
10079
    public static function saveExerciseInLp($safe_item_id, $safe_exe_id, $course_id = null)
10080
    {
10081
        $lp = Session::read('oLP');
10082
10083
        $safe_exe_id = (int) $safe_exe_id;
10084
        $safe_item_id = (int) $safe_item_id;
10085
10086
        if (empty($lp) || empty($safe_exe_id) || empty($safe_item_id)) {
10087
            return false;
10088
        }
10089
10090
        $viewId = $lp->get_view_id();
10091
        if (!isset($course_id)) {
10092
            $course_id = api_get_course_int_id();
10093
        }
10094
        $userId = (int) api_get_user_id();
10095
        $viewId = (int) $viewId;
10096
10097
        $TBL_TRACK_EXERCICES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
10098
        $TBL_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
10099
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
10100
10101
        $sql = "SELECT start_date, exe_date, score, max_score, exe_exo_id, exe_duration
10102
                FROM $TBL_TRACK_EXERCICES
10103
                WHERE exe_id = $safe_exe_id AND exe_user_id = $userId";
10104
        $res = Database::query($sql);
10105
        $row_dates = Database::fetch_array($res);
10106
10107
        if (empty($row_dates)) {
10108
            return false;
10109
        }
10110
10111
        $duration = (int) $row_dates['exe_duration'];
10112
        $score = (float) $row_dates['score'];
10113
        $max_score = (float) $row_dates['max_score'];
10114
10115
        $sql = "UPDATE $TBL_LP_ITEM SET
10116
                    max_score = '$max_score'
10117
                WHERE iid = $safe_item_id";
10118
        Database::query($sql);
10119
10120
        $sql = "SELECT iid FROM $TBL_LP_ITEM_VIEW
10121
                WHERE
10122
                    lp_item_id = $safe_item_id AND
10123
                    lp_view_id = $viewId
10124
                ORDER BY iid DESC
10125
                LIMIT 1";
10126
        $res_last_attempt = Database::query($sql);
10127
10128
        if (Database::num_rows($res_last_attempt) && !api_is_invitee()) {
10129
            $row_last_attempt = Database::fetch_row($res_last_attempt);
10130
            $lp_item_view_id = $row_last_attempt[0];
10131
10132
            $exercise = new Exercise($course_id);
10133
            $exercise->read($row_dates['exe_exo_id']);
10134
            $status = 'completed';
10135
10136
            if (!empty($exercise->pass_percentage)) {
10137
                $status = 'failed';
10138
                $success = ExerciseLib::isSuccessExerciseResult(
10139
                    $score,
10140
                    $max_score,
10141
                    $exercise->pass_percentage
10142
                );
10143
                if ($success) {
10144
                    $status = 'passed';
10145
                }
10146
            }
10147
10148
            $sql = "UPDATE $TBL_LP_ITEM_VIEW SET
10149
                        status = '$status',
10150
                        score = '$score',
10151
                        total_time = '$duration'
10152
                    WHERE iid = $lp_item_view_id";
10153
            Database::query($sql);
10154
10155
            $sql = "UPDATE $TBL_TRACK_EXERCICES SET
10156
                        orig_lp_item_view_id = '$lp_item_view_id'
10157
                    WHERE exe_id = ".$safe_exe_id;
10158
            Database::query($sql);
10159
        }
10160
    }
10161
10162
    /**
10163
     * Get the user answers saved in exercise.
10164
     *
10165
     * @param int $attemptId
10166
     *
10167
     * @return array
10168
     */
10169
    public function getUserAnswersSavedInExercise($attemptId)
10170
    {
10171
        $exerciseResult = [];
10172
10173
        $attemptList = Event::getAllExerciseEventByExeId($attemptId);
10174
10175
        foreach ($attemptList as $questionId => $options) {
10176
            foreach ($options as $option) {
10177
                $question = Question::read($option['question_id']);
10178
10179
                if ($question) {
10180
                    switch ($question->type) {
10181
                        case FILL_IN_BLANKS:
10182
                        case FILL_IN_BLANKS_COMBINATION:
10183
                            $option['answer'] = $this->fill_in_blank_answer_to_string($option['answer']);
10184
                            if ($option['answer'] === "0") {
10185
                                $option['answer'] = "there is 0 as answer so we do not want to consider it empty";
10186
                            }
10187
                            break;
10188
                    }
10189
                }
10190
10191
                if (!empty($option['answer'])) {
10192
                    $exerciseResult[] = $questionId;
10193
10194
                    break;
10195
                }
10196
            }
10197
        }
10198
10199
        return $exerciseResult;
10200
    }
10201
10202
    /**
10203
     * Get the number of user answers saved in exercise.
10204
     *
10205
     * @param int $attemptId
10206
     *
10207
     * @return int
10208
     */
10209
    public function countUserAnswersSavedInExercise($attemptId)
10210
    {
10211
        $answers = $this->getUserAnswersSavedInExercise($attemptId);
10212
10213
        return count($answers);
10214
    }
10215
10216
    public static function allowAction($action)
10217
    {
10218
        if (api_is_platform_admin()) {
10219
            return true;
10220
        }
10221
10222
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
10223
        $disableClean = ('true' === api_get_setting('exercise.disable_clean_exercise_results_for_teachers'));
10224
10225
        switch ($action) {
10226
            case 'delete':
10227
                if (api_is_allowed_to_edit(null, true)) {
10228
                    if ($limitTeacherAccess) {
10229
                        return false;
10230
                    }
10231
10232
                    return true;
10233
                }
10234
                break;
10235
            case 'clean_results':
10236
                if (api_is_allowed_to_edit(null, true)) {
10237
                    if ($limitTeacherAccess) {
10238
                        return false;
10239
                    }
10240
10241
                    if ($disableClean) {
10242
                        return false;
10243
                    }
10244
10245
                    return true;
10246
                }
10247
10248
                break;
10249
        }
10250
10251
        return false;
10252
    }
10253
10254
    public static function getLpListFromExercise($exerciseId, $courseId)
10255
    {
10256
        $tableLpItem = Database::get_course_table(TABLE_LP_ITEM);
10257
        $tblLp = Database::get_course_table(TABLE_LP_MAIN);
10258
10259
        $exerciseId = (int) $exerciseId;
10260
        $courseId = (int) $courseId;
10261
10262
        $sql = "SELECT
10263
                    lp.title,
10264
                    lpi.lp_id,
10265
                    lpi.max_score
10266
                FROM $tableLpItem lpi
10267
                INNER JOIN $tblLp lp
10268
                ON (lpi.lp_id = lp.iid)
10269
                WHERE
10270
                    lpi.item_type = '".TOOL_QUIZ."' AND
10271
                    lpi.path = '$exerciseId'";
10272
        $result = Database::query($sql);
10273
        $lpList = [];
10274
        if (Database::num_rows($result) > 0) {
10275
            $lpList = Database::store_result($result, 'ASSOC');
10276
        }
10277
10278
        return $lpList;
10279
    }
10280
10281
    /**
10282
     * Generate reminder table with robust checkbox markup (no external CSS dependency).
10283
     */
10284
    function getReminderTable($questionList, $exercise_stat_info, $disableCheckBoxes = false)
10285
    {
10286
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10287
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10288
        $learnpath_item_view_id = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10289
        $categoryId = isset($_REQUEST['category_id']) ? (int) $_REQUEST['category_id'] : 0;
10290
10291
        if (empty($exercise_stat_info)) {
10292
            return '';
10293
        }
10294
10295
        $remindList = $exercise_stat_info['questions_to_check'];
10296
        $remindList = explode(',', $remindList);
10297
10298
        $exeId = $exercise_stat_info['exe_id'];
10299
        $exerciseId = $exercise_stat_info['exe_exo_id'];
10300
        $exercise_result = Exercise::getUserAnswersSavedInExerciseStatic($exeId); // Static helper to avoid $this here
10301
10302
        $content = Display::label(get_lang('Questions without answer'), 'danger');
10303
        $content .= '<div class="clear"></div><br />';
10304
        $table = '';
10305
        $counter = 0;
10306
10307
        // Loop over all questions
10308
        foreach ($questionList as $questionId) {
10309
            $objQuestionTmp = Question::read($questionId);
10310
            $check_id = 'remind_list['.$questionId.']';
10311
            $attributes = [
10312
                'id' => $check_id,
10313
                'onclick' => "save_remind_item(this, '$questionId');",
10314
                'data-question-id' => $questionId,
10315
            ];
10316
            if (in_array($questionId, $remindList)) {
10317
                $attributes['checked'] = 1;
10318
            }
10319
10320
            // Simple, robust checkbox (no pretty-checkbox wrapper)
10321
            $checkbox = Display::input('checkbox', 'remind_list['.$questionId.']', '1', $attributes);
10322
10323
            $counter++;
10324
            $titleText = $counter.'. '.strip_tags($objQuestionTmp->selectTitle());
10325
10326
            // Mark unanswered questions
10327
            if (!in_array($questionId, $exercise_result)) {
10328
                $titleHtml = Display::label($titleText, 'danger');
10329
            } else {
10330
                $titleHtml = $titleText;
10331
            }
10332
10333
            $label_attributes = ['for' => $check_id];
10334
10335
            if (false === $disableCheckBoxes) {
10336
                // Combine checkbox + title inside a label; Tailwind classes are no-op if not present
10337
                $rowHtml = Display::tag(
10338
                    'label',
10339
                    $checkbox.' '.$titleHtml,
10340
                    $label_attributes
10341
                );
10342
            } else {
10343
                $rowHtml = $titleHtml;
10344
            }
10345
10346
            // Add container with alignment classes (works w/ or w/o Tailwind thanks to inline CSS above)
10347
            $table .= Display::div($rowHtml, ['class' => 'exercise_reminder_item flex items-center gap-2']);
10348
        }
10349
10350
        // JS functions
10351
        $content .= Display::div('', ['id' => 'message']).
10352
            Display::div($table, ['class' => 'question-check-test']);
10353
10354
        $content .= '<script>
10355
    var lp_data = $.param({
10356
        "learnpath_id": '.$learnpath_id.',
10357
        "learnpath_item_id" : '.$learnpath_item_id.',
10358
        "learnpath_item_view_id": '.$learnpath_item_view_id.'
10359
    });
10360
10361
    function final_submit() {
10362
        // Normal inputs.
10363
        window.location = "'.api_get_path(WEB_CODE_PATH).'exercise/exercise_result.php?'.api_get_cidreq().'&exe_id='.$exeId.'&" + lp_data;
10364
    }
10365
10366
    function changeOptionStatus(status)
10367
    {
10368
        $("input[type=checkbox]").each(function () {
10369
            $(this).prop("checked", status);
10370
        });
10371
10372
        var action = "";
10373
        var option = "remove_all";
10374
        if (status == 1) {
10375
            option = "add_all";
10376
        }
10377
        $.ajax({
10378
            url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10379
            data: "option="+option+"&exe_id='.$exeId.'&action="+action,
10380
            success: function(returnValue) {}
10381
        });
10382
    }
10383
10384
    function reviewQuestions() {
10385
        var hasChecked = false;
10386
        $("input[type=checkbox]").each(function () {
10387
            if ($(this).prop("checked")) {
10388
                hasChecked = true;
10389
                return false;
10390
            }
10391
        });
10392
10393
        if (!hasChecked) {
10394
            $("#message").addClass("warning-message");
10395
            $("#message").html("'.addslashes(get_lang('Select a question to revise')).'");
10396
        } else {
10397
            window.location = "exercise_submit.php?'.api_get_cidreq().'&category_id='.$categoryId.'&exerciseId='.$exerciseId.'&reminder=2&" + lp_data;
10398
        }
10399
    }
10400
10401
    // Backward-compat alias (the button calls review_questions())
10402
    function review_questions() {
10403
        return reviewQuestions();
10404
    }
10405
10406
    function save_remind_item(obj, question_id) {
10407
        var action = $(obj).prop("checked") ? "add" : "delete";
10408
        $.ajax({
10409
            url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10410
            data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
10411
            success: function(returnValue) {}
10412
        });
10413
    }
10414
    </script>';
10415
10416
        return $content;
10417
    }
10418
10419
    public static function getUserAnswersSavedInExerciseStatic($exeId)
10420
    {
10421
        $objExercise = Session::read("objExercise");
10422
        if ($objExercise && method_exists($objExercise, "getUserAnswersSavedInExercise")) {
10423
            return $objExercise->getUserAnswersSavedInExercise($exeId);
10424
        }
10425
10426
        return [];
10427
    }
10428
10429
    public function getRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10430
    {
10431
        $dataSet = [];
10432
        $labels = [];
10433
        $labelsWithId = [];
10434
        /** @var Exercise $exercise */
10435
        foreach ($exercises as $exercise) {
10436
            if (empty($labels)) {
10437
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10438
                if (!empty($categoryNameList)) {
10439
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10440
                    asort($labelsWithId);
10441
                    $labels = array_values($labelsWithId);
10442
                }
10443
            }
10444
10445
            foreach ($userList as $userId) {
10446
                $results = Event::getExerciseResultsByUser(
10447
                    $userId,
10448
                    $exercise->iId,
10449
                    $courseId,
10450
                    $sessionId
10451
                );
10452
10453
                if ($results) {
10454
                    $firstAttempt = end($results);
10455
                    $exeId = $firstAttempt['exe_id'];
10456
10457
                    ob_start();
10458
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10459
                        $exercise,
10460
                        $exeId,
10461
                        false
10462
                    );
10463
                    ob_end_clean();
10464
10465
                    $categoryList = $stats['category_list'];
10466
                    $tempResult = [];
10467
                    foreach ($labelsWithId as $category_id => $title) {
10468
                        if (isset($categoryList[$category_id])) {
10469
                            $category_item = $categoryList[$category_id];
10470
                            $tempResult[] = round($category_item['score'] / $category_item['total'] * 10);
10471
                        } else {
10472
                            $tempResult[] = 0;
10473
                        }
10474
                    }
10475
                    $dataSet[] = $tempResult;
10476
                }
10477
            }
10478
        }
10479
10480
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10481
    }
10482
10483
    public function getAverageRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10484
    {
10485
        $dataSet = [];
10486
        $labels = [];
10487
        $labelsWithId = [];
10488
10489
        $tempResult = [];
10490
        /** @var Exercise $exercise */
10491
        foreach ($exercises as $exercise) {
10492
            $exerciseId = $exercise->iId;
10493
            if (empty($labels)) {
10494
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10495
                if (!empty($categoryNameList)) {
10496
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10497
                    asort($labelsWithId);
10498
                    $labels = array_values($labelsWithId);
10499
                }
10500
            }
10501
10502
            foreach ($userList as $userId) {
10503
                $results = Event::getExerciseResultsByUser(
10504
                    $userId,
10505
                    $exerciseId,
10506
                    $courseId,
10507
                    $sessionId
10508
                );
10509
10510
                if ($results) {
10511
                    $firstAttempt = end($results);
10512
                    $exeId = $firstAttempt['exe_id'];
10513
10514
                    ob_start();
10515
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10516
                        $exercise,
10517
                        $exeId,
10518
                        false
10519
                    );
10520
                    ob_end_clean();
10521
10522
                    $categoryList = $stats['category_list'];
10523
                    foreach ($labelsWithId as $category_id => $title) {
10524
                        if (isset($categoryList[$category_id])) {
10525
                            $category_item = $categoryList[$category_id];
10526
                            if (!isset($tempResult[$exerciseId][$category_id])) {
10527
                                $tempResult[$exerciseId][$category_id] = 0;
10528
                            }
10529
                            $tempResult[$exerciseId][$category_id] += $category_item['score'] / $category_item['total'] * 10;
10530
                        }
10531
                    }
10532
                }
10533
            }
10534
        }
10535
10536
        $totalUsers = count($userList);
10537
10538
        foreach ($exercises as $exercise) {
10539
            $exerciseId = $exercise->iId;
10540
            $data = [];
10541
            foreach ($labelsWithId as $category_id => $title) {
10542
                if (isset($tempResult[$exerciseId]) && isset($tempResult[$exerciseId][$category_id])) {
10543
                    $data[] = round($tempResult[$exerciseId][$category_id] / $totalUsers);
10544
                } else {
10545
                    $data[] = 0;
10546
                }
10547
            }
10548
            $dataSet[] = $data;
10549
        }
10550
10551
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10552
    }
10553
10554
    public function getRadar($labels, $dataSet, $dataSetLabels = [])
10555
    {
10556
        if (empty($labels) || empty($dataSet)) {
10557
            return '';
10558
        }
10559
10560
        $displayLegend = 0;
10561
        if (!empty($dataSetLabels)) {
10562
            $displayLegend = 1;
10563
        }
10564
10565
        $labels = json_encode($labels);
10566
10567
        $colorList = ChamiloHelper::getColorPalette(true, true);
10568
10569
        $dataSetToJson = [];
10570
        $counter = 0;
10571
        foreach ($dataSet as $index => $resultsArray) {
10572
            $color = isset($colorList[$counter]) ? $colorList[$counter] : 'rgb('.rand(0, 255).', '.rand(0, 255).', '.rand(0, 255).', 1.0)';
10573
10574
            $label = isset($dataSetLabels[$index]) ? $dataSetLabels[$index] : '';
10575
            $background = str_replace('1.0', '0.2', $color);
10576
            $dataSetToJson[] = [
10577
                'fill' => false,
10578
                'label' => $label,
10579
                'backgroundColor' => $background,
10580
                'borderColor' => $color,
10581
                'pointBackgroundColor' => $color,
10582
                'pointBorderColor' => '#fff',
10583
                'pointHoverBackgroundColor' => '#fff',
10584
                'pointHoverBorderColor' => $color,
10585
                'pointRadius' => 6,
10586
                'pointBorderWidth' => 3,
10587
                'pointHoverRadius' => 10,
10588
                'data' => $resultsArray,
10589
            ];
10590
            $counter++;
10591
        }
10592
        $resultsToJson = json_encode($dataSetToJson);
10593
10594
        return "
10595
                <canvas id='categoryRadar' height='200'></canvas>
10596
                <script>
10597
                    var data = {
10598
                        labels: $labels,
10599
                        datasets: $resultsToJson
10600
                    }
10601
                    var options = {
10602
                        responsive: true,
10603
                        scale: {
10604
                            angleLines: {
10605
                                display: false
10606
                            },
10607
                            ticks: {
10608
                                beginAtZero: true,
10609
                                  min: 0,
10610
                                  max: 10,
10611
                                stepSize: 1,
10612
                            },
10613
                            pointLabels: {
10614
                              fontSize: 14,
10615
                              //fontStyle: 'bold'
10616
                            },
10617
                        },
10618
                        elements: {
10619
                            line: {
10620
                                tension: 0,
10621
                                borderWidth: 3
10622
                            }
10623
                        },
10624
                        legend: {
10625
                            //position: 'bottom'
10626
                            display: $displayLegend
10627
                        },
10628
                        animation: {
10629
                            animateScale: true,
10630
                            animateRotate: true
10631
                        },
10632
                    };
10633
                    var ctx = document.getElementById('categoryRadar').getContext('2d');
10634
                    var myRadarChart = new Chart(ctx, {
10635
                        type: 'radar',
10636
                        data: data,
10637
                        options: options
10638
                    });
10639
                </script>
10640
                ";
10641
    }
10642
10643
    /**
10644
     * Returns true if the exercise is locked by percentage. an exercise attempt must be passed.
10645
     */
10646
    public function isBlockedByPercentage(array $attempt = []): bool
10647
    {
10648
        if (empty($attempt)) {
10649
            return false;
10650
        }
10651
        $extraFieldValue = new ExtraFieldValue('exercise');
10652
        $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
10653
            $this->iId,
10654
            'blocking_percentage'
10655
        );
10656
10657
        if (empty($blockExercise['value'])) {
10658
            return false;
10659
        }
10660
10661
        $blockPercentage = (int) $blockExercise['value'];
10662
10663
        if (0 === $blockPercentage) {
10664
            return false;
10665
        }
10666
10667
        $resultPercentage = 0;
10668
10669
        if (isset($attempt['score']) && isset($attempt['max_score'])) {
10670
            $weight = (int) $attempt['max_score'];
10671
            $weight = (0 == $weight) ? 1 : $weight;
10672
            $resultPercentage = float_format(
10673
                ($attempt['score'] / $weight) * 100,
10674
                1
10675
            );
10676
        }
10677
        if ($resultPercentage <= $blockPercentage) {
10678
            return true;
10679
        }
10680
10681
        return false;
10682
    }
10683
10684
    /**
10685
     * Gets the question list ordered by the question_order setting (drag and drop).
10686
     *
10687
     * @param bool $adminView Optional.
10688
     *
10689
     * @return array
10690
     */
10691
    public function getQuestionOrderedList($adminView = false)
10692
    {
10693
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
10694
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
10695
10696
        // Getting question_order to verify that the question
10697
        // list is correct and all question_order's were set
10698
        $sql = "SELECT DISTINCT count(e.question_order) as count
10699
                FROM $TBL_EXERCICE_QUESTION e
10700
                INNER JOIN $TBL_QUESTIONS q
10701
                ON (e.question_id = q.iid)
10702
                WHERE
10703
                  e.quiz_id	= ".$this->getId();
10704
10705
        $result = Database::query($sql);
10706
        $row = Database::fetch_array($result);
10707
        $count_question_orders = $row['count'];
10708
10709
        // Getting question list from the order (question list drag n drop interface).
10710
        $sql = "SELECT DISTINCT e.question_id, e.question_order
10711
                FROM $TBL_EXERCICE_QUESTION e
10712
                INNER JOIN $TBL_QUESTIONS q
10713
                ON (e.question_id = q.iid)
10714
                WHERE
10715
10716
                    e.quiz_id = '".$this->getId()."'
10717
                ORDER BY question_order";
10718
        $result = Database::query($sql);
10719
10720
        // Fills the array with the question ID for this exercise
10721
        // the key of the array is the question position
10722
        $temp_question_list = [];
10723
        $counter = 1;
10724
        $questionList = [];
10725
        while ($new_object = Database::fetch_object($result)) {
10726
            if (!$adminView) {
10727
                // Correct order.
10728
                $questionList[$new_object->question_order] = $new_object->question_id;
10729
            } else {
10730
                $questionList[$counter] = $new_object->question_id;
10731
            }
10732
10733
            // Just in case we save the order in other array
10734
            $temp_question_list[$counter] = $new_object->question_id;
10735
            $counter++;
10736
        }
10737
10738
        if (!empty($temp_question_list)) {
10739
            /* If both array don't match it means that question_order was not correctly set
10740
               for all questions using the default mysql order */
10741
            if (count($temp_question_list) != $count_question_orders) {
10742
                $questionList = $temp_question_list;
10743
            }
10744
        }
10745
10746
        return $questionList;
10747
    }
10748
10749
    /**
10750
     * Get number of questions in exercise by user attempt.
10751
     *
10752
     * @return int
10753
     */
10754
    private function countQuestionsInExercise()
10755
    {
10756
        $lpId = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10757
        $lpItemId = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10758
        $lpItemViewId = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10759
10760
        $trackInfo = $this->get_stat_track_exercise_info($lpId, $lpItemId, $lpItemViewId);
10761
10762
        if (!empty($trackInfo)) {
10763
            $questionIds = explode(',', $trackInfo['data_tracking']);
10764
10765
            return count($questionIds);
10766
        }
10767
10768
        return $this->getQuestionCount();
10769
    }
10770
10771
    /**
10772
     * Select N values from the questions per category array.
10773
     *
10774
     * @param array $categoriesAddedInExercise
10775
     * @param array $question_list
10776
     * @param array $questions_by_category
10777
     * @param bool  $flatResult
10778
     * @param bool  $randomizeQuestions
10779
     * @param array $questionsByCategoryMandatory
10780
     *
10781
     * @return array
10782
     */
10783
    private function pickQuestionsPerCategory(
10784
        $categoriesAddedInExercise,
10785
        $question_list,
10786
        &$questions_by_category,
10787
        $flatResult = true,
10788
        $randomizeQuestions = false,
10789
        $questionsByCategoryMandatory = []
10790
    ) {
10791
        $addAll = true;
10792
        $categoryCountArray = [];
10793
10794
        // Getting how many questions will be selected per category.
10795
        if (!empty($categoriesAddedInExercise)) {
10796
            $addAll = false;
10797
            // Parsing question according the category rel exercise settings
10798
            foreach ($categoriesAddedInExercise as $category_info) {
10799
                $category_id = $category_info['category_id'];
10800
                if (isset($questions_by_category[$category_id])) {
10801
                    // How many question will be picked from this category.
10802
                    $count = $category_info['count_questions'];
10803
                    // -1 means all questions
10804
                    $categoryCountArray[$category_id] = $count;
10805
                    if (-1 == $count) {
10806
                        $categoryCountArray[$category_id] = 999;
10807
                    }
10808
                }
10809
            }
10810
        }
10811
10812
        if (!empty($questions_by_category)) {
10813
            $temp_question_list = [];
10814
            foreach ($questions_by_category as $category_id => &$categoryQuestionList) {
10815
                if (isset($categoryCountArray) && !empty($categoryCountArray)) {
10816
                    $numberOfQuestions = 0;
10817
                    if (isset($categoryCountArray[$category_id])) {
10818
                        $numberOfQuestions = $categoryCountArray[$category_id];
10819
                    }
10820
                }
10821
10822
                if ($addAll) {
10823
                    $numberOfQuestions = 999;
10824
                }
10825
                if (!empty($numberOfQuestions)) {
10826
                    $mandatoryQuestions = [];
10827
                    if (isset($questionsByCategoryMandatory[$category_id])) {
10828
                        $mandatoryQuestions = $questionsByCategoryMandatory[$category_id];
10829
                    }
10830
10831
                    $elements = TestCategory::getNElementsFromArray(
10832
                        $categoryQuestionList,
10833
                        $numberOfQuestions,
10834
                        $randomizeQuestions,
10835
                        $mandatoryQuestions
10836
                    );
10837
10838
                    if (!empty($elements)) {
10839
                        $temp_question_list[$category_id] = $elements;
10840
                        $categoryQuestionList = $elements;
10841
                    }
10842
                }
10843
            }
10844
10845
            if (!empty($temp_question_list)) {
10846
                if ($flatResult) {
10847
                    $temp_question_list = array_flatten($temp_question_list);
10848
                }
10849
                $question_list = $temp_question_list;
10850
            }
10851
        }
10852
10853
        return $question_list;
10854
    }
10855
10856
    /**
10857
     * Sends a notification when a user ends an examn.
10858
     *
10859
     * @param array  $question_list_answers
10860
     * @param string $origin
10861
     * @param array  $user_info
10862
     * @param string $url_email
10863
     * @param array  $teachers
10864
     */
10865
    private function sendNotificationForOpenQuestions(
10866
        $question_list_answers,
10867
        $origin,
10868
        $user_info,
10869
        $url_email,
10870
        $teachers
10871
    ) {
10872
        // Email configuration settings
10873
        $courseCode = api_get_course_id();
10874
        $courseInfo = api_get_course_info($courseCode);
10875
        $sessionId = api_get_session_id();
10876
        $sessionData = '';
10877
        if (!empty($sessionId)) {
10878
            $sessionInfo = api_get_session_info($sessionId);
10879
            if (!empty($sessionInfo)) {
10880
                $sessionData = '<tr>'
10881
                    .'<td><em>'.get_lang('Session name').'</em></td>'
10882
                    .'<td>&nbsp;<b>'.$sessionInfo['name'].'</b></td>'
10883
                    .'</tr>';
10884
            }
10885
        }
10886
10887
        $msg = get_lang('A learner has answered an open question').'<br /><br />'
10888
            .get_lang('Attempt details').' : <br /><br />'
10889
            .'<table>'
10890
            .'<tr>'
10891
            .'<td><em>'.get_lang('Course name').'</em></td>'
10892
            .'<td>&nbsp;<b>#course#</b></td>'
10893
            .'</tr>'
10894
            .$sessionData
10895
            .'<tr>'
10896
            .'<td>'.get_lang('Test attempted').'</td>'
10897
            .'<td>&nbsp;#exercise#</td>'
10898
            .'</tr>'
10899
            .'<tr>'
10900
            .'<td>'.get_lang('Learner name').'</td>'
10901
            .'<td>&nbsp;#firstName# #lastName#</td>'
10902
            .'</tr>'
10903
            .'<tr>'
10904
            .'<td>'.get_lang('Learner e-mail').'</td>'
10905
            .'<td>&nbsp;#mail#</td>'
10906
            .'</tr>'
10907
            .'</table>';
10908
10909
        $open_question_list = null;
10910
        foreach ($question_list_answers as $item) {
10911
            $question = $item['question'];
10912
            $answer = $item['answer'];
10913
            $answer_type = $item['answer_type'];
10914
10915
            if (!empty($question) && !empty($answer) && FREE_ANSWER == $answer_type) {
10916
                $open_question_list .=
10917
                    '<tr>
10918
                    <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
10919
                    <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
10920
                    </tr>
10921
                    <tr>
10922
                    <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
10923
                    <td valign="top" bgcolor="#F3F3F3">'.$answer.'</td>
10924
                    </tr>';
10925
            }
10926
        }
10927
10928
        if (!empty($open_question_list)) {
10929
            $msg .= '<p><br />'.get_lang('A learner has answered an open question').' :</p>'.
10930
                '<table width="730" height="136" border="0" cellpadding="3" cellspacing="3">';
10931
            $msg .= $open_question_list;
10932
            $msg .= '</table><br />';
10933
10934
            $msg = str_replace('#exercise#', $this->exercise, $msg);
10935
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg);
10936
            $msg = str_replace('#lastName#', $user_info['lastname'], $msg);
10937
            $msg = str_replace('#mail#', $user_info['email'], $msg);
10938
            $msg = str_replace(
10939
                '#course#',
10940
                Display::url($courseInfo['title'], $courseInfo['course_public_url'].'?sid='.$sessionId),
10941
                $msg
10942
            );
10943
10944
            if ('learnpath' !== $origin) {
10945
                $msg .= '<br /><a href="#url#">'.get_lang(
10946
                        'Click this link to check the answer and/or give feedback'
10947
                    ).'</a>';
10948
            }
10949
            $msg = str_replace('#url#', $url_email, $msg);
10950
            $subject = get_lang('A learner has answered an open question');
10951
10952
            if (!empty($teachers)) {
10953
                foreach ($teachers as $user_id => $teacher_data) {
10954
                    MessageManager::send_message_simple(
10955
                        $user_id,
10956
                        $subject,
10957
                        $msg
10958
                    );
10959
                }
10960
            }
10961
        }
10962
    }
10963
10964
    /**
10965
     * Send notification for oral questions.
10966
     *
10967
     * @param array  $question_list_answers
10968
     * @param string $origin
10969
     * @param int    $exe_id
10970
     * @param array  $user_info
10971
     * @param string $url_email
10972
     * @param array  $teachers
10973
     */
10974
    private function sendNotificationForOralQuestions(
10975
        $question_list_answers,
10976
        $origin,
10977
        $exe_id,
10978
        $user_info,
10979
        $url_email,
10980
        $teachers
10981
    ): void {
10982
10983
        // Email configuration settings
10984
        $courseCode = api_get_course_id();
10985
        $courseInfo = api_get_course_info($courseCode);
10986
        $oral_question_list = null;
10987
        foreach ($question_list_answers as $item) {
10988
            $question = $item['question'];
10989
            $file = $item['generated_oral_file'];
10990
            $answer = $item['answer'];
10991
            if (0 == $answer) {
10992
                $answer = '';
10993
            }
10994
            $answer_type = $item['answer_type'];
10995
            if (!empty($question) && (!empty($answer) || !empty($file)) && ORAL_EXPRESSION == $answer_type) {
10996
                $oral_question_list .= '<br />
10997
                    <table width="730" height="136" border="0" cellpadding="3" cellspacing="3">
10998
                    <tr>
10999
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
11000
                        <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
11001
                    </tr>
11002
                    <tr>
11003
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
11004
                        <td valign="top" bgcolor="#F3F3F3"><p>'.$answer.'</p><p>'.$file.'</p></td>
11005
                    </tr></table>';
11006
            }
11007
        }
11008
11009
        if (!empty($oral_question_list)) {
11010
            $msg = get_lang('A learner has attempted one or more oral question').'<br /><br />
11011
                    '.get_lang('Attempt details').' : <br /><br />
11012
                    <table>
11013
                        <tr>
11014
                            <td><em>'.get_lang('Course name').'</em></td>
11015
                            <td>&nbsp;<b>#course#</b></td>
11016
                        </tr>
11017
                        <tr>
11018
                            <td>'.get_lang('Test attempted').'</td>
11019
                            <td>&nbsp;#exercise#</td>
11020
                        </tr>
11021
                        <tr>
11022
                            <td>'.get_lang('Learner name').'</td>
11023
                            <td>&nbsp;#firstName# #lastName#</td>
11024
                        </tr>
11025
                        <tr>
11026
                            <td>'.get_lang('Learner e-mail').'</td>
11027
                            <td>&nbsp;#mail#</td>
11028
                        </tr>
11029
                    </table>';
11030
            $msg .= '<br />'.sprintf(
11031
                    get_lang('The attempted oral questions are %s'),
11032
                    $oral_question_list
11033
                ).'<br />';
11034
            $msg1 = str_replace('#exercise#', $this->exercise, $msg);
11035
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg1);
11036
            $msg1 = str_replace('#lastName#', $user_info['lastname'], $msg);
11037
            $msg = str_replace('#mail#', $user_info['email'], $msg1);
11038
            $msg1 = str_replace('#course#', $courseInfo['name'], $msg);
11039
11040
            if (!in_array($origin, ['learnpath', 'embeddable'])) {
11041
                $msg1 .= '<br /><a href="#url#">'.get_lang(
11042
                        'Click this link to check the answer and/or give feedback'
11043
                    ).'</a>';
11044
            }
11045
            $msg = str_replace('#url#', $url_email, $msg1);
11046
            $mail_content = $msg;
11047
            $subject = get_lang('A learner has attempted one or more oral question');
11048
11049
            if (!empty($teachers)) {
11050
                foreach ($teachers as $user_id => $teacher_data) {
11051
                    MessageManager::send_message_simple(
11052
                        $user_id,
11053
                        $subject,
11054
                        $mail_content
11055
                    );
11056
                }
11057
            }
11058
        }
11059
    }
11060
11061
    /**
11062
     * Returns an array with the media list.
11063
     *
11064
     * @param array $questionList question list
11065
     *
11066
     * @example there's 1 question with iid 5 that belongs to the media question with iid = 100
11067
     * <code>
11068
     * array (size=2)
11069
     *  999 =>
11070
     *    array (size=3)
11071
     *      0 => int 7
11072
     *      1 => int 6
11073
     *      2 => int 3254
11074
     *  100 =>
11075
     *   array (size=1)
11076
     *      0 => int 5
11077
     *  </code>
11078
     */
11079
    private function setMediaList($questionList)
11080
    {
11081
        $mediaList = [];
11082
        /*
11083
         * Media feature is not activated in 1.11.x
11084
        if (!empty($questionList)) {
11085
            foreach ($questionList as $questionId) {
11086
                $objQuestionTmp = Question::read($questionId, $this->course_id);
11087
                // If a media question exists
11088
                if (isset($objQuestionTmp->parent_id) && $objQuestionTmp->parent_id != 0) {
11089
                    $mediaList[$objQuestionTmp->parent_id][] = $objQuestionTmp->id;
11090
                } else {
11091
                    // Always the last item
11092
                    $mediaList[999][] = $objQuestionTmp->id;
11093
                }
11094
            }
11095
        }*/
11096
11097
        $this->mediaList = $mediaList;
11098
    }
11099
11100
    /**
11101
     * @return HTML_QuickForm_group
11102
     */
11103
    private function setResultDisabledGroup(FormValidator $form)
11104
    {
11105
        $resultDisabledGroup = [];
11106
11107
        $resultDisabledGroup[] = $form->createElement(
11108
            'radio',
11109
            'results_disabled',
11110
            null,
11111
            get_lang('Auto-evaluation mode: show score and expected answers'),
11112
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
11113
            ['id' => 'result_disabled_0']
11114
        );
11115
11116
        $warning = sprintf(
11117
            get_lang("The setting \"%s\" will change to \"%s\""),
11118
            get_lang('Feedback'),
11119
            get_lang('Exam (no feedback)')
11120
        );
11121
        $resultDisabledGroup[] = $form->createElement(
11122
            'radio',
11123
            'results_disabled',
11124
            null,
11125
            get_lang('Exam mode: Do not show score nor answers'),
11126
            RESULT_DISABLE_NO_SCORE_AND_EXPECTED_ANSWERS,
11127
            [
11128
                'id' => 'result_disabled_1',
11129
                //'onclick' => 'check_results_disabled()'
11130
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
11131
            ]
11132
        );
11133
11134
        $resultDisabledGroup[] = $form->createElement(
11135
            'radio',
11136
            'results_disabled',
11137
            null,
11138
            get_lang('Practice mode: Show score only, by category if at least one is used'),
11139
            RESULT_DISABLE_SHOW_SCORE_ONLY,
11140
            [
11141
                'id' => 'result_disabled_2',
11142
                //'onclick' => 'check_results_disabled()'
11143
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
11144
            ]
11145
        );
11146
11147
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
11148
            return $form->addGroup(
11149
                $resultDisabledGroup,
11150
                null,
11151
                get_lang(
11152
                    'Show score to learner'
11153
                )
11154
            );
11155
        }
11156
11157
        $resultDisabledGroup[] = $form->createElement(
11158
            'radio',
11159
            'results_disabled',
11160
            null,
11161
            get_lang(
11162
                'Show score on every attempt, show correct answers only on last attempt (only works with an attempts limit)'
11163
            ),
11164
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
11165
            ['id' => 'result_disabled_4']
11166
        );
11167
11168
        $resultDisabledGroup[] = $form->createElement(
11169
            'radio',
11170
            'results_disabled',
11171
            null,
11172
            get_lang(
11173
                'Do not show the score (only when user finishes all attempts) but show feedback for each attempt.'
11174
            ),
11175
            RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
11176
            [
11177
                'id' => 'result_disabled_5',
11178
                //'onclick' => 'check_results_disabled()'
11179
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
11180
            ]
11181
        );
11182
11183
        $resultDisabledGroup[] = $form->createElement(
11184
            'radio',
11185
            'results_disabled',
11186
            null,
11187
            get_lang(
11188
                'Ranking mode: Do not show results details question by question and show a table with the ranking of all other users.'
11189
            ),
11190
            RESULT_DISABLE_RANKING,
11191
            ['id' => 'result_disabled_6']
11192
        );
11193
11194
        $resultDisabledGroup[] = $form->createElement(
11195
            'radio',
11196
            'results_disabled',
11197
            null,
11198
            get_lang(
11199
                'Show only global score (not question score) and show only the correct answers, do not show incorrect answers at all'
11200
            ),
11201
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
11202
            ['id' => 'result_disabled_7']
11203
        );
11204
11205
        $resultDisabledGroup[] = $form->createElement(
11206
            'radio',
11207
            'results_disabled',
11208
            null,
11209
            get_lang('Auto-evaluation mode and ranking'),
11210
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
11211
            ['id' => 'result_disabled_8']
11212
        );
11213
11214
        $resultDisabledGroup[] = $form->createElement(
11215
            'radio',
11216
            'results_disabled',
11217
            null,
11218
            get_lang('Show score by category on a radar/spiderweb chart'),
11219
            RESULT_DISABLE_RADAR,
11220
            ['id' => 'result_disabled_9']
11221
        );
11222
11223
        $resultDisabledGroup[] = $form->createElement(
11224
            'radio',
11225
            'results_disabled',
11226
            null,
11227
            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.'),
11228
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
11229
            ['id' => 'result_disabled_10']
11230
        );
11231
11232
        return $form->addGroup(
11233
            $resultDisabledGroup,
11234
            null,
11235
            get_lang('Show score to learner')
11236
        );
11237
    }
11238
11239
    /**
11240
     * Return the text to display, based on the score and the max score.
11241
     * @param int|float $score
11242
     * @param int|float $maxScore
11243
     * @return string
11244
     */
11245
    public function getFinishText(int|float $score, int|float $maxScore): string
11246
    {
11247
        $passPercentage = $this->selectPassPercentage();
11248
        if (!empty($passPercentage)) {
11249
            $percentage = float_format(
11250
                ($score / (0 != $maxScore ? $maxScore : 1)) * 100,
11251
                1
11252
            );
11253
            if ($percentage >= $passPercentage) {
11254
                return $this->getTextWhenFinished();
11255
            } else {
11256
                return $this->getTextWhenFinishedFailure();
11257
            }
11258
        } else {
11259
            return $this->getTextWhenFinished();
11260
        }
11261
11262
        return '';
11263
    }
11264
}
11265