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