Passed
Pull Request — master (#5551)
by
unknown
07:24
created

Exercise::renderQuestion()   F

Complexity

Conditions 19
Paths 641

Size

Total Lines 181
Code Lines 78

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 78
nc 641
nop 9
dl 0
loc 181
rs 0.8486
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
6
use Chamilo\CoreBundle\Component\Utils\ToolIcon;
7
use Chamilo\CoreBundle\Entity\GradebookLink;
8
use Chamilo\CoreBundle\Entity\TrackEExercise;
9
use Chamilo\CoreBundle\Entity\TrackEExerciseConfirmation;
10
use Chamilo\CoreBundle\Entity\TrackEHotspot;
11
use Chamilo\CoreBundle\Framework\Container;
12
use Chamilo\CoreBundle\Repository\ResourceLinkRepository;
13
use Chamilo\CourseBundle\Entity\CQuizCategory;
14
use Chamilo\CourseBundle\Entity\CQuiz;
15
use Chamilo\CourseBundle\Entity\CQuizRelQuestionCategory;
16
use ChamiloSession as Session;
17
use Chamilo\CoreBundle\Component\Utils\ActionIcon;
18
use Chamilo\CoreBundle\Component\Utils\StateIcon;
19
20
21
/**
22
 * @author Olivier Brouckaert
23
 * @author Julio Montoya Cleaning exercises
24
 * @author Hubert Borderiou #294
25
 */
26
class Exercise
27
{
28
    public const PAGINATION_ITEMS_PER_PAGE = 20;
29
    public $iId;
30
    public $id;
31
    public $name;
32
    public $title;
33
    public $exercise;
34
    public $description;
35
    public $sound;
36
    public $type; //ALL_ON_ONE_PAGE or ONE_PER_PAGE
37
    public $random;
38
    public $random_answers;
39
    public $active;
40
    public $timeLimit;
41
    public $attempts;
42
    public $feedback_type;
43
    public $end_time;
44
    public $start_time;
45
    public $questionList; // array with the list of this exercise's questions
46
    /* including question list of the media */
47
    public $questionListUncompressed;
48
    public $results_disabled;
49
    public $expired_time;
50
    public $course;
51
    public $course_id;
52
    public $propagate_neg;
53
    public $saveCorrectAnswers;
54
    public $review_answers;
55
    public $randomByCat;
56
    public $text_when_finished;
57
    public $text_when_finished_failure;
58
    public $display_category_name;
59
    public $pass_percentage;
60
    public $edit_exercise_in_lp = false;
61
    public $is_gradebook_locked = false;
62
    public $exercise_was_added_in_lp = false;
63
    public $lpList = [];
64
    public $force_edit_exercise_in_lp = false;
65
    public $categories;
66
    public $categories_grouping = true;
67
    public $endButton = 0;
68
    public $categoryWithQuestionList;
69
    public $mediaList;
70
    public $loadQuestionAJAX = false;
71
    // Notification send to the teacher.
72
    public $emailNotificationTemplate = null;
73
    // Notification send to the student.
74
    public $emailNotificationTemplateToUser = null;
75
    public $countQuestions = 0;
76
    public $fastEdition = false;
77
    public $modelType = 1;
78
    public $questionSelectionType = EX_Q_SELECTION_ORDERED;
79
    public $hideQuestionTitle = 0;
80
    public $scoreTypeModel = 0;
81
    public $categoryMinusOne = true; // Shows the category -1: See BT#6540
82
    public $globalCategoryId = null;
83
    public $onSuccessMessage = null;
84
    public $onFailedMessage = null;
85
    public $emailAlert;
86
    public $notifyUserByEmail = '';
87
    public $sessionId = 0;
88
    public $questionFeedbackEnabled = false;
89
    public $questionTypeWithFeedback;
90
    public $showPreviousButton;
91
    public $notifications;
92
    public $export = false;
93
    public $autolaunch;
94
    public $quizCategoryId;
95
    public $pageResultConfiguration;
96
    public $hideQuestionNumber;
97
    public $preventBackwards;
98
    public $currentQuestion;
99
    public $hideComment;
100
    public $hideNoAnswer;
101
    public $hideExpectedAnswer;
102
    public $forceShowExpectedChoiceColumn;
103
    public $disableHideCorrectAnsweredQuestions;
104
105
    /**
106
     * @param int $courseId
107
     */
108
    public function __construct($courseId = 0)
109
    {
110
        $this->iId = 0;
111
        $this->id = 0;
112
        $this->exercise = '';
113
        $this->description = '';
114
        $this->sound = '';
115
        $this->type = ALL_ON_ONE_PAGE;
116
        $this->random = 0;
117
        $this->random_answers = 0;
118
        $this->active = 1;
119
        $this->questionList = [];
120
        $this->timeLimit = 0;
121
        $this->end_time = '';
122
        $this->start_time = '';
123
        $this->results_disabled = 1;
124
        $this->expired_time = 0;
125
        $this->propagate_neg = 0;
126
        $this->saveCorrectAnswers = 0;
127
        $this->review_answers = false;
128
        $this->randomByCat = 0;
129
        $this->text_when_finished = '';
130
        $this->text_when_finished_failure = '';
131
        $this->display_category_name = 0;
132
        $this->pass_percentage = 0;
133
        $this->modelType = 1;
134
        $this->questionSelectionType = EX_Q_SELECTION_ORDERED;
135
        $this->endButton = 0;
136
        $this->scoreTypeModel = 0;
137
        $this->globalCategoryId = null;
138
        $this->notifications = [];
139
        $this->quizCategoryId = 0;
140
        $this->pageResultConfiguration = null;
141
        $this->hideQuestionNumber = 0;
142
        $this->preventBackwards = 0;
143
        $this->hideComment = false;
144
        $this->hideNoAnswer = false;
145
        $this->hideExpectedAnswer = false;
146
        $this->disableHideCorrectAnsweredQuestions = false;
147
148
        if (!empty($courseId)) {
149
            $courseInfo = api_get_course_info_by_id($courseId);
150
        } else {
151
            $courseInfo = api_get_course_info();
152
        }
153
        $this->course_id = $courseInfo['real_id'];
154
        $this->course = $courseInfo;
155
        $this->sessionId = api_get_session_id();
156
157
        // ALTER TABLE c_quiz_question ADD COLUMN feedback text;
158
        $this->questionFeedbackEnabled = ('true' === api_get_setting('exercise.allow_quiz_question_feedback'));
159
        $this->showPreviousButton = true;
160
    }
161
162
    /**
163
     * Reads exercise information from the data base.
164
     *
165
     * @param int  $id                - exercise Id
166
     * @param bool $parseQuestionList
167
     *
168
     * @return bool - true if exercise exists, otherwise false
169
     */
170
    public function read($id, $parseQuestionList = true)
171
    {
172
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
173
174
        $id = (int) $id;
175
        if (empty($this->course_id) || empty($id)) {
176
            return false;
177
        }
178
179
        $sql = "SELECT * FROM $table
180
                WHERE iid = $id";
181
        $result = Database::query($sql);
182
183
        // if the exercise has been found
184
        if ($object = Database::fetch_object($result)) {
185
            $this->id = $this->iId = (int) $object->iid;
186
            $this->exercise = $object->title;
187
            $this->name = $object->title;
188
            $this->title = $object->title;
189
            $this->description = $object->description;
190
            $this->sound = $object->sound;
191
            $this->type = $object->type;
192
            if (empty($this->type)) {
193
                $this->type = ONE_PER_PAGE;
194
            }
195
            $this->random = $object->random;
196
            $this->random_answers = $object->random_answers;
197
            $this->active = $object->active;
198
            $this->results_disabled = $object->results_disabled;
199
            $this->attempts = $object->max_attempt;
200
            $this->feedback_type = $object->feedback_type;
201
            //$this->sessionId = $object->session_id;
202
            $this->propagate_neg = $object->propagate_neg;
203
            $this->saveCorrectAnswers = $object->save_correct_answers;
204
            $this->randomByCat = $object->random_by_category;
205
            $this->text_when_finished = $object->text_when_finished;
206
            $this->text_when_finished_failure = $object->text_when_finished_failure;
207
            $this->display_category_name = $object->display_category_name;
208
            $this->pass_percentage = $object->pass_percentage;
209
            $this->is_gradebook_locked = api_resource_is_locked_by_gradebook($id, LINK_EXERCISE);
210
            $this->review_answers = isset($object->review_answers) && 1 == $object->review_answers ? true : false;
211
            $this->globalCategoryId = isset($object->global_category_id) ? $object->global_category_id : null;
212
            $this->questionSelectionType = isset($object->question_selection_type) ? (int) $object->question_selection_type : null;
213
            $this->hideQuestionTitle = isset($object->hide_question_title) ? (int) $object->hide_question_title : 0;
214
            $this->autolaunch = isset($object->autolaunch) ? (int) $object->autolaunch : 0;
215
            $this->quizCategoryId = isset($object->quiz_category_id) ? (int) $object->quiz_category_id : null;
216
            $this->preventBackwards = isset($object->prevent_backwards) ? (int) $object->prevent_backwards : 0;
217
            $this->exercise_was_added_in_lp = false;
218
            $this->lpList = [];
219
            $this->notifications = [];
220
            if (!empty($object->notifications)) {
221
                $this->notifications = explode(',', $object->notifications);
222
            }
223
224
            if (!empty($object->page_result_configuration)) {
225
                //$this->pageResultConfiguration = $object->page_result_configuration;
226
            }
227
228
            $this->hideQuestionNumber = 1 == $object->hide_question_number;
229
230
            if (isset($object->show_previous_button)) {
231
                $this->showPreviousButton = 1 == $object->show_previous_button ? true : false;
232
            }
233
234
            $list = self::getLpListFromExercise($id, $this->course_id);
235
            if (!empty($list)) {
236
                $this->exercise_was_added_in_lp = true;
237
                $this->lpList = $list;
238
            }
239
240
            $this->force_edit_exercise_in_lp = api_get_configuration_value('force_edit_exercise_in_lp');
241
            $this->edit_exercise_in_lp = true;
242
            if ($this->exercise_was_added_in_lp) {
243
                $this->edit_exercise_in_lp = true == $this->force_edit_exercise_in_lp;
244
            }
245
246
            if (!empty($object->end_time)) {
247
                $this->end_time = $object->end_time;
248
            }
249
            if (!empty($object->start_time)) {
250
                $this->start_time = $object->start_time;
251
            }
252
253
            // Control time
254
            $this->expired_time = $object->expired_time;
255
256
            // Checking if question_order is correctly set
257
            if ($parseQuestionList) {
258
                $this->setQuestionList(true);
259
            }
260
261
            //overload questions list with recorded questions list
262
            //load questions only for exercises of type 'one question per page'
263
            //this is needed only is there is no questions
264
265
            // @todo not sure were in the code this is used somebody mess with the exercise tool
266
            // @todo don't know who add that config and why $_configuration['live_exercise_tracking']
267
            /*global $_configuration, $questionList;
268
            if ($this->type == ONE_PER_PAGE && $_SERVER['REQUEST_METHOD'] != 'POST'
269
                && defined('QUESTION_LIST_ALREADY_LOGGED') &&
270
                isset($_configuration['live_exercise_tracking']) && $_configuration['live_exercise_tracking']
271
            ) {
272
                $this->questionList = $questionList;
273
            }*/
274
275
            return true;
276
        }
277
278
        return false;
279
    }
280
281
    public function getCutTitle(): string
282
    {
283
        $title = $this->getUnformattedTitle();
284
285
        return cut($title, EXERCISE_MAX_NAME_SIZE);
286
    }
287
288
    public function getId()
289
    {
290
        return (int) $this->iId;
291
    }
292
293
    /**
294
     * returns the exercise title.
295
     *
296
     * @param bool $unformattedText Optional. Get the title without HTML tags
297
     *
298
     * @return string - exercise title
299
     */
300
    public function selectTitle($unformattedText = false)
301
    {
302
        if ($unformattedText) {
303
            return $this->getUnformattedTitle();
304
        }
305
306
        return $this->exercise;
307
    }
308
309
    /**
310
     * returns the number of attempts setted.
311
     *
312
     * @return int - exercise attempts
313
     */
314
    public function selectAttempts()
315
    {
316
        return $this->attempts;
317
    }
318
319
    /**
320
     * Returns the number of FeedbackType
321
     *  0: Feedback , 1: DirectFeedback, 2: NoFeedback.
322
     *
323
     * @return int - exercise attempts
324
     */
325
    public function getFeedbackType()
326
    {
327
        return (int) $this->feedback_type;
328
    }
329
330
    /**
331
     * returns the time limit.
332
     *
333
     * @return int
334
     */
335
    public function selectTimeLimit()
336
    {
337
        return $this->timeLimit;
338
    }
339
340
    /**
341
     * returns the exercise description.
342
     *
343
     * @return string - exercise description
344
     */
345
    public function selectDescription()
346
    {
347
        return $this->description;
348
    }
349
350
    /**
351
     * returns the exercise sound file.
352
     */
353
    public function getSound()
354
    {
355
        return $this->sound;
356
    }
357
358
    /**
359
     * returns the exercise type.
360
     *
361
     * @return int - exercise type
362
     *
363
     * @author Olivier Brouckaert
364
     */
365
    public function selectType()
366
    {
367
        return $this->type;
368
    }
369
370
    /**
371
     * @return int
372
     */
373
    public function getModelType()
374
    {
375
        return $this->modelType;
376
    }
377
378
    /**
379
     * @return int
380
     */
381
    public function selectEndButton()
382
    {
383
        return $this->endButton;
384
    }
385
386
    /**
387
     * @return int : do we display the question category name for students
388
     *
389
     * @author hubert borderiou 30-11-11
390
     */
391
    public function selectDisplayCategoryName()
392
    {
393
        return $this->display_category_name;
394
    }
395
396
    /**
397
     * @return int
398
     */
399
    public function selectPassPercentage()
400
    {
401
        return $this->pass_percentage;
402
    }
403
404
    /**
405
     * Modify object to update the switch display_category_name.
406
     *
407
     * @param int $value is an integer 0 or 1
408
     *
409
     * @author hubert borderiou 30-11-11
410
     */
411
    public function updateDisplayCategoryName($value)
412
    {
413
        $this->display_category_name = $value;
414
    }
415
416
    /**
417
     * @return string html text : the text to display ay the end of the test
418
     *
419
     * @author hubert borderiou 28-11-11
420
     */
421
    public function getTextWhenFinished(): string
422
    {
423
        return $this->text_when_finished;
424
    }
425
426
    /**
427
     * @param string $text
428
     *
429
     * @author hubert borderiou 28-11-11
430
     */
431
    public function setTextWhenFinished(string $text): void
432
    {
433
        $this->text_when_finished = $text;
434
    }
435
436
    /**
437
     * Get the text to display when the user has failed the test
438
     * @return string html text : the text to display ay the end of the test
439
     */
440
    public function getTextWhenFinishedFailure(): string
441
    {
442
        if (empty($this->text_when_finished_failure)) {
443
            return '';
444
        }
445
446
        return $this->text_when_finished_failure;
447
    }
448
449
    /**
450
     * Set the text to display when the user has succeeded in the test
451
     * @param string $text
452
     */
453
    public function setTextWhenFinishedFailure(string $text): void
454
    {
455
        $this->text_when_finished_failure = $text;
456
    }
457
458
    /**
459
     * return 1 or 2 if randomByCat.
460
     *
461
     * @return int - quiz random by category
462
     *
463
     * @author hubert borderiou
464
     */
465
    public function getRandomByCategory()
466
    {
467
        return $this->randomByCat;
468
    }
469
470
    /**
471
     * return 0 if no random by cat
472
     * return 1 if random by cat, categories shuffled
473
     * return 2 if random by cat, categories sorted by alphabetic order.
474
     *
475
     * @return int - quiz random by category
476
     *
477
     * @author hubert borderiou
478
     */
479
    public function isRandomByCat()
480
    {
481
        $res = EXERCISE_CATEGORY_RANDOM_DISABLED;
482
        if (EXERCISE_CATEGORY_RANDOM_SHUFFLED == $this->randomByCat) {
483
            $res = EXERCISE_CATEGORY_RANDOM_SHUFFLED;
484
        } elseif (EXERCISE_CATEGORY_RANDOM_ORDERED == $this->randomByCat) {
485
            $res = EXERCISE_CATEGORY_RANDOM_ORDERED;
486
        }
487
488
        return $res;
489
    }
490
491
    /**
492
     * return nothing
493
     * update randomByCat value for object.
494
     *
495
     * @param int $random
496
     *
497
     * @author hubert borderiou
498
     */
499
    public function updateRandomByCat($random)
500
    {
501
        $this->randomByCat = EXERCISE_CATEGORY_RANDOM_DISABLED;
502
        if (in_array(
503
            $random,
504
            [
505
                EXERCISE_CATEGORY_RANDOM_SHUFFLED,
506
                EXERCISE_CATEGORY_RANDOM_ORDERED,
507
                EXERCISE_CATEGORY_RANDOM_DISABLED,
508
            ]
509
        )) {
510
            $this->randomByCat = $random;
511
        }
512
    }
513
514
    /**
515
     * Tells if questions are selected randomly, and if so returns the draws.
516
     *
517
     * @return int - results disabled exercise
518
     *
519
     * @author Carlos Vargas
520
     */
521
    public function selectResultsDisabled()
522
    {
523
        return $this->results_disabled;
524
    }
525
526
    /**
527
     * tells if questions are selected randomly, and if so returns the draws.
528
     *
529
     * @return bool
530
     *
531
     * @author Olivier Brouckaert
532
     */
533
    public function isRandom()
534
    {
535
        $isRandom = false;
536
        // "-1" means all questions will be random
537
        if ($this->random > 0 || -1 == $this->random) {
538
            $isRandom = true;
539
        }
540
541
        return $isRandom;
542
    }
543
544
    /**
545
     * returns random answers status.
546
     *
547
     * @author Juan Carlos Rana
548
     */
549
    public function getRandomAnswers()
550
    {
551
        return $this->random_answers;
552
    }
553
554
    /**
555
     * Same as isRandom() but has a name applied to values different than 0 or 1.
556
     *
557
     * @return int
558
     */
559
    public function getShuffle()
560
    {
561
        return $this->random;
562
    }
563
564
    /**
565
     * returns the exercise status (1 = enabled ; 0 = disabled).
566
     *
567
     * @return int - 1 if enabled, otherwise 0
568
     *
569
     * @author Olivier Brouckaert
570
     */
571
    public function selectStatus()
572
    {
573
        return $this->active;
574
    }
575
576
    /**
577
     * If false the question list will be managed as always if true
578
     * the question will be filtered
579
     * depending of the exercise settings (table c_quiz_rel_category).
580
     *
581
     * @param bool $status active or inactive grouping
582
     */
583
    public function setCategoriesGrouping($status)
584
    {
585
        $this->categories_grouping = (bool) $status;
586
    }
587
588
    /**
589
     * @return int
590
     */
591
    public function getHideQuestionTitle()
592
    {
593
        return $this->hideQuestionTitle;
594
    }
595
596
    /**
597
     * @param $value
598
     */
599
    public function setHideQuestionTitle($value)
600
    {
601
        $this->hideQuestionTitle = (int) $value;
602
    }
603
604
    /**
605
     * @return int
606
     */
607
    public function getScoreTypeModel()
608
    {
609
        return $this->scoreTypeModel;
610
    }
611
612
    /**
613
     * @param int $value
614
     */
615
    public function setScoreTypeModel($value)
616
    {
617
        $this->scoreTypeModel = (int) $value;
618
    }
619
620
    /**
621
     * @return int
622
     */
623
    public function getGlobalCategoryId()
624
    {
625
        return $this->globalCategoryId;
626
    }
627
628
    /**
629
     * @param int $value
630
     */
631
    public function setGlobalCategoryId($value)
632
    {
633
        if (is_array($value) && isset($value[0])) {
634
            $value = $value[0];
635
        }
636
        $this->globalCategoryId = (int) $value;
637
    }
638
639
    /**
640
     * @param int    $start
641
     * @param int    $limit
642
     * @param string $sidx
643
     * @param string $sord
644
     * @param array  $whereCondition
645
     * @param array  $extraFields
646
     *
647
     * @return array
648
     */
649
    public function getQuestionListPagination(
650
        $start,
651
        $limit,
652
        $sidx,
653
        $sord,
654
        $whereCondition = [],
655
        $extraFields = []
656
    ) {
657
        if (!empty($this->id)) {
658
            $category_list = TestCategory::getListOfCategoriesNameForTest(
659
                $this->id,
660
                false
661
            );
662
            $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
663
            $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
664
665
            $sql = "SELECT q.iid
666
                    FROM $TBL_EXERCICE_QUESTION e
667
                    INNER JOIN $TBL_QUESTIONS  q
668
                    ON (e.question_id = q.iid)
669
					WHERE e.quiz_id	= '".$this->id."' ";
670
671
            $orderCondition = ' ORDER BY question_order ';
672
673
            if (!empty($sidx) && !empty($sord)) {
674
                if ('question' === $sidx) {
675
                    if (in_array(strtolower($sord), ['desc', 'asc'])) {
676
                        $orderCondition = " ORDER BY `q.$sidx` $sord";
677
                    }
678
                }
679
            }
680
681
            $sql .= $orderCondition;
682
            $limitCondition = null;
683
            if (isset($start) && isset($limit)) {
684
                $start = (int) $start;
685
                $limit = (int) $limit;
686
                $limitCondition = " LIMIT $start, $limit";
687
            }
688
            $sql .= $limitCondition;
689
            $result = Database::query($sql);
690
            $questions = [];
691
            if (Database::num_rows($result)) {
692
                if (!empty($extraFields)) {
693
                    $extraFieldValue = new ExtraFieldValue('question');
694
                }
695
                while ($question = Database::fetch_assoc($result)) {
696
                    /** @var Question $objQuestionTmp */
697
                    $objQuestionTmp = Question::read($question['iid']);
698
                    $category_labels = '';
699
                    // @todo not implemented in 1.11.x
700
                    /*$category_labels = TestCategory::return_category_labels(
701
                        $objQuestionTmp->category_list,
702
                        $category_list
703
                    );*/
704
705
                    if (empty($category_labels)) {
706
                        $category_labels = '-';
707
                    }
708
709
                    // Question type
710
                    $typeImg = $objQuestionTmp->getTypePicture();
711
                    $typeExpl = $objQuestionTmp->getExplanation();
712
713
                    $question_media = null;
714
                    if (!empty($objQuestionTmp->parent_id)) {
715
                        // @todo not implemented in 1.11.x
716
                        //$objQuestionMedia = Question::read($objQuestionTmp->parent_id);
717
                        //$question_media = Question::getMediaLabel($objQuestionMedia->question);
718
                    }
719
720
                    $questionType = Display::tag(
721
                        'div',
722
                        Display::return_icon($typeImg, $typeExpl, [], ICON_SIZE_MEDIUM).$question_media
723
                    );
724
725
                    $question = [
726
                        'id' => $question['iid'],
727
                        'question' => $objQuestionTmp->selectTitle(),
728
                        'type' => $questionType,
729
                        'category' => Display::tag(
730
                            'div',
731
                            '<a href="#" style="padding:0px; margin:0px;">'.$category_labels.'</a>'
732
                        ),
733
                        'score' => $objQuestionTmp->selectWeighting(),
734
                        'level' => $objQuestionTmp->level,
735
                    ];
736
737
                    if (!empty($extraFields)) {
738
                        foreach ($extraFields as $extraField) {
739
                            $value = $extraFieldValue->get_values_by_handler_and_field_id(
740
                                $question['id'],
741
                                $extraField['id']
742
                            );
743
                            $stringValue = null;
744
                            if ($value) {
745
                                $stringValue = $value['field_value'];
746
                            }
747
                            $question[$extraField['field_variable']] = $stringValue;
748
                        }
749
                    }
750
                    $questions[] = $question;
751
                }
752
            }
753
754
            return $questions;
755
        }
756
    }
757
758
    /**
759
     * Get question count per exercise from DB (any special treatment).
760
     *
761
     * @return int
762
     */
763
    public function getQuestionCount()
764
    {
765
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
766
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
767
        $sql = "SELECT count(q.iid) as count
768
                FROM $TBL_EXERCICE_QUESTION e
769
                INNER JOIN $TBL_QUESTIONS q
770
                ON (e.question_id = q.iid)
771
                WHERE
772
                    e.quiz_id = ".$this->getId();
773
        $result = Database::query($sql);
774
775
        $count = 0;
776
        if (Database::num_rows($result)) {
777
            $row = Database::fetch_array($result);
778
            $count = (int) $row['count'];
779
        }
780
781
        return $count;
782
    }
783
784
    /**
785
     * @return array
786
     */
787
    public function getQuestionOrderedListByName()
788
    {
789
        if (empty($this->course_id) || empty($this->getId())) {
790
            return [];
791
        }
792
793
        $exerciseQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
794
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
795
796
        // Getting question list from the order (question list drag n drop interface ).
797
        $sql = "SELECT e.question_id
798
                FROM $exerciseQuestionTable e
799
                INNER JOIN $questionTable q
800
                ON (e.question_id= q.iid)
801
                WHERE
802
                    e.quiz_id = '".$this->getId()."'
803
                ORDER BY q.question";
804
        $result = Database::query($sql);
805
        $list = [];
806
        if (Database::num_rows($result)) {
807
            $list = Database::store_result($result, 'ASSOC');
808
        }
809
810
        return $list;
811
    }
812
813
    /**
814
     * Selecting question list depending in the exercise-category
815
     * relationship (category table in exercise settings).
816
     *
817
     * @param array $questionList
818
     * @param int   $questionSelectionType
819
     *
820
     * @return array
821
     */
822
    public function getQuestionListWithCategoryListFilteredByCategorySettings(
823
        $questionList,
824
        $questionSelectionType
825
    ) {
826
        $result = [
827
            'question_list' => [],
828
            'category_with_questions_list' => [],
829
        ];
830
831
        // Order/random categories
832
        $cat = new TestCategory();
833
834
        // Setting category order.
835
        switch ($questionSelectionType) {
836
            case EX_Q_SELECTION_ORDERED: // 1
837
            case EX_Q_SELECTION_RANDOM:  // 2
838
                // This options are not allowed here.
839
                break;
840
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED: // 3
841
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
842
                    $this,
843
                    'title ASC',
844
                    false,
845
                    true
846
                );
847
848
                $questionsByCategory = TestCategory::getQuestionsByCat(
849
                    $this->getId(),
850
                    $questionList,
851
                    $categoriesAddedInExercise
852
                );
853
854
                $questionList = $this->pickQuestionsPerCategory(
855
                    $categoriesAddedInExercise,
856
                    $questionList,
857
                    $questionsByCategory,
858
                    true,
859
                    false
860
                );
861
862
                break;
863
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED: // 4
864
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7
865
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
866
                    $this,
867
                    null,
868
                    true,
869
                    true
870
                );
871
                $questionsByCategory = TestCategory::getQuestionsByCat(
872
                    $this->getId(),
873
                    $questionList,
874
                    $categoriesAddedInExercise
875
                );
876
                $questionList = $this->pickQuestionsPerCategory(
877
                    $categoriesAddedInExercise,
878
                    $questionList,
879
                    $questionsByCategory,
880
                    true,
881
                    false
882
                );
883
884
                break;
885
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM: // 5
886
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
887
                    $this,
888
                    'title ASC',
889
                    false,
890
                    true
891
                );
892
                $questionsByCategory = TestCategory::getQuestionsByCat(
893
                    $this->getId(),
894
                    $questionList,
895
                    $categoriesAddedInExercise
896
                );
897
                $questionsByCategoryMandatory = [];
898
                if (EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM == $this->getQuestionSelectionType() &&
899
                    ('true' === api_get_setting('exercise.allow_mandatory_question_in_category'))
900
                ) {
901
                    $questionsByCategoryMandatory = TestCategory::getQuestionsByCat(
902
                        $this->id,
903
                        $questionList,
904
                        $categoriesAddedInExercise,
905
                        true
906
                    );
907
                }
908
                $questionList = $this->pickQuestionsPerCategory(
909
                    $categoriesAddedInExercise,
910
                    $questionList,
911
                    $questionsByCategory,
912
                    true,
913
                    true,
914
                    $questionsByCategoryMandatory
915
                );
916
917
                break;
918
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM: // 6
919
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED:
920
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
921
                    $this,
922
                    null,
923
                    true,
924
                    true
925
                );
926
927
                $questionsByCategory = TestCategory::getQuestionsByCat(
928
                    $this->getId(),
929
                    $questionList,
930
                    $categoriesAddedInExercise
931
                );
932
933
                $questionList = $this->pickQuestionsPerCategory(
934
                    $categoriesAddedInExercise,
935
                    $questionList,
936
                    $questionsByCategory,
937
                    true,
938
                    true
939
                );
940
941
                break;
942
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED: // 9
943
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
944
                    $this,
945
                    'root ASC, lft ASC',
946
                    false,
947
                    true
948
                );
949
                $questionsByCategory = TestCategory::getQuestionsByCat(
950
                    $this->getId(),
951
                    $questionList,
952
                    $categoriesAddedInExercise
953
                );
954
                $questionList = $this->pickQuestionsPerCategory(
955
                    $categoriesAddedInExercise,
956
                    $questionList,
957
                    $questionsByCategory,
958
                    true,
959
                    false
960
                );
961
962
                break;
963
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM: // 10
964
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
965
                    $this,
966
                    'root, lft ASC',
967
                    false,
968
                    true
969
                );
970
                $questionsByCategory = TestCategory::getQuestionsByCat(
971
                    $this->getId(),
972
                    $questionList,
973
                    $categoriesAddedInExercise
974
                );
975
                $questionList = $this->pickQuestionsPerCategory(
976
                    $categoriesAddedInExercise,
977
                    $questionList,
978
                    $questionsByCategory,
979
                    true,
980
                    true
981
                );
982
983
                break;
984
        }
985
986
        $result['question_list'] = $questionList ?? [];
987
        $result['category_with_questions_list'] = $questionsByCategory ?? [];
988
        $parentsLoaded = [];
989
        // Adding category info in the category list with question list:
990
        if (!empty($questionsByCategory)) {
991
            $newCategoryList = [];
992
            $em = Database::getManager();
993
            $repo = $em->getRepository(CQuizRelQuestionCategory::class);
994
995
            foreach ($questionsByCategory as $categoryId => $questionList) {
996
                $category = new TestCategory();
997
                $cat = (array) $category->getCategory($categoryId);
998
                if ($cat) {
999
                    $cat['iid'] = $cat['id'];
1000
                }
1001
1002
                $categoryParentInfo = null;
1003
                // Parent is not set no loop here
1004
                if (isset($cat['parent_id']) && !empty($cat['parent_id'])) {
1005
                    /** @var CQuizRelQuestionCategory $categoryEntity */
1006
                    if (!isset($parentsLoaded[$cat['parent_id']])) {
1007
                        $categoryEntity = $em->find(CQuizRelQuestionCategory::class, $cat['parent_id']);
1008
                        $parentsLoaded[$cat['parent_id']] = $categoryEntity;
1009
                    } else {
1010
                        $categoryEntity = $parentsLoaded[$cat['parent_id']];
1011
                    }
1012
                    $path = $repo->getPath($categoryEntity);
1013
1014
                    $index = 0;
1015
                    if ($this->categoryMinusOne) {
1016
                        //$index = 1;
1017
                    }
1018
1019
                    /** @var CQuizRelQuestionCategory $categoryParent */
1020
                    // @todo not implemented in 1.11.x
1021
                    /*foreach ($path as $categoryParent) {
1022
                        $visibility = $categoryParent->getVisibility();
1023
                        if (0 == $visibility) {
1024
                            $categoryParentId = $categoryId;
1025
                            $categoryTitle = $cat['title'];
1026
                            if (count($path) > 1) {
1027
                                continue;
1028
                            }
1029
                        } else {
1030
                            $categoryParentId = $categoryParent->getIid();
1031
                            $categoryTitle = $categoryParent->getTitle();
1032
                        }
1033
1034
                        $categoryParentInfo['id'] = $categoryParentId;
1035
                        $categoryParentInfo['iid'] = $categoryParentId;
1036
                        $categoryParentInfo['parent_path'] = null;
1037
                        $categoryParentInfo['title'] = $categoryTitle;
1038
                        $categoryParentInfo['name'] = $categoryTitle;
1039
                        $categoryParentInfo['parent_id'] = null;
1040
1041
                        break;
1042
                    }*/
1043
                }
1044
                $cat['parent_info'] = $categoryParentInfo;
1045
                $newCategoryList[$categoryId] = [
1046
                    'category' => $cat,
1047
                    'question_list' => $questionList,
1048
                ];
1049
            }
1050
1051
            $result['category_with_questions_list'] = $newCategoryList;
1052
        }
1053
1054
        return $result;
1055
    }
1056
1057
    /**
1058
     * returns the array with the question ID list.
1059
     *
1060
     * @param bool $fromDatabase Whether the results should be fetched in the database or just from memory
1061
     * @param bool $adminView    Whether we should return all questions (admin view) or
1062
     *                           just a list limited by the max number of random questions
1063
     *
1064
     * @return array - question ID list
1065
     */
1066
    public function selectQuestionList($fromDatabase = false, $adminView = false)
1067
    {
1068
        //var_dump($this->getId());exit;
1069
        if ($fromDatabase && !empty($this->getId())) {
1070
            $nbQuestions = $this->getQuestionCount();
1071
1072
            $questionSelectionType = $this->getQuestionSelectionType();
1073
1074
            switch ($questionSelectionType) {
1075
                case EX_Q_SELECTION_ORDERED:
1076
                    $questionList = $this->getQuestionOrderedList($adminView);
1077
1078
                    break;
1079
                case EX_Q_SELECTION_RANDOM:
1080
                    // Not a random exercise, or if there are not at least 2 questions
1081
                    if (0 == $this->random || $nbQuestions < 2) {
1082
                        $questionList = $this->getQuestionOrderedList($adminView);
1083
                    } else {
1084
                        $questionList = $this->getRandomList($adminView);
1085
                    }
1086
1087
                    break;
1088
                default:
1089
                    $questionList = $this->getQuestionOrderedList($adminView);
1090
                    $result = $this->getQuestionListWithCategoryListFilteredByCategorySettings(
1091
                        $questionList,
1092
                        $questionSelectionType
1093
                    );
1094
                    $this->categoryWithQuestionList = $result['category_with_questions_list'];
1095
                    $questionList = $result['question_list'];
1096
1097
                    break;
1098
            }
1099
1100
            return $questionList;
1101
        }
1102
1103
        return $this->questionList;
1104
    }
1105
1106
    /**
1107
     * returns the number of questions in this exercise.
1108
     *
1109
     * @return int - number of questions
1110
     */
1111
    public function selectNbrQuestions()
1112
    {
1113
        return count($this->questionList);
1114
    }
1115
1116
    /**
1117
     * @return int
1118
     */
1119
    public function selectPropagateNeg()
1120
    {
1121
        return $this->propagate_neg;
1122
    }
1123
1124
    /**
1125
     * @return int
1126
     */
1127
    public function getSaveCorrectAnswers()
1128
    {
1129
        return $this->saveCorrectAnswers;
1130
    }
1131
1132
    /**
1133
     * Selects questions randomly in the question list.
1134
     *
1135
     * @param bool $adminView Whether we should return all
1136
     *                        questions (admin view) or just a list limited by the max number of random questions
1137
     *
1138
     * @return array - if the exercise is not set to take questions randomly, returns the question list
1139
     *               without randomizing, otherwise, returns the list with questions selected randomly
1140
     *
1141
     * @author Olivier Brouckaert
1142
     * @author Hubert Borderiou 15 nov 2011
1143
     */
1144
    public function getRandomList($adminView = false)
1145
    {
1146
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1147
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
1148
        $random = isset($this->random) && !empty($this->random) ? $this->random : 0;
1149
1150
        // Random with limit
1151
        $randomLimit = " ORDER BY RAND() LIMIT $random";
1152
1153
        // Random with no limit
1154
        if (-1 == $random) {
1155
            $randomLimit = ' ORDER BY RAND() ';
1156
        }
1157
1158
        // Admin see the list in default order
1159
        if (true === $adminView) {
1160
            // If viewing it as admin for edition, don't show it randomly, use title + id
1161
            $randomLimit = 'ORDER BY e.question_order';
1162
        }
1163
1164
        $sql = "SELECT e.question_id
1165
                FROM $quizRelQuestion e
1166
                INNER JOIN $question q
1167
                ON (e.question_id= q.iid)
1168
                WHERE
1169
                    e.quiz_id = '".$this->getId()."'
1170
                    $randomLimit ";
1171
        $result = Database::query($sql);
1172
        $questionList = [];
1173
        while ($row = Database::fetch_object($result)) {
1174
            $questionList[] = $row->question_id;
1175
        }
1176
1177
        return $questionList;
1178
    }
1179
1180
    /**
1181
     * returns 'true' if the question ID is in the question list.
1182
     *
1183
     * @param int $questionId - question ID
1184
     *
1185
     * @return bool - true if in the list, otherwise false
1186
     *
1187
     * @author Olivier Brouckaert
1188
     */
1189
    public function isInList($questionId)
1190
    {
1191
        $inList = false;
1192
        if (is_array($this->questionList)) {
1193
            $inList = in_array($questionId, $this->questionList);
1194
        }
1195
1196
        return $inList;
1197
    }
1198
1199
    /**
1200
     * If current exercise has a question.
1201
     *
1202
     * @param int $questionId
1203
     *
1204
     * @return int
1205
     */
1206
    public function hasQuestion($questionId)
1207
    {
1208
        $questionId = (int) $questionId;
1209
1210
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1211
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1212
        $sql = "SELECT q.iid
1213
                FROM $TBL_EXERCICE_QUESTION e
1214
                INNER JOIN $TBL_QUESTIONS q
1215
                ON (e.question_id = q.iid)
1216
                WHERE
1217
                    q.iid = $questionId AND
1218
                    e.quiz_id = ".$this->getId();
1219
1220
        $result = Database::query($sql);
1221
1222
        return Database::num_rows($result) > 0;
1223
    }
1224
1225
    public function hasQuestionWithType($type)
1226
    {
1227
        $type = (int) $type;
1228
1229
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1230
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
1231
        $sql = "SELECT q.iid
1232
                FROM $table e
1233
                INNER JOIN $tableQuestion q
1234
                ON (e.question_id = q.iid)
1235
                WHERE
1236
                    q.type = $type AND
1237
                    e.quiz_id = ".$this->getId();
1238
1239
        $result = Database::query($sql);
1240
1241
        return Database::num_rows($result) > 0;
1242
    }
1243
1244
    public function hasQuestionWithTypeNotInList(array $questionTypeList)
1245
    {
1246
        if (empty($questionTypeList)) {
1247
            return false;
1248
        }
1249
1250
        $questionTypeToString = implode("','", array_map('intval', $questionTypeList));
1251
1252
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1253
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
1254
        $sql = "SELECT q.iid
1255
                FROM $table e
1256
                INNER JOIN $tableQuestion q
1257
                ON (e.question_id = q.iid)
1258
                WHERE
1259
                    q.type NOT IN ('$questionTypeToString')  AND
1260
1261
                    e.quiz_id = ".$this->getId();
1262
1263
        $result = Database::query($sql);
1264
1265
        return Database::num_rows($result) > 0;
1266
    }
1267
1268
    /**
1269
     * changes the exercise title.
1270
     *
1271
     * @param string $title - exercise title
1272
     *
1273
     * @author Olivier Brouckaert
1274
     */
1275
    public function updateTitle($title)
1276
    {
1277
        $this->title = $this->exercise = $title;
1278
    }
1279
1280
    /**
1281
     * changes the exercise max attempts.
1282
     *
1283
     * @param int $attempts - exercise max attempts
1284
     */
1285
    public function updateAttempts($attempts)
1286
    {
1287
        $this->attempts = $attempts;
1288
    }
1289
1290
    /**
1291
     * changes the exercise feedback type.
1292
     *
1293
     * @param int $feedback_type
1294
     */
1295
    public function updateFeedbackType($feedback_type)
1296
    {
1297
        $this->feedback_type = $feedback_type;
1298
    }
1299
1300
    /**
1301
     * changes the exercise description.
1302
     *
1303
     * @param string $description - exercise description
1304
     *
1305
     * @author Olivier Brouckaert
1306
     */
1307
    public function updateDescription($description)
1308
    {
1309
        $this->description = $description;
1310
    }
1311
1312
    /**
1313
     * changes the exercise expired_time.
1314
     *
1315
     * @param int $expired_time The expired time of the quiz
1316
     *
1317
     * @author Isaac flores
1318
     */
1319
    public function updateExpiredTime($expired_time)
1320
    {
1321
        $this->expired_time = $expired_time;
1322
    }
1323
1324
    /**
1325
     * @param $value
1326
     */
1327
    public function updatePropagateNegative($value)
1328
    {
1329
        $this->propagate_neg = $value;
1330
    }
1331
1332
    /**
1333
     * @param int $value
1334
     */
1335
    public function updateSaveCorrectAnswers($value)
1336
    {
1337
        $this->saveCorrectAnswers = (int) $value;
1338
    }
1339
1340
    /**
1341
     * @param $value
1342
     */
1343
    public function updateReviewAnswers($value)
1344
    {
1345
        $this->review_answers = isset($value) && $value ? true : false;
1346
    }
1347
1348
    /**
1349
     * @param $value
1350
     */
1351
    public function updatePassPercentage($value)
1352
    {
1353
        $this->pass_percentage = $value;
1354
    }
1355
1356
    /**
1357
     * @param string $text
1358
     */
1359
    public function updateEmailNotificationTemplate($text)
1360
    {
1361
        $this->emailNotificationTemplate = $text;
1362
    }
1363
1364
    /**
1365
     * @param string $text
1366
     */
1367
    public function setEmailNotificationTemplateToUser($text)
1368
    {
1369
        $this->emailNotificationTemplateToUser = $text;
1370
    }
1371
1372
    /**
1373
     * @param string $value
1374
     */
1375
    public function setNotifyUserByEmail($value)
1376
    {
1377
        $this->notifyUserByEmail = $value;
1378
    }
1379
1380
    /**
1381
     * @param int $value
1382
     */
1383
    public function updateEndButton($value)
1384
    {
1385
        $this->endButton = (int) $value;
1386
    }
1387
1388
    /**
1389
     * @param string $value
1390
     */
1391
    public function setOnSuccessMessage($value)
1392
    {
1393
        $this->onSuccessMessage = $value;
1394
    }
1395
1396
    /**
1397
     * @param string $value
1398
     */
1399
    public function setOnFailedMessage($value)
1400
    {
1401
        $this->onFailedMessage = $value;
1402
    }
1403
1404
    /**
1405
     * @param $value
1406
     */
1407
    public function setModelType($value)
1408
    {
1409
        $this->modelType = (int) $value;
1410
    }
1411
1412
    /**
1413
     * @param int $value
1414
     */
1415
    public function setQuestionSelectionType($value)
1416
    {
1417
        $this->questionSelectionType = (int) $value;
1418
    }
1419
1420
    /**
1421
     * @return int
1422
     */
1423
    public function getQuestionSelectionType()
1424
    {
1425
        return (int) $this->questionSelectionType;
1426
    }
1427
1428
    /**
1429
     * @param array $categories
1430
     */
1431
    public function updateCategories($categories)
1432
    {
1433
        if (!empty($categories)) {
1434
            $categories = array_map('intval', $categories);
1435
            $this->categories = $categories;
1436
        }
1437
    }
1438
1439
    /**
1440
     * changes the exercise sound file.
1441
     *
1442
     * @param string $sound  - exercise sound file
1443
     * @param string $delete - ask to delete the file
1444
     *
1445
     * @author Olivier Brouckaert
1446
     */
1447
    public function updateSound($sound, $delete)
1448
    {
1449
        global $audioPath, $documentPath;
1450
        $TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
1451
1452
        if ($sound['size'] &&
1453
            (strstr($sound['type'], 'audio') || strstr($sound['type'], 'video'))
1454
        ) {
1455
            $this->sound = $sound['name'];
1456
1457
            if (@move_uploaded_file($sound['tmp_name'], $audioPath.'/'.$this->sound)) {
1458
                $sql = "SELECT 1 FROM $TBL_DOCUMENT
1459
                        WHERE
1460
                            c_id = ".$this->course_id." AND
1461
                            path = '".str_replace($documentPath, '', $audioPath).'/'.$this->sound."'";
1462
                $result = Database::query($sql);
1463
1464
                if (!Database::num_rows($result)) {
1465
                    DocumentManager::addDocument(
1466
                        $this->course,
1467
                        str_replace($documentPath, '', $audioPath).'/'.$this->sound,
1468
                        'file',
1469
                        $sound['size'],
1470
                        $sound['name']
1471
                    );
1472
                }
1473
            }
1474
        } elseif ($delete && is_file($audioPath.'/'.$this->sound)) {
1475
            $this->sound = '';
1476
        }
1477
    }
1478
1479
    /**
1480
     * changes the exercise type.
1481
     *
1482
     * @param int $type - exercise type
1483
     *
1484
     * @author Olivier Brouckaert
1485
     */
1486
    public function updateType($type)
1487
    {
1488
        $this->type = $type;
1489
    }
1490
1491
    /**
1492
     * sets to 0 if questions are not selected randomly
1493
     * if questions are selected randomly, sets the draws.
1494
     *
1495
     * @param int $random - 0 if not random, otherwise the draws
1496
     *
1497
     * @author Olivier Brouckaert
1498
     */
1499
    public function setRandom($random)
1500
    {
1501
        $this->random = $random;
1502
    }
1503
1504
    /**
1505
     * sets to 0 if answers are not selected randomly
1506
     * if answers are selected randomly.
1507
     *
1508
     * @param int $random_answers - random answers
1509
     *
1510
     * @author Juan Carlos Rana
1511
     */
1512
    public function updateRandomAnswers($random_answers)
1513
    {
1514
        $this->random_answers = $random_answers;
1515
    }
1516
1517
    /**
1518
     * enables the exercise.
1519
     *
1520
     * @author Olivier Brouckaert
1521
     */
1522
    public function enable()
1523
    {
1524
        $this->active = 1;
1525
    }
1526
1527
    /**
1528
     * disables the exercise.
1529
     *
1530
     * @author Olivier Brouckaert
1531
     */
1532
    public function disable()
1533
    {
1534
        $this->active = 0;
1535
    }
1536
1537
    /**
1538
     * Set disable results.
1539
     */
1540
    public function disable_results()
1541
    {
1542
        $this->results_disabled = true;
1543
    }
1544
1545
    /**
1546
     * Enable results.
1547
     */
1548
    public function enable_results()
1549
    {
1550
        $this->results_disabled = false;
1551
    }
1552
1553
    /**
1554
     * @param int $results_disabled
1555
     */
1556
    public function updateResultsDisabled($results_disabled)
1557
    {
1558
        $this->results_disabled = (int) $results_disabled;
1559
    }
1560
1561
    /**
1562
     * updates the exercise in the data base.
1563
     *
1564
     * @author Olivier Brouckaert
1565
     */
1566
    public function save()
1567
    {
1568
        $id = $this->getId();
1569
        $title = $this->exercise;
1570
        $description = $this->description;
1571
        $sound = $this->sound;
1572
        $type = $this->type;
1573
        $attempts = isset($this->attempts) ? (int) $this->attempts : 0;
1574
        $feedback_type = isset($this->feedback_type) ? (int) $this->feedback_type : 0;
1575
        $random = $this->random;
1576
        $random_answers = $this->random_answers;
1577
        $active = $this->active;
1578
        $propagate_neg = (int) $this->propagate_neg;
1579
        $saveCorrectAnswers = isset($this->saveCorrectAnswers) ? (int) $this->saveCorrectAnswers : 0;
1580
        $review_answers = isset($this->review_answers) && $this->review_answers ? 1 : 0;
1581
        $randomByCat = (int) $this->randomByCat;
1582
        $text_when_finished = $this->text_when_finished;
1583
        $text_when_finished_failure = $this->text_when_finished_failure;
1584
        $display_category_name = (int) $this->display_category_name;
1585
        $pass_percentage = (int) $this->pass_percentage;
1586
1587
        // If direct we do not show results
1588
        $results_disabled = (int) $this->results_disabled;
1589
        if (in_array($feedback_type, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1590
            $results_disabled = 0;
1591
        }
1592
        $expired_time = (int) $this->expired_time;
1593
1594
        $repo = Container::getQuizRepository();
1595
        $repoCategory = Container::getQuizCategoryRepository();
1596
1597
        // we prepare date in the database using the api_get_utc_datetime() function
1598
        $start_time = null;
1599
        if (!empty($this->start_time)) {
1600
            $start_time = $this->start_time;
1601
        }
1602
1603
        $end_time = null;
1604
        if (!empty($this->end_time)) {
1605
            $end_time = $this->end_time;
1606
        }
1607
1608
        // Exercise already exists
1609
        if ($id) {
1610
            /** @var CQuiz $exercise */
1611
            $exercise = $repo->find($id);
1612
        } else {
1613
            $exercise = new CQuiz();
1614
        }
1615
1616
        $exercise
1617
            ->setStartTime($start_time)
1618
            ->setEndTime($end_time)
1619
            ->setTitle($title)
1620
            ->setDescription($description)
1621
            ->setSound($sound)
1622
            ->setType($type)
1623
            ->setRandom((int) $random)
1624
            ->setRandomAnswers((bool) $random_answers)
1625
            ->setActive((int) $active)
1626
            ->setResultsDisabled($results_disabled)
1627
            ->setMaxAttempt($attempts)
1628
            ->setFeedbackType($feedback_type)
1629
            ->setExpiredTime($expired_time)
1630
            ->setReviewAnswers($review_answers)
1631
            ->setRandomByCategory($randomByCat)
1632
            ->setTextWhenFinished($text_when_finished)
1633
            ->setTextWhenFinishedFailure($text_when_finished_failure)
1634
            ->setDisplayCategoryName($display_category_name)
1635
            ->setPassPercentage($pass_percentage)
1636
            ->setSaveCorrectAnswers($saveCorrectAnswers)
1637
            ->setPropagateNeg($propagate_neg)
1638
            ->setHideQuestionTitle(1 === (int) $this->getHideQuestionTitle())
1639
            ->setQuestionSelectionType($this->getQuestionSelectionType())
1640
            ->setHideQuestionNumber((int) $this->hideQuestionNumber)
1641
        ;
1642
1643
        $allow = ('true' === api_get_setting('exercise.allow_exercise_categories'));
1644
        if (true === $allow && !empty($this->getQuizCategoryId())) {
1645
            $exercise->setQuizCategory($repoCategory->find($this->getQuizCategoryId()));
1646
        }
1647
1648
        $exercise->setPreventBackwards($this->getPreventBackwards());
1649
1650
        $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
1651
        if (true === $allow) {
1652
            $exercise->setShowPreviousButton($this->showPreviousButton());
1653
        }
1654
1655
        $allow = ('true' === api_get_setting('exercise.allow_notification_setting_per_exercise'));
1656
        if (true === $allow) {
1657
            $notifications = $this->getNotifications();
1658
            if (!empty($notifications)) {
1659
                $notifications = implode(',', $notifications);
1660
                $exercise->setNotifications($notifications);
1661
            }
1662
        }
1663
1664
        if (!empty($this->pageResultConfiguration)) {
1665
            $exercise->setPageResultConfiguration($this->pageResultConfiguration);
1666
        }
1667
1668
        $em = Database::getManager();
1669
1670
        if ($id) {
1671
            $repo->updateNodeForResource($exercise);
1672
1673
            if ('true' === api_get_setting('search_enabled')) {
1674
                $this->search_engine_edit();
1675
            }
1676
            $em->persist($exercise);
1677
            $em->flush();
1678
        } else {
1679
            // Creates a new exercise
1680
            $courseEntity = api_get_course_entity($this->course_id);
1681
            $exercise
1682
                ->setParent($courseEntity)
1683
                ->addCourseLink($courseEntity, api_get_session_entity());
1684
            $em->persist($exercise);
1685
            $em->flush();
1686
            $id = $exercise->getIid();
1687
            $this->iId = $this->id = $id;
1688
            if ($id) {
1689
                if ('true' === api_get_setting('search_enabled') && extension_loaded('xapian')) {
1690
                    $this->search_engine_save();
1691
                }
1692
            }
1693
        }
1694
1695
        $this->saveCategoriesInExercise($this->categories);
1696
1697
        return $id;
1698
    }
1699
1700
    /**
1701
     * Updates question position.
1702
     *
1703
     * @return bool
1704
     */
1705
    public function update_question_positions()
1706
    {
1707
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1708
        // Fixes #3483 when updating order
1709
        $questionList = $this->selectQuestionList(true);
1710
1711
        if (empty($this->getId())) {
1712
            return false;
1713
        }
1714
1715
        if (!empty($questionList)) {
1716
            foreach ($questionList as $position => $questionId) {
1717
                $position = (int) $position;
1718
                $questionId = (int) $questionId;
1719
                $sql = "UPDATE $table SET
1720
                            question_order = $position
1721
                        WHERE
1722
                            question_id = $questionId AND
1723
                            quiz_id= ".$this->getId();
1724
                Database::query($sql);
1725
            }
1726
        }
1727
1728
        return true;
1729
    }
1730
1731
    /**
1732
     * Adds a question into the question list.
1733
     *
1734
     * @param int $questionId - question ID
1735
     *
1736
     * @return bool - true if the question has been added, otherwise false
1737
     *
1738
     * @author Olivier Brouckaert
1739
     */
1740
    public function addToList($questionId)
1741
    {
1742
        // checks if the question ID is not in the list
1743
        if (!$this->isInList($questionId)) {
1744
            // selects the max position
1745
            if (!$this->selectNbrQuestions()) {
1746
                $pos = 1;
1747
            } else {
1748
                if (is_array($this->questionList)) {
1749
                    $pos = max(array_keys($this->questionList)) + 1;
1750
                }
1751
            }
1752
            $this->questionList[$pos] = $questionId;
1753
1754
            return true;
1755
        }
1756
1757
        return false;
1758
    }
1759
1760
    /**
1761
     * removes a question from the question list.
1762
     *
1763
     * @param int $questionId - question ID
1764
     *
1765
     * @return bool - true if the question has been removed, otherwise false
1766
     *
1767
     * @author Olivier Brouckaert
1768
     */
1769
    public function removeFromList($questionId)
1770
    {
1771
        // searches the position of the question ID in the list
1772
        $pos = array_search($questionId, $this->questionList);
1773
        // question not found
1774
        if (false === $pos) {
1775
            return false;
1776
        } else {
1777
            // dont reduce the number of random question if we use random by category option, or if
1778
            // random all questions
1779
            if ($this->isRandom() && 0 == $this->isRandomByCat()) {
1780
                if (count($this->questionList) >= $this->random && $this->random > 0) {
1781
                    $this->random--;
1782
                    $this->save();
1783
                }
1784
            }
1785
            // deletes the position from the array containing the wanted question ID
1786
            unset($this->questionList[$pos]);
1787
1788
            return true;
1789
        }
1790
    }
1791
1792
    /**
1793
     * deletes the exercise from the database
1794
     * Notice : leaves the question in the data base.
1795
     *
1796
     * @author Olivier Brouckaert
1797
     */
1798
    public function delete()
1799
    {
1800
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
1801
1802
        if ($limitTeacherAccess && !api_is_platform_admin()) {
1803
            return false;
1804
        }
1805
1806
        $exerciseId = $this->iId;
1807
1808
        $repo = Container::getQuizRepository();
1809
        /** @var CQuiz $exercise */
1810
        $exercise = $repo->find($exerciseId);
1811
        $linksRepo = Container::$container->get(ResourceLinkRepository::class);
1812
1813
        if (null === $exercise) {
1814
            return false;
1815
        }
1816
1817
        $locked = api_resource_is_locked_by_gradebook(
1818
            $exerciseId,
1819
            LINK_EXERCISE
1820
        );
1821
1822
        if ($locked) {
1823
            return false;
1824
        }
1825
1826
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
1827
        $sql = "UPDATE $table SET active='-1'
1828
                WHERE iid = $exerciseId";
1829
        Database::query($sql);
1830
1831
        $course = api_get_course_entity();
1832
        $session = api_get_session_entity();
1833
1834
        $linksRepo->removeByResourceInContext($exercise, $course, $session);
1835
1836
        SkillModel::deleteSkillsFromItem($exerciseId, ITEM_TYPE_EXERCISE);
1837
1838
        if ('true' === api_get_setting('search_enabled') &&
1839
            extension_loaded('xapian')
1840
        ) {
1841
            $this->search_engine_delete();
1842
        }
1843
1844
        $linkInfo = GradebookUtils::isResourceInCourseGradebook(
1845
            $this->course_id,
1846
            LINK_EXERCISE,
1847
            $exerciseId,
1848
            $this->sessionId
1849
        );
1850
        if (false !== $linkInfo) {
1851
            GradebookUtils::remove_resource_from_course_gradebook($linkInfo['id']);
1852
        }
1853
1854
        return true;
1855
    }
1856
1857
    /**
1858
     * Creates the form to create / edit an exercise.
1859
     *
1860
     * @param FormValidator $form
1861
     * @param string|array        $type
1862
     */
1863
    public function createForm($form, $type = 'full')
1864
    {
1865
        if (empty($type)) {
1866
            $type = 'full';
1867
        }
1868
1869
        // Form title
1870
        $form_title = get_lang('Create a new test');
1871
        if (!empty($_GET['id'])) {
1872
            $form_title = get_lang('Edit test name and settings');
1873
        }
1874
1875
        $form->addHeader($form_title);
1876
1877
        // Title.
1878
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
1879
            $form->addHtmlEditor(
1880
                'exerciseTitle',
1881
                get_lang('Test name'),
1882
                false,
1883
                false,
1884
                ['ToolbarSet' => 'TitleAsHtml']
1885
            );
1886
        } else {
1887
            $form->addElement(
1888
                'text',
1889
                'exerciseTitle',
1890
                get_lang('Test name'),
1891
                ['id' => 'exercise_title']
1892
            );
1893
        }
1894
1895
        $form->addElement('advanced_settings', 'advanced_params', get_lang('Advanced settings'));
1896
        $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
1897
1898
        if ('true' === api_get_setting('exercise.allow_exercise_categories')) {
1899
            $categoryManager = new ExerciseCategoryManager();
1900
            $categories = $categoryManager->getCategories(api_get_course_int_id());
1901
            $options = [];
1902
            if (!empty($categories)) {
1903
                /** @var CQuizCategory $category */
1904
                foreach ($categories as $category) {
1905
                    $options[$category->getId()] = $category->getTitle();
1906
                }
1907
            }
1908
1909
            $form->addSelect(
1910
                'quiz_category_id',
1911
                get_lang('Category'),
1912
                $options,
1913
                ['placeholder' => get_lang('Please select an option')]
1914
            );
1915
        }
1916
1917
        $editor_config = [
1918
            'ToolbarSet' => 'TestQuestionDescription',
1919
            'Width' => '100%',
1920
            'Height' => '150',
1921
        ];
1922
1923
        if (is_array($type)) {
1924
            $editor_config = array_merge($editor_config, $type);
1925
        }
1926
1927
        $form->addHtmlEditor(
1928
            'exerciseDescription',
1929
            get_lang('Give a context to the test'),
1930
            false,
1931
            false,
1932
            $editor_config
1933
        );
1934
1935
        $skillList = [];
1936
        if ('full' === $type) {
1937
            // Can't modify a DirectFeedback question.
1938
            if (!in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1939
                $this->setResultFeedbackGroup($form);
1940
1941
                // Type of results display on the final page
1942
                $this->setResultDisabledGroup($form);
1943
1944
                // Type of questions disposition on page
1945
                $radios = [];
1946
                $radios[] = $form->createElement(
1947
                    'radio',
1948
                    'exerciseType',
1949
                    null,
1950
                    get_lang('All questions on one page'),
1951
                    '1',
1952
                    [
1953
                        'onclick' => 'check_per_page_all()',
1954
                        'id' => 'option_page_all',
1955
                    ]
1956
                );
1957
                $radios[] = $form->createElement(
1958
                    'radio',
1959
                    'exerciseType',
1960
                    null,
1961
                    get_lang('One question by page'),
1962
                    '2',
1963
                    [
1964
                        'onclick' => 'check_per_page_one()',
1965
                        'id' => 'option_page_one',
1966
                    ]
1967
                );
1968
1969
                $form->addGroup($radios, null, get_lang('Questions per page'));
1970
            } else {
1971
                // if is Direct feedback but has not questions we can allow to modify the question type
1972
                if (empty($this->iId) || 0 === $this->getQuestionCount()) {
1973
                    $this->setResultFeedbackGroup($form);
1974
                    $this->setResultDisabledGroup($form);
1975
1976
                    // Type of questions disposition on page
1977
                    $radios = [];
1978
                    $radios[] = $form->createElement(
1979
                        'radio',
1980
                        'exerciseType',
1981
                        null,
1982
                        get_lang('All questions on one page'),
1983
                        '1'
1984
                    );
1985
                    $radios[] = $form->createElement(
1986
                        'radio',
1987
                        'exerciseType',
1988
                        null,
1989
                        get_lang('One question by page'),
1990
                        '2'
1991
                    );
1992
                    $form->addGroup($radios, null, get_lang('Sequential'));
1993
                } else {
1994
                    $this->setResultFeedbackGroup($form, true);
1995
                    $group = $this->setResultDisabledGroup($form);
1996
                    $group->freeze();
1997
1998
                    // we force the options to the DirectFeedback exercisetype
1999
                    //$form->addElement('hidden', 'exerciseFeedbackType', $this->getFeedbackType());
2000
                    //$form->addElement('hidden', 'exerciseType', ONE_PER_PAGE);
2001
2002
                    // Type of questions disposition on page
2003
                    $radios[] = $form->createElement(
2004
                        'radio',
2005
                        'exerciseType',
2006
                        null,
2007
                        get_lang('All questions on one page'),
2008
                        '1',
2009
                        [
2010
                            'onclick' => 'check_per_page_all()',
2011
                            'id' => 'option_page_all',
2012
                        ]
2013
                    );
2014
                    $radios[] = $form->createElement(
2015
                        'radio',
2016
                        'exerciseType',
2017
                        null,
2018
                        get_lang('One question by page'),
2019
                        '2',
2020
                        [
2021
                            'onclick' => 'check_per_page_one()',
2022
                            'id' => 'option_page_one',
2023
                        ]
2024
                    );
2025
2026
                    $type_group = $form->addGroup($radios, null, get_lang('Questions per page'));
2027
                    $type_group->freeze();
2028
                }
2029
            }
2030
2031
            $option = [
2032
                EX_Q_SELECTION_ORDERED => get_lang('Ordered by user'),
2033
                //  Defined by user
2034
                EX_Q_SELECTION_RANDOM => get_lang('Random'),
2035
                // 1-10, All
2036
                'per_categories' => '--------'.get_lang('Using categories').'----------',
2037
                // Base (A 123 {3} B 456 {3} C 789{2} D 0{0}) --> Matrix {3, 3, 2, 0}
2038
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED => get_lang(
2039
                    'Ordered categories alphabetically with questions ordered'
2040
                ),
2041
                // A 123 B 456 C 78 (0, 1, all)
2042
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED => get_lang(
2043
                    'Random categories with questions ordered'
2044
                ),
2045
                // C 78 B 456 A 123
2046
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM => get_lang(
2047
                    'Ordered categories alphabetically with random questions'
2048
                ),
2049
                // A 321 B 654 C 87
2050
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM => get_lang(
2051
                    'Random categories with random questions'
2052
                ),
2053
                // C 87 B 654 A 321
2054
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED => get_lang('RandomCategoriesWithQuestionsOrderedNoQuestionGrouped'),
2055
                /*    B 456 C 78 A 123
2056
                        456 78 123
2057
                        123 456 78
2058
                */
2059
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED => get_lang('RandomCategoriesWithRandomQuestionsNoQuestionGrouped'),
2060
                /*
2061
                    A 123 B 456 C 78
2062
                    B 456 C 78 A 123
2063
                    B 654 C 87 A 321
2064
                    654 87 321
2065
                    165 842 73
2066
                */
2067
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED => get_lang('OrderedCategoriesByParentWithQuestionsOrdered'),
2068
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM => get_lang('OrderedCategoriesByParentWithQuestionsRandom'),
2069
            ];
2070
2071
            $form->addSelect(
2072
                'question_selection_type',
2073
                [get_lang('Question selection type')],
2074
                $option,
2075
                [
2076
                    'id' => 'questionSelection',
2077
                    'onchange' => 'checkQuestionSelection()',
2078
                ]
2079
            );
2080
2081
            $group = [
2082
                $form->createElement(
2083
                    'checkbox',
2084
                    'hide_expected_answer',
2085
                    null,
2086
                    get_lang('Hide expected answers column')
2087
                ),
2088
                $form->createElement(
2089
                    'checkbox',
2090
                    'hide_total_score',
2091
                    null,
2092
                    get_lang('Hide total score')
2093
                ),
2094
                $form->createElement(
2095
                    'checkbox',
2096
                    'hide_question_score',
2097
                    null,
2098
                    get_lang('Hide question score')
2099
                ),
2100
                $form->createElement(
2101
                    'checkbox',
2102
                    'hide_category_table',
2103
                    null,
2104
                    get_lang('Hide category table')
2105
                ),
2106
                $form->createElement(
2107
                    'checkbox',
2108
                    'hide_correct_answered_questions',
2109
                    null,
2110
                    get_lang('Hide correct answered questions')
2111
                ),
2112
            ];
2113
            $form->addGroup($group, null, get_lang('Results and feedback page configuration'));
2114
2115
            $group = [
2116
                $form->createElement('radio', 'hide_question_number', null, get_lang('Yes'), '1'),
2117
                $form->createElement('radio', 'hide_question_number', null, get_lang('No'), '0'),
2118
            ];
2119
            $form->addGroup($group, null, get_lang('Hide question numbering'));
2120
2121
            $displayMatrix = 'none';
2122
            $displayRandom = 'none';
2123
            $selectionType = $this->getQuestionSelectionType();
2124
            switch ($selectionType) {
2125
                case EX_Q_SELECTION_RANDOM:
2126
                    $displayRandom = 'block';
2127
2128
                    break;
2129
                case $selectionType >= EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED:
2130
                    $displayMatrix = 'block';
2131
2132
                    break;
2133
            }
2134
2135
            $form->addHtml('<div id="hidden_random" style="display:'.$displayRandom.'">');
2136
            // Number of random question.
2137
            $max = $this->getId() > 0 ? $this->getQuestionCount() : 10;
2138
            $option = range(0, $max);
2139
            $option[0] = get_lang('No');
2140
            $option[-1] = get_lang('All');
2141
            $form->addSelect(
2142
                'randomQuestions',
2143
                [
2144
                    get_lang('Random questions'),
2145
                    get_lang('Random questionsHelp'),
2146
                ],
2147
                $option,
2148
                ['id' => 'randomQuestions']
2149
            );
2150
            $form->addHtml('</div>');
2151
            $form->addHtml('<div id="hidden_matrix" style="display:'.$displayMatrix.'">');
2152
2153
            // Category selection.
2154
            $cat = new TestCategory();
2155
            $cat_form = $cat->returnCategoryForm($this);
2156
            if (empty($cat_form)) {
2157
                $cat_form = '<span class="label label-warning">'.get_lang('No categories defined').'</span>';
2158
            }
2159
            $form->addElement('label', null, $cat_form);
2160
            $form->addHtml('</div>');
2161
2162
            // Random answers.
2163
            $radios_random_answers = [
2164
                $form->createElement('radio', 'randomAnswers', null, get_lang('Yes'), '1'),
2165
                $form->createElement('radio', 'randomAnswers', null, get_lang('No'), '0'),
2166
            ];
2167
            $form->addGroup($radios_random_answers, null, get_lang('Shuffle answers'));
2168
2169
            // Category name.
2170
            $radio_display_cat_name = [
2171
                $form->createElement('radio', 'display_category_name', null, get_lang('Yes'), '1'),
2172
                $form->createElement('radio', 'display_category_name', null, get_lang('No'), '0'),
2173
            ];
2174
            $form->addGroup($radio_display_cat_name, null, get_lang('Display questions category'));
2175
2176
            // Hide question title.
2177
            $group = [
2178
                $form->createElement('radio', 'hide_question_title', null, get_lang('Yes'), '1'),
2179
                $form->createElement('radio', 'hide_question_title', null, get_lang('No'), '0'),
2180
            ];
2181
            $form->addGroup($group, null, get_lang('Hide question title'));
2182
2183
            $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
2184
2185
            if (true === $allow) {
2186
                // Hide question title.
2187
                $group = [
2188
                    $form->createElement(
2189
                        'radio',
2190
                        'show_previous_button',
2191
                        null,
2192
                        get_lang('Yes'),
2193
                        '1'
2194
                    ),
2195
                    $form->createElement(
2196
                        'radio',
2197
                        'show_previous_button',
2198
                        null,
2199
                        get_lang('No'),
2200
                        '0'
2201
                    ),
2202
                ];
2203
                $form->addGroup($group, null, get_lang('Show previous button'));
2204
            }
2205
2206
            $form->addElement(
2207
                'number',
2208
                'exerciseAttempts',
2209
                get_lang('max. 20 characters, e.g. <i>INNOV21</i> number of attempts'),
2210
                null,
2211
                ['id' => 'exerciseAttempts']
2212
            );
2213
2214
            // Exercise time limit
2215
            $form->addElement(
2216
                'checkbox',
2217
                'activate_start_date_check',
2218
                null,
2219
                get_lang('Enable start time'),
2220
                ['onclick' => 'activate_start_date()']
2221
            );
2222
2223
            if (!empty($this->start_time)) {
2224
                $form->addElement('html', '<div id="start_date_div" style="display:block;">');
2225
            } else {
2226
                $form->addElement('html', '<div id="start_date_div" style="display:none;">');
2227
            }
2228
2229
            $form->addElement('date_time_picker', 'start_time');
2230
            $form->addElement('html', '</div>');
2231
            $form->addElement(
2232
                'checkbox',
2233
                'activate_end_date_check',
2234
                null,
2235
                get_lang('Enable end time'),
2236
                ['onclick' => 'activate_end_date()']
2237
            );
2238
2239
            if (!empty($this->end_time)) {
2240
                $form->addHtml('<div id="end_date_div" style="display:block;">');
2241
            } else {
2242
                $form->addHtml('<div id="end_date_div" style="display:none;">');
2243
            }
2244
2245
            $form->addElement('date_time_picker', 'end_time');
2246
            $form->addElement('html', '</div>');
2247
2248
            $display = 'block';
2249
            $form->addElement(
2250
                'checkbox',
2251
                'propagate_neg',
2252
                null,
2253
                get_lang('Propagate negative results between questions')
2254
            );
2255
2256
            $options = [
2257
                '' => get_lang('Please select an option'),
2258
                1 => get_lang('Save the correct answer for the next attempt'),
2259
                2 => get_lang('Pre-fill with answers from previous attempt'),
2260
            ];
2261
            $form->addSelect(
2262
                'save_correct_answers',
2263
                get_lang('Save answers'),
2264
                $options
2265
            );
2266
2267
            $form->addElement('html', '<div class="clear">&nbsp;</div>');
2268
            $form->addCheckBox('review_answers', null, get_lang('Review my answers'));
2269
            $form->addElement('html', '<div id="divtimecontrol"  style="display:'.$display.';">');
2270
2271
            // Timer control
2272
            $form->addElement(
2273
                'checkbox',
2274
                'enabletimercontrol',
2275
                null,
2276
                get_lang('Enable time control'),
2277
                [
2278
                    'onclick' => 'option_time_expired()',
2279
                    'id' => 'enabletimercontrol',
2280
                    'onload' => 'check_load_time()',
2281
                ]
2282
            );
2283
2284
            $expired_date = (int) $this->selectExpiredTime();
2285
2286
            if (('0' != $expired_date)) {
2287
                $form->addElement('html', '<div id="timercontrol" style="display:block;">');
2288
            } else {
2289
                $form->addElement('html', '<div id="timercontrol" style="display:none;">');
2290
            }
2291
            $form->addText(
2292
                'enabletimercontroltotalminutes',
2293
                get_lang('Total duration in minutes of the test'),
2294
                false,
2295
                [
2296
                    'id' => 'enabletimercontroltotalminutes',
2297
                    'cols-size' => [2, 2, 8],
2298
                ]
2299
            );
2300
            $form->addElement('html', '</div>');
2301
            $form->addCheckBox('prevent_backwards', null, get_lang('Prevent moving backwards between questions'));
2302
            $form->addElement(
2303
                'text',
2304
                'pass_percentage',
2305
                [get_lang('Pass percentage'), null, '%'],
2306
                ['id' => 'pass_percentage']
2307
            );
2308
2309
            $form->addRule('pass_percentage', get_lang('Numeric'), 'numeric');
2310
            $form->addRule('pass_percentage', get_lang('Value is too small.'), 'min_numeric_length', 0);
2311
            $form->addRule('pass_percentage', get_lang('Value is too big.'), 'max_numeric_length', 100);
2312
2313
            // add the text_when_finished textbox
2314
            $form->addHtmlEditor(
2315
                'text_when_finished',
2316
                get_lang('Text appearing at the end of the test when the user has succeeded or if no pass percentage was set.'),
2317
                false,
2318
                false,
2319
                $editor_config
2320
            );
2321
            $form->addHtmlEditor(
2322
                'text_when_finished_failure',
2323
                get_lang('Text appearing at the end of the test when the user has failed.'),
2324
                false,
2325
                false,
2326
                $editor_config
2327
            );
2328
2329
            $allow = ('true' === api_get_setting('exercise.allow_notification_setting_per_exercise'));
2330
            if (true === $allow) {
2331
                $settings = ExerciseLib::getNotificationSettings();
2332
                $group = [];
2333
                foreach ($settings as $itemId => $label) {
2334
                    $group[] = $form->createElement(
2335
                        'checkbox',
2336
                        'notifications[]',
2337
                        null,
2338
                        $label,
2339
                        ['value' => $itemId]
2340
                    );
2341
                }
2342
                $form->addGroup($group, '', [get_lang('E-mail notifications')]);
2343
            }
2344
2345
            $form->addCheckBox('update_title_in_lps', null, get_lang('Update this title in learning paths'));
2346
2347
            $defaults = [];
2348
            if ('true' === api_get_setting('search_enabled')) {
2349
                $form->addCheckBox('index_document', '', get_lang('Index document text?'));
2350
                $form->addSelectLanguage('language', get_lang('Document language for indexation'));
2351
                $specific_fields = get_specific_field_list();
2352
2353
                foreach ($specific_fields as $specific_field) {
2354
                    $form->addElement('text', $specific_field['code'], $specific_field['name']);
2355
                    $filter = [
2356
                        'c_id' => api_get_course_int_id(),
2357
                        'field_id' => $specific_field['id'],
2358
                        'ref_id' => $this->getId(),
2359
                        'tool_id' => "'".TOOL_QUIZ."'",
2360
                    ];
2361
                    $values = get_specific_field_values_list($filter, ['value']);
2362
                    if (!empty($values)) {
2363
                        $arr_str_values = [];
2364
                        foreach ($values as $value) {
2365
                            $arr_str_values[] = $value['value'];
2366
                        }
2367
                        $defaults[$specific_field['code']] = implode(', ', $arr_str_values);
2368
                    }
2369
                }
2370
            }
2371
2372
            $skillList = SkillModel::addSkillsToForm($form, ITEM_TYPE_EXERCISE, $this->iId);
2373
2374
            $extraField = new ExtraField('exercise');
2375
            $extraField->addElements(
2376
                $form,
2377
                $this->iId,
2378
                ['notifications'], //exclude
2379
                false, // filter
2380
                false, // tag as select
2381
                [], //show only fields
2382
                [], // order fields
2383
                [] // extra data
2384
            );
2385
            $settings = api_get_configuration_value('exercise_finished_notification_settings');
2386
            if (!empty($settings)) {
2387
                $options = [];
2388
                foreach ($settings as $name => $data) {
2389
                    $options[$name] = $name;
2390
                }
2391
                $form->addSelect(
2392
                    'extra_notifications',
2393
                    get_lang('Notifications'),
2394
                    $options,
2395
                    ['placeholder' => get_lang('SelectAnOption')]
2396
                );
2397
            }
2398
            $form->addElement('html', '</div>'); //End advanced setting
2399
            $form->addElement('html', '</div>');
2400
        }
2401
2402
        // submit
2403
        if (isset($_GET['id'])) {
2404
            $form->addButtonSave(get_lang('Edit test name and settings'), 'submitExercise');
2405
        } else {
2406
            $form->addButtonUpdate(get_lang('Proceed to questions'), 'submitExercise');
2407
        }
2408
2409
        $form->addRule('exerciseTitle', get_lang('Name'), 'required');
2410
2411
        // defaults
2412
        if ('full' == $type) {
2413
            // rules
2414
            $form->addRule('exerciseAttempts', get_lang('Numeric'), 'numeric');
2415
            $form->addRule('start_time', get_lang('Invalid date'), 'datetime');
2416
            $form->addRule('end_time', get_lang('Invalid date'), 'datetime');
2417
2418
            if ($this->getId() > 0) {
2419
                $defaults['randomQuestions'] = $this->random;
2420
                $defaults['randomAnswers'] = $this->getRandomAnswers();
2421
                $defaults['exerciseType'] = $this->selectType();
2422
                $defaults['exerciseTitle'] = $this->get_formated_title();
2423
                $defaults['exerciseDescription'] = $this->selectDescription();
2424
                $defaults['exerciseAttempts'] = $this->selectAttempts();
2425
                $defaults['exerciseFeedbackType'] = $this->getFeedbackType();
2426
                $defaults['results_disabled'] = $this->selectResultsDisabled();
2427
                $defaults['propagate_neg'] = $this->selectPropagateNeg();
2428
                $defaults['save_correct_answers'] = $this->getSaveCorrectAnswers();
2429
                $defaults['review_answers'] = $this->review_answers;
2430
                $defaults['randomByCat'] = $this->getRandomByCategory();
2431
                $defaults['text_when_finished'] = $this->getTextWhenFinished();
2432
                $defaults['text_when_finished_failure'] = $this->getTextWhenFinishedFailure();
2433
                $defaults['display_category_name'] = $this->selectDisplayCategoryName();
2434
                $defaults['pass_percentage'] = $this->selectPassPercentage();
2435
                $defaults['question_selection_type'] = $this->getQuestionSelectionType();
2436
                $defaults['hide_question_title'] = $this->getHideQuestionTitle();
2437
                $defaults['show_previous_button'] = $this->showPreviousButton();
2438
                $defaults['quiz_category_id'] = $this->getQuizCategoryId();
2439
                $defaults['prevent_backwards'] = $this->getPreventBackwards();
2440
                $defaults['hide_question_number'] = $this->getHideQuestionNumber();
2441
2442
                if (!empty($this->start_time)) {
2443
                    $defaults['activate_start_date_check'] = 1;
2444
                }
2445
                if (!empty($this->end_time)) {
2446
                    $defaults['activate_end_date_check'] = 1;
2447
                }
2448
2449
                $defaults['start_time'] = !empty($this->start_time) ? api_get_local_time($this->start_time) : date(
2450
                    'Y-m-d 12:00:00'
2451
                );
2452
                $defaults['end_time'] = !empty($this->end_time) ? api_get_local_time($this->end_time) : date(
2453
                    'Y-m-d 12:00:00',
2454
                    time() + 84600
2455
                );
2456
2457
                // Get expired time
2458
                if ('0' != $this->expired_time) {
2459
                    $defaults['enabletimercontrol'] = 1;
2460
                    $defaults['enabletimercontroltotalminutes'] = $this->expired_time;
2461
                } else {
2462
                    $defaults['enabletimercontroltotalminutes'] = 0;
2463
                }
2464
                $defaults['skills'] = array_keys($skillList);
2465
                $defaults['notifications'] = $this->getNotifications();
2466
            } else {
2467
                $defaults['exerciseType'] = 2;
2468
                $defaults['exerciseAttempts'] = 0;
2469
                $defaults['randomQuestions'] = 0;
2470
                $defaults['randomAnswers'] = 0;
2471
                $defaults['exerciseDescription'] = '';
2472
                $defaults['exerciseFeedbackType'] = 0;
2473
                $defaults['results_disabled'] = 0;
2474
                $defaults['randomByCat'] = 0;
2475
                $defaults['text_when_finished'] = '';
2476
                $defaults['text_when_finished_failure'] = '';
2477
                $defaults['start_time'] = date('Y-m-d 12:00:00');
2478
                $defaults['display_category_name'] = 1;
2479
                $defaults['end_time'] = date('Y-m-d 12:00:00', time() + 84600);
2480
                $defaults['pass_percentage'] = '';
2481
                $defaults['end_button'] = $this->selectEndButton();
2482
                $defaults['question_selection_type'] = 1;
2483
                $defaults['hide_question_title'] = 0;
2484
                $defaults['show_previous_button'] = 1;
2485
                $defaults['on_success_message'] = null;
2486
                $defaults['on_failed_message'] = null;
2487
            }
2488
        } else {
2489
            $defaults['exerciseTitle'] = $this->selectTitle();
2490
            $defaults['exerciseDescription'] = $this->selectDescription();
2491
        }
2492
2493
        if ('true' === api_get_setting('search_enabled')) {
2494
            $defaults['index_document'] = 'checked="checked"';
2495
        }
2496
2497
        $this->setPageResultConfigurationDefaults($defaults);
2498
        $form->setDefaults($defaults);
2499
2500
        // Freeze some elements.
2501
        if (0 != $this->getId() && false == $this->edit_exercise_in_lp) {
2502
            $elementsToFreeze = [
2503
                'randomQuestions',
2504
                //'randomByCat',
2505
                'exerciseAttempts',
2506
                'propagate_neg',
2507
                'enabletimercontrol',
2508
                'review_answers',
2509
            ];
2510
2511
            foreach ($elementsToFreeze as $elementName) {
2512
                /** @var HTML_QuickForm_element $element */
2513
                $element = $form->getElement($elementName);
2514
                $element->freeze();
2515
            }
2516
        }
2517
    }
2518
2519
    public function setResultFeedbackGroup(FormValidator $form, $checkFreeze = true)
2520
    {
2521
        // Feedback type.
2522
        $feedback = [];
2523
        $warning = sprintf(
2524
            get_lang('TheSettingXWillChangeToX'),
2525
            get_lang('ShowResultsToStudents'),
2526
            get_lang('ShowScoreAndRightAnswer')
2527
        );
2528
        $endTest = $form->createElement(
2529
            'radio',
2530
            'exerciseFeedbackType',
2531
            null,
2532
            get_lang('At end of test'),
2533
            EXERCISE_FEEDBACK_TYPE_END,
2534
            [
2535
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_END,
2536
                //'onclick' => 'if confirm() check_feedback()',
2537
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_feedback(); } else { return false;} ',
2538
            ]
2539
        );
2540
2541
        $noFeedBack = $form->createElement(
2542
            'radio',
2543
            'exerciseFeedbackType',
2544
            null,
2545
            get_lang('Exam (no feedback)'),
2546
            EXERCISE_FEEDBACK_TYPE_EXAM,
2547
            [
2548
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_EXAM,
2549
            ]
2550
        );
2551
2552
        $feedback[] = $endTest;
2553
        $feedback[] = $noFeedBack;
2554
2555
        $scenarioEnabled = 'true' === api_get_setting('enable_quiz_scenario');
2556
        $freeze = true;
2557
        if ($scenarioEnabled) {
2558
            if ($this->getQuestionCount() > 0) {
2559
                $hasDifferentQuestion = $this->hasQuestionWithTypeNotInList([UNIQUE_ANSWER, HOT_SPOT_DELINEATION]);
2560
                if (false === $hasDifferentQuestion) {
2561
                    $freeze = false;
2562
                }
2563
            } else {
2564
                $freeze = false;
2565
            }
2566
            // Can't convert a question from one feedback to another
2567
            $direct = $form->createElement(
2568
                'radio',
2569
                'exerciseFeedbackType',
2570
                null,
2571
                get_lang('Adaptative test with immediate feedback'),
2572
                EXERCISE_FEEDBACK_TYPE_DIRECT,
2573
                [
2574
                    'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_DIRECT,
2575
                    'onclick' => 'check_direct_feedback()',
2576
                ]
2577
            );
2578
2579
            $directPopUp = $form->createElement(
2580
                'radio',
2581
                'exerciseFeedbackType',
2582
                null,
2583
                get_lang('Direct pop-up mode'),
2584
                EXERCISE_FEEDBACK_TYPE_POPUP,
2585
                ['id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_POPUP, 'onclick' => 'check_direct_feedback()']
2586
            );
2587
            if ($freeze) {
2588
                $direct->freeze();
2589
                $directPopUp->freeze();
2590
            }
2591
2592
            // If has delineation freeze all.
2593
            $hasDelineation = $this->hasQuestionWithType(HOT_SPOT_DELINEATION);
2594
            if ($hasDelineation) {
2595
                $endTest->freeze();
2596
                $noFeedBack->freeze();
2597
                $direct->freeze();
2598
                $directPopUp->freeze();
2599
            }
2600
2601
            $feedback[] = $direct;
2602
            $feedback[] = $directPopUp;
2603
        }
2604
2605
        $form->addGroup(
2606
            $feedback,
2607
            null,
2608
            [
2609
                get_lang('Feedback'),
2610
                get_lang(
2611
                    'How should we show the feedback/comment for each question? This option defines how it will be shown to the learner when taking the test. We recommend you try different options by editing your test options before having learners take it.'
2612
                ),
2613
            ]
2614
        );
2615
    }
2616
2617
    /**
2618
     * function which process the creation of exercises.
2619
     *
2620
     * @param FormValidator $form
2621
     *
2622
     * @return int c_quiz.iid
2623
     */
2624
    public function processCreation($form)
2625
    {
2626
        $this->updateTitle(self::format_title_variable($form->getSubmitValue('exerciseTitle')));
2627
        $this->updateDescription($form->getSubmitValue('exerciseDescription'));
2628
        $this->updateAttempts($form->getSubmitValue('exerciseAttempts'));
2629
        $this->updateFeedbackType($form->getSubmitValue('exerciseFeedbackType'));
2630
        $this->updateType($form->getSubmitValue('exerciseType'));
2631
2632
        // If direct feedback then force to One per page
2633
        if (EXERCISE_FEEDBACK_TYPE_DIRECT == $form->getSubmitValue('exerciseFeedbackType')) {
2634
            $this->updateType(ONE_PER_PAGE);
2635
        }
2636
2637
        $this->setRandom($form->getSubmitValue('randomQuestions'));
2638
        $this->updateRandomAnswers($form->getSubmitValue('randomAnswers'));
2639
        $this->updateResultsDisabled($form->getSubmitValue('results_disabled'));
2640
        $this->updateExpiredTime($form->getSubmitValue('enabletimercontroltotalminutes'));
2641
        $this->updatePropagateNegative($form->getSubmitValue('propagate_neg'));
2642
        $this->updateSaveCorrectAnswers($form->getSubmitValue('save_correct_answers'));
2643
        $this->updateRandomByCat($form->getSubmitValue('randomByCat'));
2644
        $this->setTextWhenFinished($form->getSubmitValue('text_when_finished'));
2645
        $this->setTextWhenFinishedFailure($form->getSubmitValue('text_when_finished_failure'));
2646
        $this->updateDisplayCategoryName($form->getSubmitValue('display_category_name'));
2647
        $this->updateReviewAnswers($form->getSubmitValue('review_answers'));
2648
        $this->updatePassPercentage($form->getSubmitValue('pass_percentage'));
2649
        $this->updateCategories($form->getSubmitValue('category'));
2650
        $this->updateEndButton($form->getSubmitValue('end_button'));
2651
        $this->setOnSuccessMessage($form->getSubmitValue('on_success_message'));
2652
        $this->setOnFailedMessage($form->getSubmitValue('on_failed_message'));
2653
        $this->updateEmailNotificationTemplate($form->getSubmitValue('email_notification_template'));
2654
        $this->setEmailNotificationTemplateToUser($form->getSubmitValue('email_notification_template_to_user'));
2655
        $this->setNotifyUserByEmail($form->getSubmitValue('notify_user_by_email'));
2656
        $this->setModelType($form->getSubmitValue('model_type'));
2657
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2658
        $this->setHideQuestionTitle($form->getSubmitValue('hide_question_title'));
2659
        $this->sessionId = api_get_session_id();
2660
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2661
        $this->setScoreTypeModel($form->getSubmitValue('score_type_model'));
2662
        $this->setGlobalCategoryId($form->getSubmitValue('global_category_id'));
2663
        $this->setShowPreviousButton($form->getSubmitValue('show_previous_button'));
2664
        $this->setNotifications($form->getSubmitValue('notifications'));
2665
        $this->setQuizCategoryId($form->getSubmitValue('quiz_category_id'));
2666
        $this->setPageResultConfiguration($form->getSubmitValues());
2667
        $this->setHideQuestionNumber($form->getSubmitValue('hide_question_number'));
2668
        $this->preventBackwards = (int) $form->getSubmitValue('prevent_backwards');
2669
2670
        $this->start_time = null;
2671
        if (1 == $form->getSubmitValue('activate_start_date_check')) {
2672
            $start_time = $form->getSubmitValue('start_time');
2673
            $this->start_time = api_get_utc_datetime($start_time);
2674
        }
2675
2676
        $this->end_time = null;
2677
        if (1 == $form->getSubmitValue('activate_end_date_check')) {
2678
            $end_time = $form->getSubmitValue('end_time');
2679
            $this->end_time = api_get_utc_datetime($end_time);
2680
        }
2681
2682
        $this->expired_time = 0;
2683
        if (1 == $form->getSubmitValue('enabletimercontrol')) {
2684
            $expired_total_time = $form->getSubmitValue('enabletimercontroltotalminutes');
2685
            if (0 == $this->expired_time) {
2686
                $this->expired_time = $expired_total_time;
2687
            }
2688
        }
2689
2690
        $this->random_answers = 0;
2691
        if (1 == $form->getSubmitValue('randomAnswers')) {
2692
            $this->random_answers = 1;
2693
        }
2694
2695
        // Update title in all LPs that have this quiz added
2696
        if (1 == $form->getSubmitValue('update_title_in_lps')) {
2697
            $table = Database::get_course_table(TABLE_LP_ITEM);
2698
            $sql = "SELECT iid FROM $table
2699
                    WHERE
2700
                        item_type = 'quiz' AND
2701
                        path = '".$this->getId()."'
2702
                    ";
2703
            $result = Database::query($sql);
2704
            $items = Database::store_result($result);
2705
            if (!empty($items)) {
2706
                foreach ($items as $item) {
2707
                    $itemId = $item['iid'];
2708
                    $sql = "UPDATE $table
2709
                            SET title = '".$this->title."'
2710
                            WHERE iid = $itemId ";
2711
                    Database::query($sql);
2712
                }
2713
            }
2714
        }
2715
2716
        $iId = $this->save();
2717
        if (!empty($iId)) {
2718
            $values = $form->getSubmitValues();
2719
            $values['item_id'] = $iId;
2720
            $extraFieldValue = new ExtraFieldValue('exercise');
2721
            $extraFieldValue->saveFieldValues($values);
2722
2723
            SkillModel::saveSkills($form, ITEM_TYPE_EXERCISE, $iId);
2724
        }
2725
    }
2726
2727
    public function search_engine_save()
2728
    {
2729
        if (1 != $_POST['index_document']) {
2730
            return;
2731
        }
2732
        $course_id = api_get_course_id();
2733
        $specific_fields = get_specific_field_list();
2734
        $ic_slide = new IndexableChunk();
2735
2736
        $all_specific_terms = '';
2737
        foreach ($specific_fields as $specific_field) {
2738
            if (isset($_REQUEST[$specific_field['code']])) {
2739
                $sterms = trim($_REQUEST[$specific_field['code']]);
2740
                if (!empty($sterms)) {
2741
                    $all_specific_terms .= ' '.$sterms;
2742
                    $sterms = explode(',', $sterms);
2743
                    foreach ($sterms as $sterm) {
2744
                        $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2745
                        add_specific_field_value($specific_field['id'], $course_id, TOOL_QUIZ, $this->getId(), $sterm);
2746
                    }
2747
                }
2748
            }
2749
        }
2750
2751
        // build the chunk to index
2752
        $ic_slide->addValue('title', $this->exercise);
2753
        $ic_slide->addCourseId($course_id);
2754
        $ic_slide->addToolId(TOOL_QUIZ);
2755
        $xapian_data = [
2756
            SE_COURSE_ID => $course_id,
2757
            SE_TOOL_ID => TOOL_QUIZ,
2758
            SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->getId()],
2759
            SE_USER => (int) api_get_user_id(),
2760
        ];
2761
        $ic_slide->xapian_data = serialize($xapian_data);
2762
        $exercise_description = $all_specific_terms.' '.$this->description;
2763
        $ic_slide->addValue('content', $exercise_description);
2764
2765
        $di = new ChamiloIndexer();
2766
        isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
2767
        $di->connectDb(null, null, $lang);
2768
        $di->addChunk($ic_slide);
2769
2770
        //index and return search engine document id
2771
        $did = $di->index();
2772
        if ($did) {
2773
            // save it to db
2774
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2775
            $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2776
			    VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2777
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId(), $did);
2778
            Database::query($sql);
2779
        }
2780
    }
2781
2782
    public function search_engine_edit()
2783
    {
2784
        // update search enchine and its values table if enabled
2785
        if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
2786
            $course_id = api_get_course_id();
2787
2788
            // actually, it consists on delete terms from db,
2789
            // insert new ones, create a new search engine document, and remove the old one
2790
            // get search_did
2791
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2792
            $sql = 'SELECT * FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s LIMIT 1';
2793
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2794
            $res = Database::query($sql);
2795
2796
            if (Database::num_rows($res) > 0) {
2797
                $se_ref = Database::fetch_array($res);
2798
                $specific_fields = get_specific_field_list();
2799
                $ic_slide = new IndexableChunk();
2800
2801
                $all_specific_terms = '';
2802
                foreach ($specific_fields as $specific_field) {
2803
                    delete_all_specific_field_value($course_id, $specific_field['id'], TOOL_QUIZ, $this->getId());
2804
                    if (isset($_REQUEST[$specific_field['code']])) {
2805
                        $sterms = trim($_REQUEST[$specific_field['code']]);
2806
                        $all_specific_terms .= ' '.$sterms;
2807
                        $sterms = explode(',', $sterms);
2808
                        foreach ($sterms as $sterm) {
2809
                            $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2810
                            add_specific_field_value(
2811
                                $specific_field['id'],
2812
                                $course_id,
2813
                                TOOL_QUIZ,
2814
                                $this->getId(),
2815
                                $sterm
2816
                            );
2817
                        }
2818
                    }
2819
                }
2820
2821
                // build the chunk to index
2822
                $ic_slide->addValue('title', $this->exercise);
2823
                $ic_slide->addCourseId($course_id);
2824
                $ic_slide->addToolId(TOOL_QUIZ);
2825
                $xapian_data = [
2826
                    SE_COURSE_ID => $course_id,
2827
                    SE_TOOL_ID => TOOL_QUIZ,
2828
                    SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->getId()],
2829
                    SE_USER => (int) api_get_user_id(),
2830
                ];
2831
                $ic_slide->xapian_data = serialize($xapian_data);
2832
                $exercise_description = $all_specific_terms.' '.$this->description;
2833
                $ic_slide->addValue('content', $exercise_description);
2834
2835
                $di = new ChamiloIndexer();
2836
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
2837
                $di->connectDb(null, null, $lang);
2838
                $di->remove_document($se_ref['search_did']);
2839
                $di->addChunk($ic_slide);
2840
2841
                //index and return search engine document id
2842
                $did = $di->index();
2843
                if ($did) {
2844
                    // save it to db
2845
                    $sql = 'DELETE FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=\'%s\'';
2846
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2847
                    Database::query($sql);
2848
                    $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2849
                        VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2850
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId(), $did);
2851
                    Database::query($sql);
2852
                }
2853
            } else {
2854
                $this->search_engine_save();
2855
            }
2856
        }
2857
    }
2858
2859
    public function search_engine_delete()
2860
    {
2861
        // remove from search engine if enabled
2862
        if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
2863
            $course_id = api_get_course_id();
2864
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2865
            $sql = 'SELECT * FROM %s
2866
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
2867
                    LIMIT 1';
2868
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2869
            $res = Database::query($sql);
2870
            if (Database::num_rows($res) > 0) {
2871
                $row = Database::fetch_array($res);
2872
                $di = new ChamiloIndexer();
2873
                $di->remove_document($row['search_did']);
2874
                unset($di);
2875
                $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2876
                foreach ($this->questionList as $question_i) {
2877
                    $sql = 'SELECT type FROM %s WHERE id=%s';
2878
                    $sql = sprintf($sql, $tbl_quiz_question, $question_i);
2879
                    $qres = Database::query($sql);
2880
                    if (Database::num_rows($qres) > 0) {
2881
                        $qrow = Database::fetch_array($qres);
2882
                        $objQuestion = Question::getInstance($qrow['type']);
2883
                        $objQuestion = Question::read((int) $question_i);
2884
                        $objQuestion->search_engine_edit($this->getId(), false, true);
2885
                        unset($objQuestion);
2886
                    }
2887
                }
2888
            }
2889
            $sql = 'DELETE FROM %s
2890
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
2891
                    LIMIT 1';
2892
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2893
            Database::query($sql);
2894
2895
            // remove terms from db
2896
            delete_all_values_for_item($course_id, TOOL_QUIZ, $this->getId());
2897
        }
2898
    }
2899
2900
    public function selectExpiredTime()
2901
    {
2902
        return $this->expired_time;
2903
    }
2904
2905
    /**
2906
     * Cleans the student's results only for the Exercise tool (Not from the LP)
2907
     * The LP results are NOT deleted by default, otherwise put $cleanLpTests = true
2908
     * Works with exercises in sessions.
2909
     *
2910
     * @param bool   $cleanLpTests
2911
     * @param string $cleanResultBeforeDate
2912
     *
2913
     * @return int quantity of user's exercises deleted
2914
     */
2915
    public function cleanResults($cleanLpTests = false, $cleanResultBeforeDate = null)
2916
    {
2917
        $sessionId = api_get_session_id();
2918
        $table_track_e_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2919
        $table_track_e_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
2920
2921
        $sql_where = '  AND
2922
                        orig_lp_id = 0 AND
2923
                        orig_lp_item_id = 0';
2924
2925
        // if we want to delete results from LP too
2926
        if ($cleanLpTests) {
2927
            $sql_where = '';
2928
        }
2929
2930
        // if we want to delete attempts before date $cleanResultBeforeDate
2931
        // $cleanResultBeforeDate must be a valid UTC-0 date yyyy-mm-dd
2932
        if (!empty($cleanResultBeforeDate)) {
2933
            $cleanResultBeforeDate = Database::escape_string($cleanResultBeforeDate);
2934
            if (api_is_valid_date($cleanResultBeforeDate)) {
2935
                $sql_where .= "  AND exe_date <= '$cleanResultBeforeDate' ";
2936
            } else {
2937
                return 0;
2938
            }
2939
        }
2940
2941
        $sessionCondition = api_get_session_condition($sessionId);
2942
        $sql = "SELECT exe_id
2943
                FROM $table_track_e_exercises
2944
                WHERE
2945
                    c_id = ".api_get_course_int_id().' AND
2946
                    exe_exo_id = '.$this->getId()."
2947
                    $sessionCondition
2948
                    $sql_where";
2949
2950
        $result = Database::query($sql);
2951
        $exe_list = Database::store_result($result);
2952
2953
        // deleting TRACK_E_ATTEMPT table
2954
        // check if exe in learning path or not
2955
        $i = 0;
2956
        if (is_array($exe_list) && count($exe_list) > 0) {
2957
            foreach ($exe_list as $item) {
2958
                $sql = "DELETE FROM $table_track_e_attempt
2959
                        WHERE exe_id = '".$item['exe_id']."'";
2960
                Database::query($sql);
2961
                $i++;
2962
            }
2963
        }
2964
2965
        // delete TRACK_E_EXERCISES table
2966
        $sql = "DELETE FROM $table_track_e_exercises
2967
                WHERE
2968
                  c_id = ".api_get_course_int_id().' AND
2969
                  exe_exo_id = '.$this->getId()." $sql_where $sessionCondition";
2970
        Database::query($sql);
2971
2972
        $this->generateStats($this->getId(), api_get_course_info(), $sessionId);
2973
2974
        Event::addEvent(
2975
            LOG_EXERCISE_RESULT_DELETE,
2976
            LOG_EXERCISE_ID,
2977
            $this->getId(),
2978
            null,
2979
            null,
2980
            api_get_course_int_id(),
2981
            $sessionId
2982
        );
2983
2984
        return $i;
2985
    }
2986
2987
    /**
2988
     * Copies an exercise (duplicate all questions and answers).
2989
     */
2990
    public function copyExercise()
2991
    {
2992
        $exerciseObject = $this;
2993
        $categories = $exerciseObject->getCategoriesInExercise(true);
2994
        // Get all questions no matter the order/category settings
2995
        $questionList = $exerciseObject->getQuestionOrderedList();
2996
        $sourceId = $exerciseObject->iId;
2997
        // Force the creation of a new exercise
2998
        $exerciseObject->updateTitle($exerciseObject->selectTitle().' - '.get_lang('Copy'));
2999
        // Hides the new exercise
3000
        $exerciseObject->updateStatus(false);
3001
        $exerciseObject->iId = 0;
3002
        $exerciseObject->sessionId = api_get_session_id();
3003
        $courseId = api_get_course_int_id();
3004
        $exerciseObject->save();
3005
        $newId = $exerciseObject->getId();
3006
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
3007
3008
        $count = 1;
3009
        $batchSize = 20;
3010
        $em = Database::getManager();
3011
        if ($newId && !empty($questionList)) {
3012
            $extraField = new ExtraFieldValue('exercise');
3013
            $extraField->copy($sourceId, $newId);
3014
            // Question creation
3015
            foreach ($questionList as $oldQuestionId) {
3016
                $oldQuestionObj = Question::read($oldQuestionId, null, false);
3017
                $newQuestionId = $oldQuestionObj->duplicate();
3018
                if ($newQuestionId) {
3019
                    $newQuestionObj = Question::read($newQuestionId, null, false);
3020
                    if (isset($newQuestionObj) && $newQuestionObj) {
3021
                        $sql = "INSERT INTO $exerciseRelQuestionTable (question_id, quiz_id, question_order)
3022
                                VALUES (".$newQuestionId.", ".$newId.", '$count')";
3023
                        Database::query($sql);
3024
                        $count++;
3025
                        if (!empty($oldQuestionObj->category)) {
3026
                            $newQuestionObj->saveCategory($oldQuestionObj->category);
3027
                        }
3028
3029
                        // This should be moved to the duplicate function
3030
                        $newAnswerObj = new Answer($oldQuestionId, $courseId, $exerciseObject);
3031
                        $newAnswerObj->read();
3032
                        $newAnswerObj->duplicate($newQuestionObj);
3033
                        if (($count % $batchSize) === 0) {
3034
                            $em->clear(); // Detaches all objects from Doctrine!
3035
                        }
3036
                    }
3037
                }
3038
            }
3039
            if (!empty($categories)) {
3040
                $newCategoryList = [];
3041
                foreach ($categories as $category) {
3042
                    $newCategoryList[$category['category_id']] = $category['count_questions'];
3043
                }
3044
                $exerciseObject->saveCategoriesInExercise($newCategoryList);
3045
            }
3046
        }
3047
    }
3048
3049
    /**
3050
     * Changes the exercise status.
3051
     *
3052
     * @param string $status - exercise status
3053
     */
3054
    public function updateStatus($status)
3055
    {
3056
        $this->active = $status;
3057
    }
3058
3059
    /**
3060
     * @param int    $lp_id
3061
     * @param int    $lp_item_id
3062
     * @param int    $lp_item_view_id
3063
     * @param string $status
3064
     *
3065
     * @return array
3066
     */
3067
    public function get_stat_track_exercise_info(
3068
        $lp_id = 0,
3069
        $lp_item_id = 0,
3070
        $lp_item_view_id = 0,
3071
        $status = 'incomplete'
3072
    ) {
3073
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3074
        $lp_id = (int) $lp_id;
3075
        $lp_item_id = (int) $lp_item_id;
3076
        $lp_item_view_id = (int) $lp_item_view_id;
3077
3078
        $sessionCondition = api_get_session_condition(api_get_session_id());
3079
        $condition = " WHERE exe_exo_id 	= ".$this->getId()." AND
3080
					   exe_user_id 			= '".api_get_user_id()."' AND
3081
					   c_id                 = ".api_get_course_int_id()." AND
3082
					   status 				= '".Database::escape_string($status)."' AND
3083
					   orig_lp_id 			= $lp_id AND
3084
					   orig_lp_item_id 		= $lp_item_id AND
3085
                       orig_lp_item_view_id =  $lp_item_view_id
3086
					   ";
3087
3088
        $sql_track = " SELECT * FROM  $track_exercises $condition $sessionCondition LIMIT 1 ";
3089
3090
        $result = Database::query($sql_track);
3091
        $new_array = [];
3092
        if (Database::num_rows($result) > 0) {
3093
            $new_array = Database::fetch_assoc($result);
3094
            $new_array['num_exe'] = Database::num_rows($result);
3095
        }
3096
3097
        return $new_array;
3098
    }
3099
3100
    /**
3101
     * Saves a test attempt.
3102
     *
3103
     * @param int   $clock_expired_time   clock_expired_time
3104
     * @param int   $safe_lp_id           lp id
3105
     * @param int   $safe_lp_item_id      lp item id
3106
     * @param int   $safe_lp_item_view_id lp item_view id
3107
     * @param array $questionList
3108
     * @param float $weight
3109
     *
3110
     * @throws \Doctrine\ORM\ORMException
3111
     * @throws \Doctrine\ORM\OptimisticLockException
3112
     * @throws \Doctrine\ORM\TransactionRequiredException
3113
     *
3114
     * @return int
3115
     */
3116
    public function save_stat_track_exercise_info(
3117
        $clock_expired_time,
3118
        $safe_lp_id = 0,
3119
        $safe_lp_item_id = 0,
3120
        $safe_lp_item_view_id = 0,
3121
        $questionList = [],
3122
        $weight = 0
3123
    ) {
3124
        $safe_lp_id = (int) $safe_lp_id;
3125
        $safe_lp_item_id = (int) $safe_lp_item_id;
3126
        $safe_lp_item_view_id = (int) $safe_lp_item_view_id;
3127
3128
        if (empty($clock_expired_time)) {
3129
            $clock_expired_time = null;
3130
        }
3131
3132
        $questionList = array_map('intval', $questionList);
3133
        $em = Database::getManager();
3134
3135
        $quiz = $em->find(CQuiz::class, $this->getId());
3136
3137
        $trackExercise = (new TrackEExercise())
3138
            ->setSession(api_get_session_entity())
3139
            ->setCourse(api_get_course_entity())
3140
            ->setMaxScore($weight)
3141
            ->setDataTracking(implode(',', $questionList))
3142
            ->setUser(api_get_user_entity())
3143
            ->setUserIp(api_get_real_ip())
3144
            ->setOrigLpId($safe_lp_id)
3145
            ->setOrigLpItemId($safe_lp_item_id)
3146
            ->setOrigLpItemViewId($safe_lp_item_view_id)
3147
            ->setExpiredTimeControl($clock_expired_time)
3148
            ->setQuiz($quiz)
3149
        ;
3150
        $em->persist($trackExercise);
3151
        $em->flush();
3152
3153
        return $trackExercise->getExeId();
3154
    }
3155
3156
    /**
3157
     * @param int    $question_id
3158
     * @param int    $questionNum
3159
     * @param array  $questions_in_media
3160
     * @param string $currentAnswer
3161
     * @param array  $myRemindList
3162
     * @param bool   $showPreviousButton
3163
     *
3164
     * @return string
3165
     */
3166
    public function show_button(
3167
        $question_id,
3168
        $questionNum,
3169
        $questions_in_media = [],
3170
        $currentAnswer = '',
3171
        $myRemindList = [],
3172
        $showPreviousButton = true
3173
    ) {
3174
        global $safe_lp_id, $safe_lp_item_id, $safe_lp_item_view_id;
3175
        $nbrQuestions = $this->countQuestionsInExercise();
3176
        $buttonList = [];
3177
        $html = $label = '';
3178
        $hotspotGet = isset($_POST['hotspot']) ? Security::remove_XSS($_POST['hotspot']) : null;
3179
3180
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]) &&
3181
            ONE_PER_PAGE == $this->type
3182
        ) {
3183
            $urlTitle = get_lang('Proceed with the test');
3184
            if ($questionNum == count($this->questionList)) {
3185
                $urlTitle = get_lang('End test');
3186
            }
3187
3188
            $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit_modal.php?'.api_get_cidreq();
3189
            $url .= '&'.http_build_query(
3190
                    [
3191
                        'learnpath_id' => $safe_lp_id,
3192
                        'learnpath_item_id' => $safe_lp_item_id,
3193
                        'learnpath_item_view_id' => $safe_lp_item_view_id,
3194
                        'hotspot' => $hotspotGet,
3195
                        'nbrQuestions' => $nbrQuestions,
3196
                        'num' => $questionNum,
3197
                        'exerciseType' => $this->type,
3198
                        'exerciseId' => $this->getId(),
3199
                        'reminder' => empty($myRemindList) ? null : 2,
3200
                        'tryagain' => isset($_REQUEST['tryagain']) && 1 === (int) $_REQUEST['tryagain'] ? 1 : 0,
3201
                    ]
3202
                );
3203
3204
            $params = [
3205
                'class' => 'ajax btn btn--plain no-close-button',
3206
                'data-title' => Security::remove_XSS(get_lang('Comment')),
3207
                'data-size' => 'md',
3208
                'id' => "button_$question_id",
3209
            ];
3210
3211
            if (EXERCISE_FEEDBACK_TYPE_POPUP === $this->getFeedbackType()) {
3212
                //$params['data-block-div-after-closing'] = "question_div_$question_id";
3213
                $params['data-block-closing'] = 'true';
3214
                $params['class'] .= ' no-header ';
3215
            }
3216
3217
            $html .= Display::url($urlTitle, $url, $params);
3218
            $html .= '<br />';
3219
3220
            return $html;
3221
        }
3222
3223
        if (!api_is_allowed_to_session_edit()) {
3224
            return '';
3225
        }
3226
3227
        $isReviewingAnswers = isset($_REQUEST['reminder']) && 2 == $_REQUEST['reminder'];
3228
3229
        // User
3230
        $endReminderValue = false;
3231
        if (!empty($myRemindList) && $isReviewingAnswers) {
3232
            $endValue = end($myRemindList);
3233
            if ($endValue == $question_id) {
3234
                $endReminderValue = true;
3235
            }
3236
        }
3237
        $endTest = false;
3238
        if (ALL_ON_ONE_PAGE == $this->type || $nbrQuestions == $questionNum || $endReminderValue) {
3239
            if ($this->review_answers) {
3240
                $label = get_lang('ReviewQuestions');
3241
                $class = 'btn btn--success';
3242
            } else {
3243
                $endTest = true;
3244
                $label = get_lang('End Test');
3245
                $class = 'btn btn--warning';
3246
            }
3247
        } else {
3248
            $label = get_lang('Next question');
3249
            $class = 'btn btn--primary';
3250
        }
3251
        // used to select it with jquery
3252
        $class .= ' question-validate-btn';
3253
        if (ONE_PER_PAGE == $this->type) {
3254
            if (1 != $questionNum && $this->showPreviousButton()) {
3255
                $prev_question = $questionNum - 2;
3256
                $showPreview = true;
3257
                if (!empty($myRemindList) && $isReviewingAnswers) {
3258
                    $beforeId = null;
3259
                    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...
3260
                        if (isset($myRemindList[$i]) && $myRemindList[$i] == $question_id) {
3261
                            $beforeId = isset($myRemindList[$i - 1]) ? $myRemindList[$i - 1] : null;
3262
3263
                            break;
3264
                        }
3265
                    }
3266
3267
                    if (empty($beforeId)) {
3268
                        $showPreview = false;
3269
                    } else {
3270
                        $num = 0;
3271
                        foreach ($this->questionList as $originalQuestionId) {
3272
                            if ($originalQuestionId == $beforeId) {
3273
                                break;
3274
                            }
3275
                            $num++;
3276
                        }
3277
                        $prev_question = $num;
3278
                    }
3279
                }
3280
3281
                if ($showPreviousButton && $showPreview && 0 === $this->getPreventBackwards()) {
3282
                    $buttonList[] = Display::button(
3283
                        'previous_question_and_save',
3284
                        get_lang('Previous question'),
3285
                        [
3286
                            'type' => 'button',
3287
                            'class' => 'btn btn--plain',
3288
                            'data-prev' => $prev_question,
3289
                            'data-question' => $question_id,
3290
                        ]
3291
                    );
3292
                }
3293
            }
3294
3295
            // Next question
3296
            if (!empty($questions_in_media)) {
3297
                $buttonList[] = Display::button(
3298
                    'save_question_list',
3299
                    $label,
3300
                    [
3301
                        'type' => 'button',
3302
                        'class' => $class,
3303
                        'data-list' => implode(',', $questions_in_media),
3304
                    ]
3305
                );
3306
            } else {
3307
                $attributes = ['type' => 'button', 'class' => $class, 'data-question' => $question_id];
3308
                $name = 'save_now';
3309
                if ($endTest && api_get_configuration_value('quiz_check_all_answers_before_end_test')) {
3310
                    $name = 'check_answers';
3311
                }
3312
                $buttonList[] = Display::button(
3313
                    $name,
3314
                    $label,
3315
                    $attributes
3316
                );
3317
            }
3318
            $buttonList[] = '<span id="save_for_now_'.$question_id.'" class="exercise_save_mini_message"></span>';
3319
3320
            $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3321
3322
            return $html;
3323
        }
3324
3325
        if ($this->review_answers) {
3326
            $all_label = get_lang('Review selected questions');
3327
            $class = 'btn btn--success';
3328
        } else {
3329
            $all_label = get_lang('End test');
3330
            $class = 'btn btn--warning';
3331
        }
3332
        // used to select it with jquery
3333
        $class .= ' question-validate-btn';
3334
        $buttonList[] = Display::button(
3335
            'validate_all',
3336
            $all_label,
3337
            ['type' => 'button', 'class' => $class]
3338
        );
3339
        $buttonList[] = Display::span(null, ['id' => 'save_all_response']);
3340
        $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3341
3342
        return $html;
3343
    }
3344
3345
    /**
3346
     * @param int    $timeLeft in seconds
3347
     * @param string $url
3348
     *
3349
     * @return string
3350
     */
3351
    public function showSimpleTimeControl($timeLeft, $url = '')
3352
    {
3353
        $timeLeft = (int) $timeLeft;
3354
3355
        return "<script>
3356
            function openClockWarning() {
3357
                $('#clock_warning').dialog({
3358
                    modal:true,
3359
                    height:320,
3360
                    width:550,
3361
                    closeOnEscape: false,
3362
                    resizable: false,
3363
                    buttons: {
3364
                        '".addslashes(get_lang('Close'))."': function() {
3365
                            $('#clock_warning').dialog('close');
3366
                        }
3367
                    },
3368
                    close: function() {
3369
                        window.location.href = '$url';
3370
                    }
3371
                });
3372
                $('#clock_warning').dialog('open');
3373
                $('#counter_to_redirect').epiclock({
3374
                    mode: $.epiclock.modes.countdown,
3375
                    offset: {seconds: 5},
3376
                    format: 's'
3377
                }).bind('timer', function () {
3378
                    window.location.href = '$url';
3379
                });
3380
            }
3381
3382
            function onExpiredTimeExercise() {
3383
                $('#wrapper-clock').hide();
3384
                $('#expired-message-id').show();
3385
                // Fixes bug #5263
3386
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3387
                openClockWarning();
3388
            }
3389
3390
			$(function() {
3391
				// time in seconds when using minutes there are some seconds lost
3392
                var time_left = parseInt(".$timeLeft.");
3393
                $('#exercise_clock_warning').epiclock({
3394
                    mode: $.epiclock.modes.countdown,
3395
                    offset: {seconds: time_left},
3396
                    format: 'x:i:s',
3397
                    renderer: 'minute'
3398
                }).bind('timer', function () {
3399
                    onExpiredTimeExercise();
3400
                });
3401
	       		$('#submit_save').click(function () {});
3402
	        });
3403
	    </script>";
3404
    }
3405
3406
    /**
3407
     * So the time control will work.
3408
     *
3409
     * @param int    $timeLeft
3410
     * @param string $redirectToUrl
3411
     *
3412
     * @return string
3413
     */
3414
    public function showTimeControlJS($timeLeft, $redirectToUrl = '')
3415
    {
3416
        $timeLeft = (int) $timeLeft;
3417
        $script = 'redirectExerciseToResult();';
3418
        if (ALL_ON_ONE_PAGE == $this->type) {
3419
            $script = "save_now_all('validate');";
3420
        } elseif (ONE_PER_PAGE == $this->type) {
3421
            $script = 'window.quizTimeEnding = true;
3422
                $(\'[name="save_now"]\').trigger(\'click\');';
3423
        }
3424
3425
        $exerciseSubmitRedirect = '';
3426
        if (!empty($redirectToUrl)) {
3427
            $exerciseSubmitRedirect = "window.location = '$redirectToUrl'";
3428
        }
3429
3430
        return "<script>
3431
            function openClockWarning() {
3432
                $('#clock_warning').dialog({
3433
                    modal:true,
3434
                    height:320,
3435
                    width:550,
3436
                    closeOnEscape: false,
3437
                    resizable: false,
3438
                    buttons: {
3439
                        '".addslashes(get_lang('End test'))."': function() {
3440
                            $('#clock_warning').dialog('close');
3441
                        }
3442
                    },
3443
                    close: function() {
3444
                        send_form();
3445
                    }
3446
                });
3447
3448
                $('#clock_warning').dialog('open');
3449
                $('#counter_to_redirect').epiclock({
3450
                    mode: $.epiclock.modes.countdown,
3451
                    offset: {seconds: 5},
3452
                    format: 's'
3453
                }).bind('timer', function () {
3454
                    send_form();
3455
                });
3456
            }
3457
3458
            function send_form() {
3459
                if ($('#exercise_form').length) {
3460
                    $script
3461
                } else {
3462
                    $exerciseSubmitRedirect
3463
                    // In exercise_reminder.php
3464
                    final_submit();
3465
                }
3466
            }
3467
3468
            function onExpiredTimeExercise() {
3469
                $('#wrapper-clock').hide();
3470
                $('#expired-message-id').show();
3471
                // Fixes bug #5263
3472
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3473
                openClockWarning();
3474
            }
3475
3476
			$(function() {
3477
				// time in seconds when using minutes there are some seconds lost
3478
                var time_left = parseInt(".$timeLeft.");
3479
                $('#exercise_clock_warning').epiclock({
3480
                    mode: $.epiclock.modes.countdown,
3481
                    offset: {seconds: time_left},
3482
                    format: 'x:C:s',
3483
                    renderer: 'minute'
3484
                }).bind('timer', function () {
3485
                    onExpiredTimeExercise();
3486
                });
3487
	       		$('#submit_save').click(function () {});
3488
	        });
3489
	    </script>";
3490
    }
3491
3492
    /**
3493
     * This function was originally found in the exercise_show.php.
3494
     *
3495
     * @param int    $exeId
3496
     * @param int    $questionId
3497
     * @param mixed  $choice                                    the user-selected option
3498
     * @param string $from                                      function is called from 'exercise_show' or
3499
     *                                                          'exercise_result'
3500
     * @param array  $exerciseResultCoordinates                 the hotspot coordinates $hotspot[$question_id] =
3501
     *                                                          coordinates
3502
     * @param bool   $save_results                              save results in the DB or just show the response
3503
     * @param bool   $from_database                             gets information from DB or from the current selection
3504
     * @param bool   $show_result                               show results or not
3505
     * @param int    $propagate_neg
3506
     * @param array  $hotspot_delineation_result
3507
     * @param bool   $showTotalScoreAndUserChoicesInLastAttempt
3508
     * @param bool   $updateResults
3509
     * @param bool   $showHotSpotDelineationTable
3510
     * @param int    $questionDuration                          seconds
3511
     *
3512
     * @return string html code
3513
     *
3514
     * @todo    reduce parameters of this function
3515
     */
3516
    public function manage_answer(
3517
        $exeId,
3518
        $questionId,
3519
        $choice,
3520
        $from = 'exercise_show',
3521
        $exerciseResultCoordinates = [],
3522
        $save_results = true,
3523
        $from_database = false,
3524
        $show_result = true,
3525
        $propagate_neg = 0,
3526
        $hotspot_delineation_result = [],
3527
        $showTotalScoreAndUserChoicesInLastAttempt = true,
3528
        $updateResults = false,
3529
        $showHotSpotDelineationTable = false,
3530
        $questionDuration = 0
3531
    ) {
3532
        $debug = false;
3533
        //needed in order to use in the exercise_attempt() for the time
3534
        global $learnpath_id, $learnpath_item_id;
3535
        require_once api_get_path(LIBRARY_PATH).'geometry.lib.php';
3536
        $em = Database::getManager();
3537
        $feedback_type = $this->getFeedbackType();
3538
        $results_disabled = $this->selectResultsDisabled();
3539
        $questionDuration = (int) $questionDuration;
3540
3541
        if ($debug) {
3542
            error_log('<------ manage_answer ------> ');
3543
            error_log('exe_id: '.$exeId);
3544
            error_log('$from:  '.$from);
3545
            error_log('$save_results: '.(int) $save_results);
3546
            error_log('$from_database: '.(int) $from_database);
3547
            error_log('$show_result: '.(int) $show_result);
3548
            error_log('$propagate_neg: '.$propagate_neg);
3549
            error_log('$exerciseResultCoordinates: '.print_r($exerciseResultCoordinates, 1));
3550
            error_log('$hotspot_delineation_result: '.print_r($hotspot_delineation_result, 1));
3551
            error_log('$learnpath_id: '.$learnpath_id);
3552
            error_log('$learnpath_item_id: '.$learnpath_item_id);
3553
            error_log('$choice: '.print_r($choice, 1));
3554
            error_log('-----------------------------');
3555
        }
3556
3557
        $final_overlap = 0;
3558
        $final_missing = 0;
3559
        $final_excess = 0;
3560
        $overlap_color = 0;
3561
        $missing_color = 0;
3562
        $excess_color = 0;
3563
        $threadhold1 = 0;
3564
        $threadhold2 = 0;
3565
        $threadhold3 = 0;
3566
        $arrques = null;
3567
        $arrans = null;
3568
        $studentChoice = null;
3569
        $expectedAnswer = '';
3570
        $calculatedChoice = '';
3571
        $calculatedStatus = '';
3572
        $questionId = (int) $questionId;
3573
        $exeId = (int) $exeId;
3574
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3575
        $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
3576
        $studentChoiceDegree = null;
3577
3578
        // Creates a temporary Question object
3579
        $course_id = $this->course_id;
3580
        $objQuestionTmp = Question::read($questionId, $this->course);
3581
3582
        if (false === $objQuestionTmp) {
3583
            return false;
3584
        }
3585
3586
        $questionName = $objQuestionTmp->selectTitle();
3587
        $questionWeighting = $objQuestionTmp->selectWeighting();
3588
        $answerType = $objQuestionTmp->selectType();
3589
        $quesId = $objQuestionTmp->getId();
3590
        $extra = $objQuestionTmp->extra;
3591
        $next = 1; //not for now
3592
        $totalWeighting = 0;
3593
        $totalScore = 0;
3594
3595
        // Extra information of the question
3596
        if ((
3597
                MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
3598
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
3599
            )
3600
            && !empty($extra)
3601
        ) {
3602
            $extra = explode(':', $extra);
3603
            // Fixes problems with negatives values using intval
3604
            $true_score = (float) trim($extra[0]);
3605
            $false_score = (float) trim($extra[1]);
3606
            $doubt_score = (float) trim($extra[2]);
3607
        }
3608
3609
        // Construction of the Answer object
3610
        $objAnswerTmp = new Answer($questionId, $course_id);
3611
        $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
3612
3613
        if ($debug) {
3614
            error_log('Count of possible answers: '.$nbrAnswers);
3615
            error_log('$answerType: '.$answerType);
3616
        }
3617
3618
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
3619
            $choiceTmp = $choice;
3620
            $choice = isset($choiceTmp['choice']) ? $choiceTmp['choice'] : '';
3621
            $choiceDegreeCertainty = isset($choiceTmp['choiceDegreeCertainty']) ? $choiceTmp['choiceDegreeCertainty'] : '';
3622
        }
3623
3624
        if (FREE_ANSWER == $answerType ||
3625
            ORAL_EXPRESSION == $answerType ||
3626
            CALCULATED_ANSWER == $answerType ||
3627
            ANNOTATION == $answerType
3628
        ) {
3629
            $nbrAnswers = 1;
3630
        }
3631
3632
        $generatedFiles = [];
3633
        if ($answerType == ORAL_EXPRESSION) {
3634
            $generatedFiles = ExerciseLib::getOralFileAudio($exeId, $questionId, true);
3635
        }
3636
3637
3638
        $generatedFilesHtml = '';
3639
        if (!empty($generatedFiles)) {
3640
            foreach ($generatedFiles as $fileUrl) {
3641
                $generatedFilesHtml .= '<a href="'.$fileUrl.'">'.$fileUrl.'</a><br />';
3642
            }
3643
        }
3644
3645
        $user_answer = '';
3646
        // Get answer list for matching
3647
        $sql = "SELECT iid, answer
3648
                FROM $table_ans
3649
                WHERE question_id = $questionId";
3650
        $res_answer = Database::query($sql);
3651
3652
        $answerMatching = [];
3653
        while ($real_answer = Database::fetch_array($res_answer)) {
3654
            $answerMatching[$real_answer['iid']] = $real_answer['answer'];
3655
        }
3656
3657
        // Get first answer needed for global question, no matter the answer shuffle option;
3658
        $firstAnswer = [];
3659
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
3660
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
3661
        ) {
3662
            $sql = "SELECT *
3663
                    FROM $table_ans
3664
                    WHERE question_id = $questionId
3665
                    ORDER BY position
3666
                    LIMIT 1";
3667
            $result = Database::query($sql);
3668
            if (Database::num_rows($result)) {
3669
                $firstAnswer = Database::fetch_array($result);
3670
            }
3671
        }
3672
3673
        $real_answers = [];
3674
        $quiz_question_options = Question::readQuestionOption($questionId, $course_id);
3675
3676
        $organs_at_risk_hit = 0;
3677
        $questionScore = 0;
3678
        $orderedHotSpots = [];
3679
        if (HOT_SPOT == $answerType || ANNOTATION == $answerType) {
3680
            $orderedHotSpots = $em->getRepository(TrackEHotspot::class)->findBy(
3681
                [
3682
                    'hotspotQuestionId' => $questionId,
3683
                    'course' => $course_id,
3684
                    'hotspotExeId' => $exeId,
3685
                ],
3686
                ['hotspotAnswerId' => 'ASC']
3687
            );
3688
        }
3689
3690
        if ($debug) {
3691
            error_log('-- Start answer loop --');
3692
        }
3693
3694
        $answerDestination = null;
3695
        $userAnsweredQuestion = false;
3696
        $correctAnswerId = [];
3697
        for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
3698
            $answer = $objAnswerTmp->selectAnswer($answerId);
3699
            $answerComment = $objAnswerTmp->selectComment($answerId);
3700
            $answerCorrect = $objAnswerTmp->isCorrect($answerId);
3701
            $answerWeighting = (float) $objAnswerTmp->selectWeighting($answerId);
3702
            $answerAutoId = $objAnswerTmp->selectAutoId($answerId);
3703
            $answerIid = isset($objAnswerTmp->iid[$answerId]) ? (int) $objAnswerTmp->iid[$answerId] : 0;
3704
3705
            if ($debug) {
3706
                error_log("c_quiz_answer.id_auto: $answerAutoId ");
3707
                error_log("Answer marked as correct in db (0/1)?: $answerCorrect ");
3708
                error_log("answerWeighting: $answerWeighting");
3709
            }
3710
3711
            // Delineation
3712
            $delineation_cord = $objAnswerTmp->selectHotspotCoordinates(1);
3713
            $answer_delineation_destination = $objAnswerTmp->selectDestination(1);
3714
3715
            switch ($answerType) {
3716
                case UNIQUE_ANSWER:
3717
                case UNIQUE_ANSWER_IMAGE:
3718
                case UNIQUE_ANSWER_NO_OPTION:
3719
                case READING_COMPREHENSION:
3720
                    if ($from_database) {
3721
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3722
                                WHERE
3723
                                    exe_id = $exeId AND
3724
                                    question_id = $questionId";
3725
                        $result = Database::query($sql);
3726
                        $choice = Database::result($result, 0, 'answer');
3727
3728
                        if (false === $userAnsweredQuestion) {
3729
                            $userAnsweredQuestion = !empty($choice);
3730
                        }
3731
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3732
                        if ($studentChoice) {
3733
                            $questionScore += $answerWeighting;
3734
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3735
                            $correctAnswerId[] = $answerId;
3736
                        }
3737
                    } else {
3738
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3739
                        if ($studentChoice) {
3740
                            $questionScore += $answerWeighting;
3741
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3742
                            $correctAnswerId[] = $answerId;
3743
                        }
3744
                    }
3745
3746
                    break;
3747
                case MULTIPLE_ANSWER_TRUE_FALSE:
3748
                    if ($from_database) {
3749
                        $choice = [];
3750
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3751
                                WHERE
3752
                                    exe_id = $exeId AND
3753
                                    question_id = ".$questionId;
3754
3755
                        $result = Database::query($sql);
3756
                        while ($row = Database::fetch_array($result)) {
3757
                            $values = explode(':', $row['answer']);
3758
                            $my_answer_id = isset($values[0]) ? $values[0] : '';
3759
                            $option = isset($values[1]) ? $values[1] : '';
3760
                            $choice[$my_answer_id] = $option;
3761
                        }
3762
                        $userAnsweredQuestion = !empty($choice);
3763
                    }
3764
3765
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3766
                    if (!empty($studentChoice)) {
3767
                        $correctAnswerId[] = $answerAutoId;
3768
                        if ($studentChoice == $answerCorrect) {
3769
                            $questionScore += $true_score;
3770
                        } else {
3771
                            if (isset($quiz_question_options[$studentChoice])
3772
                                && in_array($quiz_question_options[$studentChoice]['name'], ["Don't know", 'DoubtScore'])
3773
                            ) {
3774
                                $questionScore += $doubt_score;
3775
                            } else {
3776
                                $questionScore += $false_score;
3777
                            }
3778
                        }
3779
                    } else {
3780
                        // If no result then the user just hit don't know
3781
                        $studentChoice = 3;
3782
                        $questionScore += $doubt_score;
3783
                    }
3784
                    $totalScore = $questionScore;
3785
3786
                    break;
3787
                case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
3788
                    if ($from_database) {
3789
                        $choice = [];
3790
                        $choiceDegreeCertainty = [];
3791
                        $sql = "SELECT answer
3792
                                FROM $TBL_TRACK_ATTEMPT
3793
                                WHERE exe_id = $exeId AND question_id = $questionId";
3794
3795
                        $result = Database::query($sql);
3796
                        while ($row = Database::fetch_array($result)) {
3797
                            $ind = $row['answer'];
3798
                            $values = explode(':', $ind);
3799
                            $myAnswerId = $values[0] ?? null;
3800
                            $option = $values[1] ?? null;
3801
                            $percent = $values[2] ?? null;
3802
                            $choice[$myAnswerId] = $option;
3803
                            $choiceDegreeCertainty[$myAnswerId] = $percent;
3804
                        }
3805
                    }
3806
3807
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3808
                    $studentChoiceDegree = isset($choiceDegreeCertainty[$answerAutoId]) ? $choiceDegreeCertainty[$answerAutoId] : null;
3809
3810
                    // student score update
3811
                    if (!empty($studentChoice)) {
3812
                        if ($studentChoice == $answerCorrect) {
3813
                            // correct answer and student is Unsure or PrettySur
3814
                            if (isset($quiz_question_options[$studentChoiceDegree]) &&
3815
                                $quiz_question_options[$studentChoiceDegree]['position'] >= 3 &&
3816
                                $quiz_question_options[$studentChoiceDegree]['position'] < 9
3817
                            ) {
3818
                                $questionScore += $true_score;
3819
                            } else {
3820
                                // student ignore correct answer
3821
                                $questionScore += $doubt_score;
3822
                            }
3823
                        } else {
3824
                            // false answer and student is Unsure or PrettySur
3825
                            if (isset($quiz_question_options[$studentChoiceDegree]) && $quiz_question_options[$studentChoiceDegree]['position'] >= 3
3826
                                && $quiz_question_options[$studentChoiceDegree]['position'] < 9) {
3827
                                $questionScore += $false_score;
3828
                            } else {
3829
                                // student ignore correct answer
3830
                                $questionScore += $doubt_score;
3831
                            }
3832
                        }
3833
                    }
3834
                    $totalScore = $questionScore;
3835
3836
                    break;
3837
                case MULTIPLE_ANSWER:
3838
                    if ($from_database) {
3839
                        $choice = [];
3840
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3841
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3842
                        $resultans = Database::query($sql);
3843
                        while ($row = Database::fetch_array($resultans)) {
3844
                            $choice[$row['answer']] = 1;
3845
                        }
3846
3847
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3848
                        $real_answers[$answerId] = (bool) $studentChoice;
3849
3850
                        if ($studentChoice) {
3851
                            $questionScore += $answerWeighting;
3852
                        }
3853
                    } else {
3854
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3855
                        $real_answers[$answerId] = (bool) $studentChoice;
3856
3857
                        if (isset($studentChoice)) {
3858
                            $correctAnswerId[] = $answerAutoId;
3859
                            $questionScore += $answerWeighting;
3860
                        }
3861
                    }
3862
                    $totalScore += $answerWeighting;
3863
3864
                    break;
3865
                case GLOBAL_MULTIPLE_ANSWER:
3866
                    if ($from_database) {
3867
                        $choice = [];
3868
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3869
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3870
                        $resultans = Database::query($sql);
3871
                        while ($row = Database::fetch_array($resultans)) {
3872
                            $choice[$row['answer']] = 1;
3873
                        }
3874
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3875
                        $real_answers[$answerId] = (bool) $studentChoice;
3876
                        if ($studentChoice) {
3877
                            $questionScore += $answerWeighting;
3878
                        }
3879
                    } else {
3880
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3881
                        if (isset($studentChoice)) {
3882
                            $questionScore += $answerWeighting;
3883
                        }
3884
                        $real_answers[$answerId] = (bool) $studentChoice;
3885
                    }
3886
                    $totalScore += $answerWeighting;
3887
                    if ($debug) {
3888
                        error_log("studentChoice: $studentChoice");
3889
                    }
3890
3891
                    break;
3892
                case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
3893
                    if ($from_database) {
3894
                        $choice = [];
3895
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3896
                                WHERE exe_id = $exeId AND question_id = $questionId";
3897
                        $resultans = Database::query($sql);
3898
                        while ($row = Database::fetch_array($resultans)) {
3899
                            $result = explode(':', $row['answer']);
3900
                            if (isset($result[0])) {
3901
                                $my_answer_id = isset($result[0]) ? $result[0] : '';
3902
                                $option = isset($result[1]) ? $result[1] : '';
3903
                                $choice[$my_answer_id] = $option;
3904
                            }
3905
                        }
3906
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3907
3908
                        $real_answers[$answerId] = false;
3909
                        if ($answerCorrect == $studentChoice) {
3910
                            $real_answers[$answerId] = true;
3911
                        }
3912
                    } else {
3913
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3914
                        $real_answers[$answerId] = false;
3915
                        if ($answerCorrect == $studentChoice) {
3916
                            $real_answers[$answerId] = true;
3917
                        }
3918
                    }
3919
3920
                    break;
3921
                case MULTIPLE_ANSWER_COMBINATION:
3922
                    if ($from_database) {
3923
                        $choice = [];
3924
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3925
                                WHERE exe_id = $exeId AND question_id = $questionId";
3926
                        $resultans = Database::query($sql);
3927
                        while ($row = Database::fetch_array($resultans)) {
3928
                            $choice[$row['answer']] = 1;
3929
                        }
3930
3931
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3932
                        if (1 == $answerCorrect) {
3933
                            $real_answers[$answerId] = false;
3934
                            if ($studentChoice) {
3935
                                $real_answers[$answerId] = true;
3936
                            }
3937
                        } else {
3938
                            $real_answers[$answerId] = true;
3939
                            if ($studentChoice) {
3940
                                $real_answers[$answerId] = false;
3941
                            }
3942
                        }
3943
                    } else {
3944
                        $studentChoice = $choice[$answerAutoId] ?? null;
3945
                        if (1 == $answerCorrect) {
3946
                            $real_answers[$answerId] = false;
3947
                            if ($studentChoice) {
3948
                                $real_answers[$answerId] = true;
3949
                            }
3950
                        } else {
3951
                            $real_answers[$answerId] = true;
3952
                            if ($studentChoice) {
3953
                                $real_answers[$answerId] = false;
3954
                            }
3955
                        }
3956
                    }
3957
3958
                    break;
3959
                case FILL_IN_BLANKS:
3960
                    $str = '';
3961
                    $answerFromDatabase = '';
3962
                    if ($from_database) {
3963
                        $sql = "SELECT answer
3964
                                FROM $TBL_TRACK_ATTEMPT
3965
                                WHERE
3966
                                    exe_id = $exeId AND
3967
                                    question_id= $questionId ";
3968
                        $result = Database::query($sql);
3969
                        $str = $answerFromDatabase = Database::result($result, 0, 'answer');
3970
                    }
3971
3972
                    // if ($saved_results == false && strpos($answerFromDatabase, 'font color') !== false) {
3973
                    if (false) {
3974
                        // the question is encoded like this
3975
                        // [A] B [C] D [E] F::10,10,10@1
3976
                        // number 1 before the "@" means that is a switchable fill in blank question
3977
                        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
3978
                        // means that is a normal fill blank question
3979
                        // first we explode the "::"
3980
                        $pre_array = explode('::', $answer);
3981
3982
                        // is switchable fill blank or not
3983
                        $last = count($pre_array) - 1;
3984
                        $is_set_switchable = explode('@', $pre_array[$last]);
3985
                        $switchable_answer_set = false;
3986
                        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
3987
                            $switchable_answer_set = true;
3988
                        }
3989
                        $answer = '';
3990
                        for ($k = 0; $k < $last; $k++) {
3991
                            $answer .= $pre_array[$k];
3992
                        }
3993
                        // splits weightings that are joined with a comma
3994
                        $answerWeighting = explode(',', $is_set_switchable[0]);
3995
                        // we save the answer because it will be modified
3996
                        $temp = $answer;
3997
                        $answer = '';
3998
                        $j = 0;
3999
                        //initialise answer tags
4000
                        $user_tags = $correct_tags = $real_text = [];
4001
                        // the loop will stop at the end of the text
4002
                        while (1) {
4003
                            // quits the loop if there are no more blanks (detect '[')
4004
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
4005
                                // adds the end of the text
4006
                                $answer = $temp;
4007
                                $real_text[] = $answer;
4008
4009
                                break; //no more "blanks", quit the loop
4010
                            }
4011
                            // adds the piece of text that is before the blank
4012
                            //and ends with '[' into a general storage array
4013
                            $real_text[] = api_substr($temp, 0, $pos + 1);
4014
                            $answer .= api_substr($temp, 0, $pos + 1);
4015
                            //take the string remaining (after the last "[" we found)
4016
                            $temp = api_substr($temp, $pos + 1);
4017
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4018
                            if (false === ($pos = api_strpos($temp, ']'))) {
4019
                                // adds the end of the text
4020
                                $answer .= $temp;
4021
4022
                                break;
4023
                            }
4024
                            if ($from_database) {
4025
                                $str = $answerFromDatabase;
4026
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4027
                                $str = str_replace('\r\n', '', $str);
4028
4029
                                $choice = $arr[1];
4030
                                if (isset($choice[$j])) {
4031
                                    $tmp = api_strrpos($choice[$j], ' / ');
4032
                                    $choice[$j] = api_substr($choice[$j], 0, $tmp);
4033
                                    $choice[$j] = trim($choice[$j]);
4034
                                    // Needed to let characters ' and " to work as part of an answer
4035
                                    $choice[$j] = stripslashes($choice[$j]);
4036
                                } else {
4037
                                    $choice[$j] = null;
4038
                                }
4039
                            } else {
4040
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4041
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4042
                            }
4043
4044
                            $user_tags[] = $choice[$j];
4045
                            // Put the contents of the [] answer tag into correct_tags[]
4046
                            $correct_tags[] = api_substr($temp, 0, $pos);
4047
                            $j++;
4048
                            $temp = api_substr($temp, $pos + 1);
4049
                        }
4050
                        $answer = '';
4051
                        $real_correct_tags = $correct_tags;
4052
                        $chosen_list = [];
4053
4054
                        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...
4055
                            if (0 == $i) {
4056
                                $answer .= $real_text[0];
4057
                            }
4058
                            if (!$switchable_answer_set) {
4059
                                // Needed to parse ' and " characters
4060
                                $user_tags[$i] = stripslashes($user_tags[$i]);
4061
                                if ($correct_tags[$i] == $user_tags[$i]) {
4062
                                    // gives the related weighting to the student
4063
                                    $questionScore += $answerWeighting[$i];
4064
                                    // increments total score
4065
                                    $totalScore += $answerWeighting[$i];
4066
                                    // adds the word in green at the end of the string
4067
                                    $answer .= $correct_tags[$i];
4068
                                } elseif (!empty($user_tags[$i])) {
4069
                                    // else if the word entered by the student IS NOT the same as
4070
                                    // the one defined by the professor
4071
                                    // adds the word in red at the end of the string, and strikes it
4072
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4073
                                } else {
4074
                                    // adds a tabulation if no word has been typed by the student
4075
                                    $answer .= ''; // remove &nbsp; that causes issue
4076
                                }
4077
                            } else {
4078
                                // switchable fill in the blanks
4079
                                if (in_array($user_tags[$i], $correct_tags)) {
4080
                                    $chosen_list[] = $user_tags[$i];
4081
                                    $correct_tags = array_diff($correct_tags, $chosen_list);
4082
                                    // gives the related weighting to the student
4083
                                    $questionScore += $answerWeighting[$i];
4084
                                    // increments total score
4085
                                    $totalScore += $answerWeighting[$i];
4086
                                    // adds the word in green at the end of the string
4087
                                    $answer .= $user_tags[$i];
4088
                                } elseif (!empty($user_tags[$i])) {
4089
                                    // else if the word entered by the student IS NOT the same
4090
                                    // as the one defined by the professor
4091
                                    // adds the word in red at the end of the string, and strikes it
4092
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4093
                                } else {
4094
                                    // adds a tabulation if no word has been typed by the student
4095
                                    $answer .= ''; // remove &nbsp; that causes issue
4096
                                }
4097
                            }
4098
4099
                            // adds the correct word, followed by ] to close the blank
4100
                            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4101
                            if (isset($real_text[$i + 1])) {
4102
                                $answer .= $real_text[$i + 1];
4103
                            }
4104
                        }
4105
                    } else {
4106
                        // insert the student result in the track_e_attempt table, field answer
4107
                        // $answer is the answer like in the c_quiz_answer table for the question
4108
                        // student data are choice[]
4109
                        $listCorrectAnswers = FillBlanks::getAnswerInfo($answer);
4110
                        $switchableAnswerSet = $listCorrectAnswers['switchable'];
4111
                        $answerWeighting = $listCorrectAnswers['weighting'];
4112
                        // user choices is an array $choice
4113
4114
                        // get existing user data in n the BDD
4115
                        if ($from_database) {
4116
                            $listStudentResults = FillBlanks::getAnswerInfo(
4117
                                $answerFromDatabase,
4118
                                true
4119
                            );
4120
                            $choice = $listStudentResults['student_answer'];
4121
                        }
4122
4123
                        // loop other all blanks words
4124
                        if (!$switchableAnswerSet) {
4125
                            // not switchable answer, must be in the same place than teacher order
4126
                            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...
4127
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
4128
                                $correctAnswer = $listCorrectAnswers['words'][$i];
4129
4130
                                if ($debug) {
4131
                                    error_log("Student answer: $i");
4132
                                    error_log($studentAnswer);
4133
                                }
4134
4135
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4136
                                // Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
4137
                                // ENT_QUOTES is used in order to transform ' to &#039;
4138
                                if (!$from_database) {
4139
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4140
                                    if ($debug) {
4141
                                        error_log('Student answer cleaned:');
4142
                                        error_log($studentAnswer);
4143
                                    }
4144
                                }
4145
4146
                                $isAnswerCorrect = 0;
4147
                                if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
4148
                                    // gives the related weighting to the student
4149
                                    $questionScore += $answerWeighting[$i];
4150
                                    // increments total score
4151
                                    $totalScore += $answerWeighting[$i];
4152
                                    $isAnswerCorrect = 1;
4153
                                }
4154
                                if ($debug) {
4155
                                    error_log("isAnswerCorrect $i: $isAnswerCorrect");
4156
                                }
4157
4158
                                $studentAnswerToShow = $studentAnswer;
4159
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4160
                                if ($debug) {
4161
                                    error_log("Fill in blank type: $type");
4162
                                }
4163
                                if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4164
                                    $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4165
                                    if ('' != $studentAnswer) {
4166
                                        foreach ($listMenu as $item) {
4167
                                            if (sha1($item) == $studentAnswer) {
4168
                                                $studentAnswerToShow = $item;
4169
                                            }
4170
                                        }
4171
                                    }
4172
                                }
4173
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4174
                                $listCorrectAnswers['student_score'][$i] = $isAnswerCorrect;
4175
                            }
4176
                        } else {
4177
                            // switchable answer
4178
                            $listStudentAnswerTemp = $choice;
4179
                            $listTeacherAnswerTemp = $listCorrectAnswers['words'];
4180
4181
                            // for every teacher answer, check if there is a student answer
4182
                            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...
4183
                                $studentAnswer = trim($listStudentAnswerTemp[$i]);
4184
                                $studentAnswerToShow = $studentAnswer;
4185
4186
                                if (empty($studentAnswer)) {
4187
                                    break;
4188
                                }
4189
4190
                                if ($debug) {
4191
                                    error_log("Student answer: $i");
4192
                                    error_log($studentAnswer);
4193
                                }
4194
4195
                                if (!$from_database) {
4196
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4197
                                    if ($debug) {
4198
                                        error_log("Student answer cleaned:");
4199
                                        error_log($studentAnswer);
4200
                                    }
4201
                                }
4202
4203
                                $found = false;
4204
                                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...
4205
                                    $correctAnswer = $listTeacherAnswerTemp[$j];
4206
4207
                                    if (!$found) {
4208
                                        if (FillBlanks::isStudentAnswerGood(
4209
                                            $studentAnswer,
4210
                                            $correctAnswer,
4211
                                            $from_database
4212
                                        )) {
4213
                                            $questionScore += $answerWeighting[$i];
4214
                                            $totalScore += $answerWeighting[$i];
4215
                                            $listTeacherAnswerTemp[$j] = '';
4216
                                            $found = true;
4217
                                        }
4218
                                    }
4219
4220
                                    $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4221
                                    if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4222
                                        $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4223
                                        if (!empty($studentAnswer)) {
4224
                                            foreach ($listMenu as $key => $item) {
4225
                                                if ($key == $correctAnswer) {
4226
                                                    $studentAnswerToShow = $item;
4227
                                                    break;
4228
                                                }
4229
                                            }
4230
                                        }
4231
                                    }
4232
                                }
4233
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4234
                                $listCorrectAnswers['student_score'][$i] = $found ? 1 : 0;
4235
                            }
4236
                        }
4237
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
4238
                    }
4239
4240
                    break;
4241
                case CALCULATED_ANSWER:
4242
                    $calculatedAnswerList = Session::read('calculatedAnswerId');
4243
                    if (!empty($calculatedAnswerList)) {
4244
                        $answer = $objAnswerTmp->selectAnswer($calculatedAnswerList[$questionId]);
4245
                        $preArray = explode('@@', $answer);
4246
                        $last = count($preArray) - 1;
4247
                        $answer = '';
4248
                        for ($k = 0; $k < $last; $k++) {
4249
                            $answer .= $preArray[$k];
4250
                        }
4251
                        $answerWeighting = [$answerWeighting];
4252
                        // we save the answer because it will be modified
4253
                        $temp = $answer;
4254
                        $answer = '';
4255
                        $j = 0;
4256
                        // initialise answer tags
4257
                        $userTags = $correctTags = $realText = [];
4258
                        // the loop will stop at the end of the text
4259
                        while (1) {
4260
                            // quits the loop if there are no more blanks (detect '[')
4261
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
4262
                                // adds the end of the text
4263
                                $answer = $temp;
4264
                                $realText[] = $answer;
4265
4266
                                break; //no more "blanks", quit the loop
4267
                            }
4268
                            // adds the piece of text that is before the blank
4269
                            // and ends with '[' into a general storage array
4270
                            $realText[] = api_substr($temp, 0, $pos + 1);
4271
                            $answer .= api_substr($temp, 0, $pos + 1);
4272
                            // take the string remaining (after the last "[" we found)
4273
                            $temp = api_substr($temp, $pos + 1);
4274
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4275
                            if (false === ($pos = api_strpos($temp, ']'))) {
4276
                                // adds the end of the text
4277
                                $answer .= $temp;
4278
4279
                                break;
4280
                            }
4281
4282
                            if ($from_database) {
4283
                                $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4284
                                        WHERE
4285
                                            exe_id = $exeId AND
4286
                                            question_id = $questionId ";
4287
                                $result = Database::query($sql);
4288
                                $str = Database::result($result, 0, 'answer');
4289
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4290
                                $str = str_replace('\r\n', '', $str);
4291
                                $choice = $arr[1];
4292
                                if (isset($choice[$j])) {
4293
                                    $tmp = api_strrpos($choice[$j], ' / ');
4294
                                    if ($tmp) {
4295
                                        $choice[$j] = api_substr($choice[$j], 0, $tmp);
4296
                                    } else {
4297
                                        $tmp = ltrim($tmp, '[');
4298
                                        $tmp = rtrim($tmp, ']');
4299
                                    }
4300
                                    $choice[$j] = trim($choice[$j]);
4301
                                    // Needed to let characters ' and " to work as part of an answer
4302
                                    $choice[$j] = stripslashes($choice[$j]);
4303
                                } else {
4304
                                    $choice[$j] = null;
4305
                                }
4306
                            } else {
4307
                                // This value is the user input not escaped while correct answer is escaped by ckeditor
4308
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4309
                            }
4310
                            $userTags[] = $choice[$j];
4311
                            // put the contents of the [] answer tag into correct_tags[]
4312
                            $correctTags[] = api_substr($temp, 0, $pos);
4313
                            $j++;
4314
                            $temp = api_substr($temp, $pos + 1);
4315
                        }
4316
                        $answer = '';
4317
                        $realCorrectTags = $correctTags;
4318
                        $calculatedStatus = Display::label(get_lang('Incorrect'), 'danger');
4319
                        $expectedAnswer = '';
4320
                        $calculatedChoice = '';
4321
4322
                        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...
4323
                            if (0 == $i) {
4324
                                $answer .= $realText[0];
4325
                            }
4326
                            // Needed to parse ' and " characters
4327
                            $userTags[$i] = stripslashes($userTags[$i]);
4328
                            if ($correctTags[$i] == $userTags[$i]) {
4329
                                // gives the related weighting to the student
4330
                                $questionScore += $answerWeighting[$i];
4331
                                // increments total score
4332
                                $totalScore += $answerWeighting[$i];
4333
                                // adds the word in green at the end of the string
4334
                                $answer .= $correctTags[$i];
4335
                                $calculatedChoice = $correctTags[$i];
4336
                            } elseif (!empty($userTags[$i])) {
4337
                                // else if the word entered by the student IS NOT the same as
4338
                                // the one defined by the professor
4339
                                // adds the word in red at the end of the string, and strikes it
4340
                                $answer .= '<font color="red"><s>'.$userTags[$i].'</s></font>';
4341
                                $calculatedChoice = $userTags[$i];
4342
                            } else {
4343
                                // adds a tabulation if no word has been typed by the student
4344
                                $answer .= ''; // remove &nbsp; that causes issue
4345
                            }
4346
                            // adds the correct word, followed by ] to close the blank
4347
                            if (EXERCISE_FEEDBACK_TYPE_EXAM != $this->results_disabled) {
4348
                                $answer .= ' / <font color="green"><b>'.$realCorrectTags[$i].'</b></font>';
4349
                                $calculatedStatus = Display::label(get_lang('Correct'), 'success');
4350
                                $expectedAnswer = $realCorrectTags[$i];
4351
                            }
4352
                            $answer .= ']';
4353
                            if (isset($realText[$i + 1])) {
4354
                                $answer .= $realText[$i + 1];
4355
                            }
4356
                        }
4357
                    } else {
4358
                        if ($from_database) {
4359
                            $sql = "SELECT *
4360
                                    FROM $TBL_TRACK_ATTEMPT
4361
                                    WHERE
4362
                                        exe_id = $exeId AND
4363
                                        question_id = $questionId ";
4364
                            $result = Database::query($sql);
4365
                            $resultData = Database::fetch_assoc($result);
4366
                            $answer = $resultData['answer'];
4367
                            $questionScore = $resultData['marks'];
4368
                        }
4369
                    }
4370
4371
                    break;
4372
                case FREE_ANSWER:
4373
                    if ($from_database) {
4374
                        $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
4375
                                 WHERE
4376
                                    exe_id = $exeId AND
4377
                                    question_id= ".$questionId;
4378
                        $result = Database::query($sql);
4379
                        $data = Database::fetch_array($result);
4380
                        $choice = '';
4381
                        $questionScore = 0;
4382
                        if ($data) {
4383
                            $choice = $data['answer'];
4384
                            $questionScore = $data['marks'];
4385
                        }
4386
4387
                        $choice = str_replace('\r\n', '', $choice);
4388
                        $choice = stripslashes($choice);
4389
4390
                        if (-1 == $questionScore) {
4391
                            $totalScore += 0;
4392
                        } else {
4393
                            $totalScore += $questionScore;
4394
                        }
4395
                        if ('' == $questionScore) {
4396
                            $questionScore = 0;
4397
                        }
4398
                        $arrques = $questionName;
4399
                        $arrans = $choice;
4400
                    } else {
4401
                        $studentChoice = $choice;
4402
                        if ($studentChoice) {
4403
                            //Fixing negative puntation see #2193
4404
                            $questionScore = 0;
4405
                            $totalScore += 0;
4406
                        }
4407
                    }
4408
4409
                    break;
4410
                case ORAL_EXPRESSION:
4411
                    if ($from_database) {
4412
                        $query = "SELECT answer, marks
4413
                                  FROM $TBL_TRACK_ATTEMPT
4414
                                  WHERE
4415
                                        exe_id = $exeId AND
4416
                                        question_id = $questionId
4417
                                 ";
4418
                        $resq = Database::query($query);
4419
                        $row = Database::fetch_assoc($resq);
4420
                        $choice = '';
4421
                        $questionScore = 0;
4422
4423
                        if (is_array($row)) {
4424
                            $choice = $row['answer'];
4425
                            $choice = str_replace('\r\n', '', $choice);
4426
                            $choice = stripslashes($choice);
4427
                            $questionScore = $row['marks'];
4428
                        }
4429
4430
                        if (-1 == $questionScore) {
4431
                            $totalScore += 0;
4432
                        } else {
4433
                            $totalScore += $questionScore;
4434
                        }
4435
                        $arrques = $questionName;
4436
                        $arrans = $choice;
4437
                    } else {
4438
                        $studentChoice = $choice;
4439
                        if ($studentChoice) {
4440
                            //Fixing negative puntation see #2193
4441
                            $questionScore = 0;
4442
                            $totalScore += 0;
4443
                        }
4444
                    }
4445
4446
                    break;
4447
                case DRAGGABLE:
4448
                case MATCHING_DRAGGABLE:
4449
                case MATCHING:
4450
                    if ($from_database) {
4451
                        $sql = "SELECT iid, answer
4452
                                FROM $table_ans
4453
                                WHERE
4454
                                    question_id = $questionId AND
4455
                                    correct = 0
4456
                                ";
4457
                        $result = Database::query($sql);
4458
                        // Getting the real answer
4459
                        $real_list = [];
4460
                        while ($realAnswer = Database::fetch_array($result)) {
4461
                            $real_list[$realAnswer['iid']] = $realAnswer['answer'];
4462
                        }
4463
4464
                        $orderBy = ' ORDER BY iid ';
4465
                        if (DRAGGABLE == $answerType) {
4466
                            $orderBy = ' ORDER BY correct ';
4467
                        }
4468
4469
                        $sql = "SELECT iid, answer, correct, ponderation
4470
                                FROM $table_ans
4471
                                WHERE
4472
                                    question_id = $questionId AND
4473
                                    correct <> 0
4474
                                $orderBy";
4475
                        $result = Database::query($sql);
4476
                        $options = [];
4477
                        $correctAnswers = [];
4478
                        while ($row = Database::fetch_assoc($result)) {
4479
                            $options[] = $row;
4480
                            $correctAnswers[$row['correct']] = $row['answer'];
4481
                        }
4482
4483
                        $questionScore = 0;
4484
                        $counterAnswer = 1;
4485
                        foreach ($options as $a_answers) {
4486
                            $i_answer_id = $a_answers['iid']; //3
4487
                            $s_answer_label = $a_answers['answer']; // your daddy - your mother
4488
                            $i_answer_correct_answer = $a_answers['correct']; //1 - 2
4489
                            $i_answer_id_auto = $a_answers['iid']; // 3 - 4
4490
4491
                            $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4492
                                    WHERE
4493
                                        exe_id = '$exeId' AND
4494
                                        question_id = '$questionId' AND
4495
                                        position = '$i_answer_id_auto'";
4496
                            $result = Database::query($sql);
4497
                            $s_user_answer = 0;
4498
                            if (Database::num_rows($result) > 0) {
4499
                                //  rich - good looking
4500
                                $s_user_answer = Database::result($result, 0, 0);
4501
                            }
4502
                            $i_answerWeighting = $a_answers['ponderation'];
4503
                            $user_answer = '';
4504
                            $status = Display::label(get_lang('Incorrect'), 'danger');
4505
4506
                            if (!empty($s_user_answer)) {
4507
                                if (DRAGGABLE == $answerType) {
4508
                                    if ($s_user_answer == $i_answer_correct_answer) {
4509
                                        $questionScore += $i_answerWeighting;
4510
                                        $totalScore += $i_answerWeighting;
4511
                                        $user_answer = Display::label(get_lang('Correct'), 'success');
4512
                                        if ($this->showExpectedChoice() && !empty($i_answer_id_auto)) {
4513
                                            $user_answer = $answerMatching[$i_answer_id_auto];
4514
                                        }
4515
                                        $status = Display::label(get_lang('Correct'), 'success');
4516
                                    } else {
4517
                                        $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4518
                                        if ($this->showExpectedChoice() && !empty($s_user_answer)) {
4519
                                            /*$data = $options[$real_list[$s_user_answer] - 1];
4520
                                            $user_answer = $data['answer'];*/
4521
                                            $user_answer = $correctAnswers[$s_user_answer] ?? '';
4522
                                        }
4523
                                    }
4524
                                } else {
4525
                                    if ($s_user_answer == $i_answer_correct_answer) {
4526
                                        $questionScore += $i_answerWeighting;
4527
                                        $totalScore += $i_answerWeighting;
4528
                                        $status = Display::label(get_lang('Correct'), 'success');
4529
4530
                                        // Try with id
4531
                                        if (isset($real_list[$i_answer_id])) {
4532
                                            $user_answer = Display::span(
4533
                                                $real_list[$i_answer_id],
4534
                                                ['style' => 'color: #008000; font-weight: bold;']
4535
                                            );
4536
                                        }
4537
4538
                                        // Try with $i_answer_id_auto
4539
                                        if (empty($user_answer)) {
4540
                                            if (isset($real_list[$i_answer_id_auto])) {
4541
                                                $user_answer = Display::span(
4542
                                                    $real_list[$i_answer_id_auto],
4543
                                                    ['style' => 'color: #008000; font-weight: bold;']
4544
                                                );
4545
                                            }
4546
                                        }
4547
4548
                                        if (isset($real_list[$i_answer_correct_answer])) {
4549
                                            $user_answer = Display::span(
4550
                                                $real_list[$i_answer_correct_answer],
4551
                                                ['style' => 'color: #008000; font-weight: bold;']
4552
                                            );
4553
                                        }
4554
                                    } else {
4555
                                        $user_answer = Display::span(
4556
                                            $real_list[$s_user_answer],
4557
                                            ['style' => 'color: #FF0000; text-decoration: line-through;']
4558
                                        );
4559
                                        if ($this->showExpectedChoice()) {
4560
                                            if (isset($real_list[$s_user_answer])) {
4561
                                                $user_answer = Display::span($real_list[$s_user_answer]);
4562
                                            }
4563
                                        }
4564
                                    }
4565
                                }
4566
                            } elseif (DRAGGABLE == $answerType) {
4567
                                $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4568
                                if ($this->showExpectedChoice()) {
4569
                                    $user_answer = '';
4570
                                }
4571
                            } else {
4572
                                $user_answer = Display::span(
4573
                                    get_lang('Incorrect').' &nbsp;',
4574
                                    ['style' => 'color: #FF0000; text-decoration: line-through;']
4575
                                );
4576
                                if ($this->showExpectedChoice()) {
4577
                                    $user_answer = '';
4578
                                }
4579
                            }
4580
4581
                            if ($show_result) {
4582
                                if (false === $this->showExpectedChoice() &&
4583
                                    false === $showTotalScoreAndUserChoicesInLastAttempt
4584
                                ) {
4585
                                    $user_answer = '';
4586
                                }
4587
                                switch ($answerType) {
4588
                                    case MATCHING:
4589
                                    case MATCHING_DRAGGABLE:
4590
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4591
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4592
                                                break;
4593
                                            }
4594
                                        }
4595
                                        echo '<tr>';
4596
                                        if (!in_array(
4597
                                            $this->results_disabled,
4598
                                            [
4599
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4600
                                            ]
4601
                                        )
4602
                                        ) {
4603
                                            echo '<td>'.$s_answer_label.'</td>';
4604
                                            echo '<td>'.$user_answer.'</td>';
4605
                                        } else {
4606
                                            echo '<td>'.$s_answer_label.'</td>';
4607
                                            $status = Display::label(get_lang('Correct'), 'success');
4608
                                        }
4609
4610
                                        if ($this->showExpectedChoice()) {
4611
                                            if ($this->showExpectedChoiceColumn()) {
4612
                                                echo '<td>';
4613
                                                if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4614
                                                    if (isset($real_list[$i_answer_correct_answer]) &&
4615
                                                        true == $showTotalScoreAndUserChoicesInLastAttempt
4616
                                                    ) {
4617
                                                        echo Display::span(
4618
                                                            $real_list[$i_answer_correct_answer]
4619
                                                        );
4620
                                                    }
4621
                                                }
4622
                                                echo '</td>';
4623
                                            }
4624
                                            echo '<td class="text-center">'.$status.'</td>';
4625
                                        } else {
4626
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4627
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4628
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4629
                                                ) {
4630
                                                    if ($this->showExpectedChoiceColumn()) {
4631
                                                        echo '<td>';
4632
                                                        echo Display::span(
4633
                                                            $real_list[$i_answer_correct_answer],
4634
                                                            ['style' => 'color: #008000; font-weight: bold;']
4635
                                                        );
4636
                                                        echo '</td>';
4637
                                                    }
4638
                                                }
4639
                                            }
4640
                                        }
4641
                                        echo '</tr>';
4642
4643
                                        break;
4644
                                    case DRAGGABLE:
4645
                                        if (false == $showTotalScoreAndUserChoicesInLastAttempt) {
4646
                                            $s_answer_label = '';
4647
                                        }
4648
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4649
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4650
                                                break;
4651
                                            }
4652
                                        }
4653
                                        echo '<tr>';
4654
                                        if ($this->showExpectedChoice()) {
4655
                                            if (!in_array(
4656
                                                $this->results_disabled,
4657
                                                [
4658
                                                    RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4659
                                                    //RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4660
                                                ]
4661
                                            )
4662
                                            ) {
4663
                                                echo '<td>'.$user_answer.'</td>';
4664
                                            } else {
4665
                                                $status = Display::label(get_lang('Correct'), 'success');
4666
                                            }
4667
                                            echo '<td>'.$s_answer_label.'</td>';
4668
                                            echo '<td class="text-center">'.$status.'</td>';
4669
                                        } else {
4670
                                            echo '<td>'.$s_answer_label.'</td>';
4671
                                            echo '<td>'.$user_answer.'</td>';
4672
                                            echo '<td>';
4673
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4674
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4675
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4676
                                                ) {
4677
                                                    echo Display::span(
4678
                                                        $real_list[$i_answer_correct_answer],
4679
                                                        ['style' => 'color: #008000; font-weight: bold;']
4680
                                                    );
4681
                                                }
4682
                                            }
4683
                                            echo '</td>';
4684
                                        }
4685
                                        echo '</tr>';
4686
4687
                                        break;
4688
                                }
4689
                            }
4690
                            $counterAnswer++;
4691
                        }
4692
4693
                        break 2; // break the switch and the "for" condition
4694
                    } else {
4695
                        if ($answerCorrect) {
4696
                            if (isset($choice[$answerAutoId]) &&
4697
                                $answerCorrect == $choice[$answerAutoId]
4698
                            ) {
4699
                                $correctAnswerId[] = $answerAutoId;
4700
                                $questionScore += $answerWeighting;
4701
                                $totalScore += $answerWeighting;
4702
                                $user_answer = Display::span($answerMatching[$choice[$answerAutoId]]);
4703
                            } else {
4704
                                if (isset($answerMatching[$choice[$answerAutoId]])) {
4705
                                    $user_answer = Display::span(
4706
                                        $answerMatching[$choice[$answerAutoId]],
4707
                                        ['style' => 'color: #FF0000; text-decoration: line-through;']
4708
                                    );
4709
                                }
4710
                            }
4711
                            $matching[$answerAutoId] = $choice[$answerAutoId];
4712
                        }
4713
                    }
4714
4715
                    break;
4716
                case HOT_SPOT:
4717
                    if ($from_database) {
4718
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4719
                        // Check auto id
4720
                        $foundAnswerId = $answerAutoId;
4721
                        $sql = "SELECT hotspot_correct
4722
                                FROM $TBL_TRACK_HOTSPOT
4723
                                WHERE
4724
                                    hotspot_exe_id = $exeId AND
4725
                                    hotspot_question_id= $questionId AND
4726
                                    hotspot_answer_id = $answerAutoId
4727
                                ORDER BY hotspot_id ASC";
4728
                        $result = Database::query($sql);
4729
                        if (Database::num_rows($result)) {
4730
                            $studentChoice = Database::result(
4731
                                $result,
4732
                                0,
4733
                                'hotspot_correct'
4734
                            );
4735
4736
                            if ($studentChoice) {
4737
                                $questionScore += $answerWeighting;
4738
                                $totalScore += $answerWeighting;
4739
                            }
4740
                        } else {
4741
                            // If answer.id is different:
4742
                            $sql = "SELECT hotspot_correct
4743
                                FROM $TBL_TRACK_HOTSPOT
4744
                                WHERE
4745
                                    hotspot_exe_id = $exeId AND
4746
                                    hotspot_question_id= $questionId AND
4747
                                    hotspot_answer_id = ".(int) $answerId.'
4748
                                ORDER BY hotspot_id ASC';
4749
                            $result = Database::query($sql);
4750
4751
                            $foundAnswerId = $answerId;
4752
                            if (Database::num_rows($result)) {
4753
                                $studentChoice = Database::result(
4754
                                    $result,
4755
                                    0,
4756
                                    'hotspot_correct'
4757
                                );
4758
4759
                                if ($studentChoice) {
4760
                                    $questionScore += $answerWeighting;
4761
                                    $totalScore += $answerWeighting;
4762
                                }
4763
                            } else {
4764
                                // check answer.iid
4765
                                if (!empty($answerIid)) {
4766
                                    $sql = "SELECT hotspot_correct
4767
                                            FROM $TBL_TRACK_HOTSPOT
4768
                                            WHERE
4769
                                                hotspot_exe_id = $exeId AND
4770
                                                hotspot_question_id= $questionId AND
4771
                                                hotspot_answer_id = $answerIid
4772
                                            ORDER BY hotspot_id ASC";
4773
                                    $result = Database::query($sql);
4774
4775
                                    $foundAnswerId = $answerIid;
4776
                                    $studentChoice = Database::result(
4777
                                        $result,
4778
                                        0,
4779
                                        'hotspot_correct'
4780
                                    );
4781
4782
                                    if ($studentChoice) {
4783
                                        $questionScore += $answerWeighting;
4784
                                        $totalScore += $answerWeighting;
4785
                                    }
4786
                                }
4787
                            }
4788
                        }
4789
                    } else {
4790
                        if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
4791
                            $choice[$answerAutoId] = 0;
4792
                            $choice[$answerIid] = 0;
4793
                        } else {
4794
                            $studentChoice = $choice[$answerAutoId];
4795
                            if (empty($studentChoice)) {
4796
                                $studentChoice = $choice[$answerIid];
4797
                            }
4798
                            $choiceIsValid = false;
4799
                            if (!empty($studentChoice)) {
4800
                                $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
4801
                                $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
4802
                                $choicePoint = Geometry::decodePoint($studentChoice);
4803
4804
                                switch ($hotspotType) {
4805
                                    case 'square':
4806
                                        $hotspotProperties = Geometry::decodeSquare($hotspotCoordinates);
4807
                                        $choiceIsValid = Geometry::pointIsInSquare($hotspotProperties, $choicePoint);
4808
4809
                                        break;
4810
                                    case 'circle':
4811
                                        $hotspotProperties = Geometry::decodeEllipse($hotspotCoordinates);
4812
                                        $choiceIsValid = Geometry::pointIsInEllipse($hotspotProperties, $choicePoint);
4813
4814
                                        break;
4815
                                    case 'poly':
4816
                                        $hotspotProperties = Geometry::decodePolygon($hotspotCoordinates);
4817
                                        $choiceIsValid = Geometry::pointIsInPolygon($hotspotProperties, $choicePoint);
4818
4819
                                        break;
4820
                                }
4821
                            }
4822
4823
                            $choice[$answerAutoId] = 0;
4824
                            if ($choiceIsValid) {
4825
                                $questionScore += $answerWeighting;
4826
                                $totalScore += $answerWeighting;
4827
                                $choice[$answerAutoId] = 1;
4828
                                $choice[$answerIid] = 1;
4829
                            }
4830
                        }
4831
                    }
4832
4833
                    break;
4834
                case HOT_SPOT_ORDER:
4835
                    // @todo never added to chamilo
4836
                    // for hotspot with fixed order
4837
                    $studentChoice = $choice['order'][$answerId];
4838
                    if ($studentChoice == $answerId) {
4839
                        $questionScore += $answerWeighting;
4840
                        $totalScore += $answerWeighting;
4841
                        $studentChoice = true;
4842
                    } else {
4843
                        $studentChoice = false;
4844
                    }
4845
4846
                    break;
4847
                case HOT_SPOT_DELINEATION:
4848
                    // for hotspot with delineation
4849
                    if ($from_database) {
4850
                        // getting the user answer
4851
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4852
                        $query = "SELECT hotspot_correct, hotspot_coordinate
4853
                                    FROM $TBL_TRACK_HOTSPOT
4854
                                    WHERE
4855
                                        hotspot_exe_id = $exeId AND
4856
                                        hotspot_question_id= $questionId AND
4857
                                        hotspot_answer_id = '1'";
4858
                        // By default we take 1 because it's a delineation
4859
                        $resq = Database::query($query);
4860
                        $row = Database::fetch_assoc($resq);
4861
4862
                        $choice = $row['hotspot_correct'];
4863
                        $user_answer = $row['hotspot_coordinate'];
4864
4865
                        // THIS is very important otherwise the poly_compile will throw an error!!
4866
                        // round-up the coordinates
4867
                        $coords = explode('/', $user_answer);
4868
                        $coords = array_filter($coords);
4869
                        $user_array = '';
4870
                        foreach ($coords as $coord) {
4871
                            [$x, $y] = explode(';', $coord);
4872
                            $user_array .= round($x).';'.round($y).'/';
4873
                        }
4874
                        $user_array = substr($user_array, 0, -1) ?: '';
4875
                    } else {
4876
                        if (!empty($studentChoice)) {
4877
                            $correctAnswerId[] = $answerAutoId;
4878
                            $newquestionList[] = $questionId;
4879
                        }
4880
4881
                        if (1 === $answerId) {
4882
                            $studentChoice = $choice[$answerId];
4883
                            $questionScore += $answerWeighting;
4884
                        }
4885
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
4886
                            $user_array = $_SESSION['exerciseResultCoordinates'][$questionId];
4887
                        }
4888
                    }
4889
                    $_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
4890
                    $_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
4891
4892
                    break;
4893
                case ANNOTATION:
4894
                    if ($from_database) {
4895
                        $sql = "SELECT answer, marks
4896
                                FROM $TBL_TRACK_ATTEMPT
4897
                                WHERE
4898
                                  exe_id = $exeId AND
4899
                                  question_id = $questionId ";
4900
                        $resq = Database::query($sql);
4901
                        $data = Database::fetch_array($resq);
4902
4903
                        $questionScore = empty($data['marks']) ? 0 : $data['marks'];
4904
                        $arrques = $questionName;
4905
4906
                        break;
4907
                    }
4908
                    $studentChoice = $choice;
4909
                    if ($studentChoice) {
4910
                        $questionScore = 0;
4911
                    }
4912
4913
                    break;
4914
            }
4915
4916
            if ($show_result) {
4917
                if ('exercise_result' === $from) {
4918
                    // Display answers (if not matching type, or if the answer is correct)
4919
                    if (!in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE]) ||
4920
                        $answerCorrect
4921
                    ) {
4922
                        if (in_array(
4923
                            $answerType,
4924
                            [
4925
                                UNIQUE_ANSWER,
4926
                                UNIQUE_ANSWER_IMAGE,
4927
                                UNIQUE_ANSWER_NO_OPTION,
4928
                                MULTIPLE_ANSWER,
4929
                                MULTIPLE_ANSWER_COMBINATION,
4930
                                GLOBAL_MULTIPLE_ANSWER,
4931
                                READING_COMPREHENSION,
4932
                            ]
4933
                        )) {
4934
                            ExerciseShowFunctions::display_unique_or_multiple_answer(
4935
                                $this,
4936
                                $feedback_type,
4937
                                $answerType,
4938
                                $studentChoice,
4939
                                $answer,
4940
                                $answerComment,
4941
                                $answerCorrect,
4942
                                0,
4943
                                0,
4944
                                0,
4945
                                $results_disabled,
4946
                                $showTotalScoreAndUserChoicesInLastAttempt,
4947
                                $this->export
4948
                            );
4949
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
4950
                            ExerciseShowFunctions::display_multiple_answer_true_false(
4951
                                $this,
4952
                                $feedback_type,
4953
                                $answerType,
4954
                                $studentChoice,
4955
                                $answer,
4956
                                $answerComment,
4957
                                $answerCorrect,
4958
                                0,
4959
                                $questionId,
4960
                                0,
4961
                                $results_disabled,
4962
                                $showTotalScoreAndUserChoicesInLastAttempt
4963
                            );
4964
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
4965
                            ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
4966
                                $this,
4967
                                $feedback_type,
4968
                                $studentChoice,
4969
                                $studentChoiceDegree,
4970
                                $answer,
4971
                                $answerComment,
4972
                                $answerCorrect,
4973
                                $questionId,
4974
                                $results_disabled
4975
                            );
4976
                        } elseif (MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType) {
4977
                            ExerciseShowFunctions::display_multiple_answer_combination_true_false(
4978
                                $this,
4979
                                $feedback_type,
4980
                                $answerType,
4981
                                $studentChoice,
4982
                                $answer,
4983
                                $answerComment,
4984
                                $answerCorrect,
4985
                                0,
4986
                                0,
4987
                                0,
4988
                                $results_disabled,
4989
                                $showTotalScoreAndUserChoicesInLastAttempt
4990
                            );
4991
                        } elseif (FILL_IN_BLANKS == $answerType) {
4992
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
4993
                                $this,
4994
                                $feedback_type,
4995
                                $answer,
4996
                                0,
4997
                                0,
4998
                                $results_disabled,
4999
                                $showTotalScoreAndUserChoicesInLastAttempt,
5000
                                ''
5001
                            );
5002
                        } elseif (CALCULATED_ANSWER == $answerType) {
5003
                            ExerciseShowFunctions::display_calculated_answer(
5004
                                $this,
5005
                                $feedback_type,
5006
                                $answer,
5007
                                0,
5008
                                0,
5009
                                $results_disabled,
5010
                                $showTotalScoreAndUserChoicesInLastAttempt,
5011
                                $expectedAnswer,
5012
                                $calculatedChoice,
5013
                                $calculatedStatus
5014
                            );
5015
                        } elseif (FREE_ANSWER == $answerType) {
5016
                            ExerciseShowFunctions::display_free_answer(
5017
                                $feedback_type,
5018
                                $choice,
5019
                                $exeId,
5020
                                $questionId,
5021
                                $questionScore,
5022
                                $results_disabled
5023
                            );
5024
                        } elseif (ORAL_EXPRESSION == $answerType) {
5025
                            // to store the details of open questions in an array to be used in mail
5026
                            /** @var OralExpression $objQuestionTmp */
5027
                            ExerciseShowFunctions::display_oral_expression_answer(
5028
                                $feedback_type,
5029
                                $choice,
5030
                                $exeId,
5031
                                $questionId,
5032
                                $results_disabled,
5033
                                $questionScore,
5034
                                true
5035
                            );
5036
                        } elseif (HOT_SPOT == $answerType) {
5037
                            $correctAnswerId = 0;
5038
                            /** @var TrackEHotspot $hotspot */
5039
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5040
                                if ($hotspot->getHotspotAnswerId() == $answerAutoId) {
5041
                                    break;
5042
                                }
5043
                            }
5044
5045
                            // force to show whether the choice is correct or not
5046
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
5047
                            ExerciseShowFunctions::display_hotspot_answer(
5048
                                $this,
5049
                                $feedback_type,
5050
                                $answerId,
5051
                                $answer,
5052
                                $studentChoice,
5053
                                $answerComment,
5054
                                $results_disabled,
5055
                                $answerId,
5056
                                $showTotalScoreAndUserChoicesInLastAttempt
5057
                            );
5058
                        } elseif (HOT_SPOT_ORDER == $answerType) {
5059
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5060
                                $feedback_type,
5061
                                $answerId,
5062
                                $answer,
5063
                                $studentChoice,
5064
                                $answerComment
5065
                            );*/
5066
                        } elseif (HOT_SPOT_DELINEATION == $answerType) {
5067
                            $user_answer = $_SESSION['exerciseResultCoordinates'][$questionId];
5068
5069
                            // Round-up the coordinates
5070
                            $coords = explode('/', $user_answer);
5071
                            $coords = array_filter($coords);
5072
                            $user_array = '';
5073
                            foreach ($coords as $coord) {
5074
                                if (!empty($coord)) {
5075
                                    $parts = explode(';', $coord);
5076
                                    if (!empty($parts)) {
5077
                                        $user_array .= round($parts[0]).';'.round($parts[1]).'/';
5078
                                    }
5079
                                }
5080
                            }
5081
                            $user_array = substr($user_array, 0, -1) ?: '';
5082
                            if ($next) {
5083
                                $user_answer = $user_array;
5084
                                // We compare only the delineation not the other points
5085
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5086
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5087
5088
                                // Calculating the area
5089
                                $poly_user = convert_coordinates($user_answer, '/');
5090
                                $poly_answer = convert_coordinates($answer_question, '|');
5091
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5092
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5093
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5094
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5095
5096
                                $overlap = $poly_results['both'];
5097
                                $poly_answer_area = $poly_results['s1'];
5098
                                $poly_user_area = $poly_results['s2'];
5099
                                $missing = $poly_results['s1Only'];
5100
                                $excess = $poly_results['s2Only'];
5101
5102
                                // //this is an area in pixels
5103
                                if ($debug > 0) {
5104
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5105
                                }
5106
5107
                                if ($overlap < 1) {
5108
                                    // Shortcut to avoid complicated calculations
5109
                                    $final_overlap = 0;
5110
                                    $final_missing = 100;
5111
                                    $final_excess = 100;
5112
                                } else {
5113
                                    // the final overlap is the percentage of the initial polygon
5114
                                    // that is overlapped by the user's polygon
5115
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5116
                                    if ($debug > 1) {
5117
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap, 0);
5118
                                    }
5119
                                    // the final missing area is the percentage of the initial polygon
5120
                                    // that is not overlapped by the user's polygon
5121
                                    $final_missing = 100 - $final_overlap;
5122
                                    if ($debug > 1) {
5123
                                        error_log(__LINE__.' - Final missing is '.$final_missing, 0);
5124
                                    }
5125
                                    // the final excess area is the percentage of the initial polygon's size
5126
                                    // that is covered by the user's polygon outside of the initial polygon
5127
                                    $final_excess = round(
5128
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5129
                                    );
5130
                                    if ($debug > 1) {
5131
                                        error_log(__LINE__.' - Final excess is '.$final_excess, 0);
5132
                                    }
5133
                                }
5134
5135
                                // Checking the destination parameters parsing the "@@"
5136
                                $destination_items = explode('@@', $answerDestination);
5137
                                $threadhold_total = $destination_items[0];
5138
                                $threadhold_items = explode(';', $threadhold_total);
5139
                                $threadhold1 = $threadhold_items[0]; // overlap
5140
                                $threadhold2 = $threadhold_items[1]; // excess
5141
                                $threadhold3 = $threadhold_items[2]; // missing
5142
5143
                                // if is delineation
5144
                                if (1 === $answerId) {
5145
                                    //setting colors
5146
                                    if ($final_overlap >= $threadhold1) {
5147
                                        $overlap_color = true;
5148
                                    }
5149
                                    if ($final_excess <= $threadhold2) {
5150
                                        $excess_color = true;
5151
                                    }
5152
                                    if ($final_missing <= $threadhold3) {
5153
                                        $missing_color = true;
5154
                                    }
5155
5156
                                    // if pass
5157
                                    if ($final_overlap >= $threadhold1 &&
5158
                                        $final_missing <= $threadhold3 &&
5159
                                        $final_excess <= $threadhold2
5160
                                    ) {
5161
                                        $next = 1; //go to the oars
5162
                                        $result_comment = get_lang('Acceptable');
5163
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5164
                                    } else {
5165
                                        $next = 0;
5166
                                        $result_comment = get_lang('Unacceptable');
5167
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5168
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5169
                                        // checking the destination parameters parsing the "@@"
5170
                                        $destination_items = explode('@@', $answerDestination);
5171
                                    }
5172
                                } elseif ($answerId > 1) {
5173
                                    if ('noerror' == $objAnswerTmp->selectHotspotType($answerId)) {
5174
                                        if ($debug > 0) {
5175
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5176
                                        }
5177
                                        //type no error shouldn't be treated
5178
                                        $next = 1;
5179
5180
                                        continue;
5181
                                    }
5182
                                    if ($debug > 0) {
5183
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5184
                                    }
5185
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5186
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5187
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5188
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5189
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5190
5191
                                    if (false == $overlap) {
5192
                                        //all good, no overlap
5193
                                        $next = 1;
5194
5195
                                        continue;
5196
                                    } else {
5197
                                        if ($debug > 0) {
5198
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5199
                                        }
5200
                                        $organs_at_risk_hit++;
5201
                                        //show the feedback
5202
                                        $next = 0;
5203
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5204
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5205
5206
                                        $destination_items = explode('@@', $answerDestination);
5207
                                        $try_hotspot = $destination_items[1];
5208
                                        $lp_hotspot = $destination_items[2];
5209
                                        $select_question_hotspot = $destination_items[3];
5210
                                        $url_hotspot = $destination_items[4];
5211
                                    }
5212
                                }
5213
                            } else {
5214
                                // the first delineation feedback
5215
                                if ($debug > 0) {
5216
                                    error_log(__LINE__.' first', 0);
5217
                                }
5218
                            }
5219
                        } elseif (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
5220
                            echo '<tr>';
5221
                            echo Display::tag('td', $answerMatching[$answerId]);
5222
                            echo Display::tag(
5223
                                'td',
5224
                                "$user_answer / ".Display::tag(
5225
                                    'strong',
5226
                                    $answerMatching[$answerCorrect],
5227
                                    ['style' => 'color: #008000; font-weight: bold;']
5228
                                )
5229
                            );
5230
                            echo '</tr>';
5231
                        } elseif (ANNOTATION == $answerType) {
5232
                            ExerciseShowFunctions::displayAnnotationAnswer(
5233
                                $feedback_type,
5234
                                $exeId,
5235
                                $questionId,
5236
                                $questionScore,
5237
                                $results_disabled
5238
                            );
5239
                        }
5240
                    }
5241
                } else {
5242
                    if ($debug) {
5243
                        error_log('Showing questions $from '.$from);
5244
                    }
5245
5246
                    switch ($answerType) {
5247
                        case UNIQUE_ANSWER:
5248
                        case UNIQUE_ANSWER_IMAGE:
5249
                        case UNIQUE_ANSWER_NO_OPTION:
5250
                        case MULTIPLE_ANSWER:
5251
                        case GLOBAL_MULTIPLE_ANSWER:
5252
                        case MULTIPLE_ANSWER_COMBINATION:
5253
                        case READING_COMPREHENSION:
5254
                            if (1 == $answerId) {
5255
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5256
                                    $this,
5257
                                    $feedback_type,
5258
                                    $answerType,
5259
                                    $studentChoice,
5260
                                    $answer,
5261
                                    $answerComment,
5262
                                    $answerCorrect,
5263
                                    $exeId,
5264
                                    $questionId,
5265
                                    $answerId,
5266
                                    $results_disabled,
5267
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5268
                                    $this->export
5269
                                );
5270
                            } else {
5271
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5272
                                    $this,
5273
                                    $feedback_type,
5274
                                    $answerType,
5275
                                    $studentChoice,
5276
                                    $answer,
5277
                                    $answerComment,
5278
                                    $answerCorrect,
5279
                                    $exeId,
5280
                                    $questionId,
5281
                                    '',
5282
                                    $results_disabled,
5283
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5284
                                    $this->export
5285
                                );
5286
                            }
5287
5288
                            break;
5289
                        case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
5290
                            if (1 == $answerId) {
5291
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5292
                                    $this,
5293
                                    $feedback_type,
5294
                                    $answerType,
5295
                                    $studentChoice,
5296
                                    $answer,
5297
                                    $answerComment,
5298
                                    $answerCorrect,
5299
                                    $exeId,
5300
                                    $questionId,
5301
                                    $answerId,
5302
                                    $results_disabled,
5303
                                    $showTotalScoreAndUserChoicesInLastAttempt
5304
                                );
5305
                            } else {
5306
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5307
                                    $this,
5308
                                    $feedback_type,
5309
                                    $answerType,
5310
                                    $studentChoice,
5311
                                    $answer,
5312
                                    $answerComment,
5313
                                    $answerCorrect,
5314
                                    $exeId,
5315
                                    $questionId,
5316
                                    '',
5317
                                    $results_disabled,
5318
                                    $showTotalScoreAndUserChoicesInLastAttempt
5319
                                );
5320
                            }
5321
5322
                            break;
5323
                        case MULTIPLE_ANSWER_TRUE_FALSE:
5324
                            if (1 == $answerId) {
5325
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5326
                                    $this,
5327
                                    $feedback_type,
5328
                                    $answerType,
5329
                                    $studentChoice,
5330
                                    $answer,
5331
                                    $answerComment,
5332
                                    $answerCorrect,
5333
                                    $exeId,
5334
                                    $questionId,
5335
                                    $answerId,
5336
                                    $results_disabled,
5337
                                    $showTotalScoreAndUserChoicesInLastAttempt
5338
                                );
5339
                            } else {
5340
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5341
                                    $this,
5342
                                    $feedback_type,
5343
                                    $answerType,
5344
                                    $studentChoice,
5345
                                    $answer,
5346
                                    $answerComment,
5347
                                    $answerCorrect,
5348
                                    $exeId,
5349
                                    $questionId,
5350
                                    '',
5351
                                    $results_disabled,
5352
                                    $showTotalScoreAndUserChoicesInLastAttempt
5353
                                );
5354
                            }
5355
5356
                            break;
5357
                        case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
5358
                            if (1 == $answerId) {
5359
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5360
                                    $this,
5361
                                    $feedback_type,
5362
                                    $studentChoice,
5363
                                    $studentChoiceDegree,
5364
                                    $answer,
5365
                                    $answerComment,
5366
                                    $answerCorrect,
5367
                                    $questionId,
5368
                                    $results_disabled
5369
                                );
5370
                            } else {
5371
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5372
                                    $this,
5373
                                    $feedback_type,
5374
                                    $studentChoice,
5375
                                    $studentChoiceDegree,
5376
                                    $answer,
5377
                                    $answerComment,
5378
                                    $answerCorrect,
5379
                                    $questionId,
5380
                                    $results_disabled
5381
                                );
5382
                            }
5383
5384
                            break;
5385
                        case FILL_IN_BLANKS:
5386
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5387
                                $this,
5388
                                $feedback_type,
5389
                                $answer,
5390
                                $exeId,
5391
                                $questionId,
5392
                                $results_disabled,
5393
                                $showTotalScoreAndUserChoicesInLastAttempt,
5394
                                $str
5395
                            );
5396
5397
                            break;
5398
                        case CALCULATED_ANSWER:
5399
                            ExerciseShowFunctions::display_calculated_answer(
5400
                                $this,
5401
                                $feedback_type,
5402
                                $answer,
5403
                                $exeId,
5404
                                $questionId,
5405
                                $results_disabled,
5406
                                '',
5407
                                $showTotalScoreAndUserChoicesInLastAttempt
5408
                            );
5409
5410
                            break;
5411
                        case FREE_ANSWER:
5412
                            echo ExerciseShowFunctions::display_free_answer(
5413
                                $feedback_type,
5414
                                $choice,
5415
                                $exeId,
5416
                                $questionId,
5417
                                $questionScore,
5418
                                $results_disabled
5419
                            );
5420
5421
                            break;
5422
                        case ORAL_EXPRESSION:
5423
                            /** @var OralExpression $objQuestionTmp */
5424
                            echo '<tr>
5425
                                <td valign="top">'.
5426
                                ExerciseShowFunctions::display_oral_expression_answer(
5427
                                    $feedback_type,
5428
                                    $choice,
5429
                                    $exeId,
5430
                                    $questionId,
5431
                                    $results_disabled,
5432
                                    $questionScore
5433
                                ).'</td>
5434
                                </tr>
5435
                                </table>';
5436
                            break;
5437
                        case HOT_SPOT:
5438
                            $correctAnswerId = 0;
5439
5440
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5441
                                if ($hotspot->getHotspotAnswerId() == $foundAnswerId) {
5442
                                    break;
5443
                                }
5444
                            }
5445
                            ExerciseShowFunctions::display_hotspot_answer(
5446
                                $this,
5447
                                $feedback_type,
5448
                                $answerId,
5449
                                $answer,
5450
                                $studentChoice,
5451
                                $answerComment,
5452
                                $results_disabled,
5453
                                $answerId,
5454
                                $showTotalScoreAndUserChoicesInLastAttempt
5455
                            );
5456
5457
                            break;
5458
                        case HOT_SPOT_DELINEATION:
5459
                            $user_answer = $user_array;
5460
                            if ($next) {
5461
                                $user_answer = $user_array;
5462
                                // we compare only the delineation not the other points
5463
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5464
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5465
5466
                                // calculating the area
5467
                                $poly_user = convert_coordinates($user_answer, '/');
5468
                                $poly_answer = convert_coordinates($answer_question, '|');
5469
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5470
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5471
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5472
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5473
5474
                                $overlap = $poly_results['both'];
5475
                                $poly_answer_area = $poly_results['s1'];
5476
                                $poly_user_area = $poly_results['s2'];
5477
                                $missing = $poly_results['s1Only'];
5478
                                $excess = $poly_results['s2Only'];
5479
                                if ($debug > 0) {
5480
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5481
                                }
5482
                                if ($overlap < 1) {
5483
                                    //shortcut to avoid complicated calculations
5484
                                    $final_overlap = 0;
5485
                                    $final_missing = 100;
5486
                                    $final_excess = 100;
5487
                                } else {
5488
                                    // the final overlap is the percentage of the initial polygon
5489
                                    // that is overlapped by the user's polygon
5490
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5491
5492
                                    // the final missing area is the percentage of the initial polygon that
5493
                                    // is not overlapped by the user's polygon
5494
                                    $final_missing = 100 - $final_overlap;
5495
                                    // the final excess area is the percentage of the initial polygon's size that is
5496
                                    // covered by the user's polygon outside of the initial polygon
5497
                                    $final_excess = round(
5498
                                        (((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100
5499
                                    );
5500
5501
                                    if ($debug > 1) {
5502
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap);
5503
                                        error_log(__LINE__.' - Final excess is '.$final_excess);
5504
                                        error_log(__LINE__.' - Final missing is '.$final_missing);
5505
                                    }
5506
                                }
5507
5508
                                // Checking the destination parameters parsing the "@@"
5509
                                $destination_items = explode('@@', $answerDestination);
5510
                                $threadhold_total = $destination_items[0];
5511
                                $threadhold_items = explode(';', $threadhold_total);
5512
                                $threadhold1 = $threadhold_items[0]; // overlap
5513
                                $threadhold2 = $threadhold_items[1]; // excess
5514
                                $threadhold3 = $threadhold_items[2]; //missing
5515
                                // if is delineation
5516
                                if (1 === $answerId) {
5517
                                    //setting colors
5518
                                    if ($final_overlap >= $threadhold1) {
5519
                                        $overlap_color = true;
5520
                                    }
5521
                                    if ($final_excess <= $threadhold2) {
5522
                                        $excess_color = true;
5523
                                    }
5524
                                    if ($final_missing <= $threadhold3) {
5525
                                        $missing_color = true;
5526
                                    }
5527
5528
                                    // if pass
5529
                                    if ($final_overlap >= $threadhold1 &&
5530
                                        $final_missing <= $threadhold3 &&
5531
                                        $final_excess <= $threadhold2
5532
                                    ) {
5533
                                        $next = 1; //go to the oars
5534
                                        $result_comment = get_lang('Acceptable');
5535
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5536
                                    } else {
5537
                                        $next = 0;
5538
                                        $result_comment = get_lang('Unacceptable');
5539
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5540
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5541
                                        //checking the destination parameters parsing the "@@"
5542
                                        $destination_items = explode('@@', $answerDestination);
5543
                                    }
5544
                                } elseif ($answerId > 1) {
5545
                                    if ('noerror' === $objAnswerTmp->selectHotspotType($answerId)) {
5546
                                        if ($debug > 0) {
5547
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5548
                                        }
5549
                                        //type no error shouldn't be treated
5550
                                        $next = 1;
5551
5552
                                        break;
5553
                                    }
5554
                                    if ($debug > 0) {
5555
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5556
                                    }
5557
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5558
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5559
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5560
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5561
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5562
5563
                                    if (false == $overlap) {
5564
                                        //all good, no overlap
5565
                                        $next = 1;
5566
5567
                                        break;
5568
                                    } else {
5569
                                        if ($debug > 0) {
5570
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5571
                                        }
5572
                                        $organs_at_risk_hit++;
5573
                                        //show the feedback
5574
                                        $next = 0;
5575
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5576
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5577
5578
                                        $destination_items = explode('@@', $answerDestination);
5579
                                        $try_hotspot = $destination_items[1];
5580
                                        $lp_hotspot = $destination_items[2];
5581
                                        $select_question_hotspot = $destination_items[3];
5582
                                        $url_hotspot = $destination_items[4];
5583
                                    }
5584
                                }
5585
                            }
5586
5587
                            break;
5588
                        case HOT_SPOT_ORDER:
5589
                            /*ExerciseShowFunctions::display_hotspot_order_answer(
5590
                                $feedback_type,
5591
                                $answerId,
5592
                                $answer,
5593
                                $studentChoice,
5594
                                $answerComment
5595
                            );*/
5596
5597
                            break;
5598
                        case DRAGGABLE:
5599
                        case MATCHING_DRAGGABLE:
5600
                        case MATCHING:
5601
                            echo '<tr>';
5602
                            echo Display::tag('td', $answerMatching[$answerId]);
5603
                            echo Display::tag(
5604
                                'td',
5605
                                "$user_answer / ".Display::tag(
5606
                                    'strong',
5607
                                    $answerMatching[$answerCorrect],
5608
                                    ['style' => 'color: #008000; font-weight: bold;']
5609
                                )
5610
                            );
5611
                            echo '</tr>';
5612
5613
                            break;
5614
                        case ANNOTATION:
5615
                            ExerciseShowFunctions::displayAnnotationAnswer(
5616
                                $feedback_type,
5617
                                $exeId,
5618
                                $questionId,
5619
                                $questionScore,
5620
                                $results_disabled
5621
                            );
5622
5623
                            break;
5624
                    }
5625
                }
5626
            }
5627
        } // end for that loops over all answers of the current question
5628
5629
        if ($debug) {
5630
            error_log('-- End answer loop --');
5631
        }
5632
5633
        $final_answer = true;
5634
5635
        foreach ($real_answers as $my_answer) {
5636
            if (!$my_answer) {
5637
                $final_answer = false;
5638
            }
5639
        }
5640
5641
        //we add the total score after dealing with the answers
5642
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
5643
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
5644
        ) {
5645
            if ($final_answer) {
5646
                //getting only the first score where we save the weight of all the question
5647
                $answerWeighting = $objAnswerTmp->selectWeighting(1);
5648
                if (empty($answerWeighting) && !empty($firstAnswer) && isset($firstAnswer['ponderation'])) {
5649
                    $answerWeighting = $firstAnswer['ponderation'];
5650
                }
5651
                $questionScore += $answerWeighting;
5652
            }
5653
        }
5654
5655
        $extra_data = [
5656
            'final_overlap' => $final_overlap,
5657
            'final_missing' => $final_missing,
5658
            'final_excess' => $final_excess,
5659
            'overlap_color' => $overlap_color,
5660
            'missing_color' => $missing_color,
5661
            'excess_color' => $excess_color,
5662
            'threadhold1' => $threadhold1,
5663
            'threadhold2' => $threadhold2,
5664
            'threadhold3' => $threadhold3,
5665
        ];
5666
5667
        if ('exercise_result' === $from) {
5668
            // if answer is hotspot. To the difference of exercise_show.php,
5669
            //  we use the results from the session (from_db=0)
5670
            // TODO Change this, because it is wrong to show the user
5671
            //  some results that haven't been stored in the database yet
5672
            if (HOT_SPOT == $answerType || HOT_SPOT_ORDER == $answerType || HOT_SPOT_DELINEATION == $answerType) {
5673
                if ($debug) {
5674
                    error_log('$from AND this is a hotspot kind of question ');
5675
                }
5676
                if (HOT_SPOT_DELINEATION === $answerType) {
5677
                    if ($showHotSpotDelineationTable) {
5678
                        if (!is_numeric($final_overlap)) {
5679
                            $final_overlap = 0;
5680
                        }
5681
                        if (!is_numeric($final_missing)) {
5682
                            $final_missing = 0;
5683
                        }
5684
                        if (!is_numeric($final_excess)) {
5685
                            $final_excess = 0;
5686
                        }
5687
5688
                        if ($final_overlap > 100) {
5689
                            $final_overlap = 100;
5690
                        }
5691
5692
                        $overlap = 0;
5693
                        if ($final_overlap > 0) {
5694
                            $overlap = (int) $final_overlap;
5695
                        }
5696
5697
                        $excess = 0;
5698
                        if ($final_excess > 0) {
5699
                            $excess = (int) $final_excess;
5700
                        }
5701
5702
                        $missing = 0;
5703
                        if ($final_missing > 0) {
5704
                            $missing = (int) $final_missing;
5705
                        }
5706
5707
                        $table_resume = '<table class="table table-hover table-striped data_table">
5708
                                <tr class="row_odd" >
5709
                                    <td></td>
5710
                                    <td ><b>'.get_lang('Requirements').'</b></td>
5711
                                    <td><b>'.get_lang('Your answer').'</b></td>
5712
                                </tr>
5713
                                <tr class="row_even">
5714
                                    <td><b>'.get_lang('Overlapping areaping area').'</b></td>
5715
                                    <td>'.get_lang('Minimum').' '.$threadhold1.'</td>
5716
                                    <td class="text-right '.($overlap_color ? 'text-success' : 'text-error').'">'
5717
                                    .$overlap.'</td>
5718
                                </tr>
5719
                                <tr>
5720
                                    <td><b>'.get_lang('Excessive areaive area').'</b></td>
5721
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold2.'</td>
5722
                                    <td class="text-right '.($excess_color ? 'text-success' : 'text-error').'">'
5723
                                    .$excess.'</td>
5724
                                </tr>
5725
                                <tr class="row_even">
5726
                                    <td><b>'.get_lang('Missing area area').'</b></td>
5727
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold3.'</td>
5728
                                    <td class="text-right '.($missing_color ? 'text-success' : 'text-error').'">'
5729
                                    .$missing.'</td>
5730
                                </tr>
5731
                            </table>';
5732
                        if (0 == $next) {
5733
                        } else {
5734
                            $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
5735
                            $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
5736
                        }
5737
5738
                        $message = '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>
5739
                                    <p style="text-align:center">';
5740
                        $message .= '<p>'.get_lang('Your delineation :').'</p>';
5741
                        $message .= $table_resume;
5742
                        $message .= '<br />'.get_lang('Your result is :').' '.$result_comment.'<br />';
5743
                        if ($organs_at_risk_hit > 0) {
5744
                            $message .= '<p><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
5745
                        }
5746
                        $message .= '<p>'.$comment.'</p>';
5747
                        echo $message;
5748
5749
                        $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][0] = $message;
5750
                        $_SESSION['hotspot_delineation_result'][$this->getId()][$questionId][1] = $_SESSION['exerciseResultCoordinates'][$questionId];
5751
                    } else {
5752
                        echo $hotspot_delineation_result[0];
5753
                    }
5754
5755
                    // Save the score attempts
5756
                    if (1) {
5757
                        //getting the answer 1 or 0 comes from exercise_submit_modal.php
5758
                        $final_answer = $hotspot_delineation_result[1] ?? '';
5759
                        if (0 == $final_answer) {
5760
                            $questionScore = 0;
5761
                        }
5762
                        // we always insert the answer_id 1 = delineation
5763
                        Event::saveQuestionAttempt($this, $questionScore, 1, $quesId, $exeId, 0);
5764
                        //in delineation mode, get the answer from $hotspot_delineation_result[1]
5765
                        $hotspotValue = isset($hotspot_delineation_result[1]) ? 1 === (int) $hotspot_delineation_result[1] ? 1 : 0 : 0;
5766
                        Event::saveExerciseAttemptHotspot(
5767
                            $this,
5768
                            $exeId,
5769
                            $quesId,
5770
                            1,
5771
                            $hotspotValue,
5772
                            $exerciseResultCoordinates[$quesId],
5773
                            false,
5774
                            0,
5775
                            $learnpath_id,
5776
                            $learnpath_item_id
5777
                        );
5778
                    } else {
5779
                        if (0 == $final_answer) {
5780
                            $questionScore = 0;
5781
                            $answer = 0;
5782
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
5783
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5784
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5785
                                    Event::saveExerciseAttemptHotspot(
5786
                                        $this,
5787
                                        $exeId,
5788
                                        $quesId,
5789
                                        $idx,
5790
                                        0,
5791
                                        $val,
5792
                                        false,
5793
                                        0,
5794
                                        $learnpath_id,
5795
                                        $learnpath_item_id
5796
                                    );
5797
                                }
5798
                            }
5799
                        } else {
5800
                            Event::saveQuestionAttempt($this, $questionScore, $answer, $quesId, $exeId, 0);
5801
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5802
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5803
                                    $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
5804
                                    Event::saveExerciseAttemptHotspot(
5805
                                        $this,
5806
                                        $exeId,
5807
                                        $quesId,
5808
                                        $idx,
5809
                                        $hotspotValue,
5810
                                        $val,
5811
                                        false,
5812
                                        0,
5813
                                        $learnpath_id,
5814
                                        $learnpath_item_id
5815
                                    );
5816
                                }
5817
                            }
5818
                        }
5819
                    }
5820
                }
5821
            }
5822
5823
            $relPath = api_get_path(WEB_CODE_PATH);
5824
5825
            if (HOT_SPOT == $answerType || HOT_SPOT_ORDER == $answerType) {
5826
                // We made an extra table for the answers
5827
                if ($show_result) {
5828
                    echo '</table></td></tr>';
5829
                    echo '
5830
                        <tr>
5831
                            <td colspan="2">
5832
                                <p><em>'.get_lang('Image zones')."</em></p>
5833
                                <div id=\"hotspot-solution-$questionId\"></div>
5834
                                <script>
5835
                                    $(function() {
5836
                                        new HotspotQuestion({
5837
                                            questionId: $questionId,
5838
                                            exerciseId: {$this->getId()},
5839
                                            exeId: $exeId,
5840
                                            selector: '#hotspot-solution-$questionId',
5841
                                            for: 'solution',
5842
                                            relPath: '$relPath'
5843
                                        });
5844
                                    });
5845
                                </script>
5846
                            </td>
5847
                        </tr>
5848
                    ";
5849
                }
5850
            } elseif (ANNOTATION == $answerType) {
5851
                if ($show_result) {
5852
                    echo '
5853
                        <p><em>'.get_lang('Annotation').'</em></p>
5854
                        <div id="annotation-canvas-'.$questionId.'"></div>
5855
                        <script>
5856
                            AnnotationQuestion({
5857
                                questionId: parseInt('.$questionId.'),
5858
                                exerciseId: parseInt('.$exeId.'),
5859
                                relPath: \''.$relPath.'\',
5860
                                courseId: parseInt('.$course_id.')
5861
                            });
5862
                        </script>
5863
                    ';
5864
                }
5865
            }
5866
5867
            if ($show_result && ANNOTATION != $answerType) {
5868
                echo '</table>';
5869
            }
5870
        }
5871
        unset($objAnswerTmp);
5872
5873
        $totalWeighting += $questionWeighting;
5874
        // Store results directly in the database
5875
        // For all in one page exercises, the results will be
5876
        // stored by exercise_results.php (using the session)
5877
        if ($save_results) {
5878
            if ($debug) {
5879
                error_log("Save question results $save_results");
5880
                error_log("Question score: $questionScore");
5881
                error_log('choice: ');
5882
                error_log(print_r($choice, 1));
5883
            }
5884
5885
            if (empty($choice)) {
5886
                $choice = 0;
5887
            }
5888
            // with certainty degree
5889
            if (empty($choiceDegreeCertainty)) {
5890
                $choiceDegreeCertainty = 0;
5891
            }
5892
            if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
5893
                MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType ||
5894
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
5895
            ) {
5896
                if (0 != $choice) {
5897
                    $reply = array_keys($choice);
5898
                    $countReply = count($reply);
5899
                    for ($i = 0; $i < $countReply; $i++) {
5900
                        $chosenAnswer = $reply[$i];
5901
                        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
5902
                            if (0 != $choiceDegreeCertainty) {
5903
                                $replyDegreeCertainty = array_keys($choiceDegreeCertainty);
5904
                                $answerDegreeCertainty = isset($replyDegreeCertainty[$i]) ? $replyDegreeCertainty[$i] : '';
5905
                                $answerValue = isset($choiceDegreeCertainty[$answerDegreeCertainty]) ? $choiceDegreeCertainty[$answerDegreeCertainty] : '';
5906
                                Event::saveQuestionAttempt(
5907
                                    $this,
5908
                                    $questionScore,
5909
                                    $chosenAnswer.':'.$choice[$chosenAnswer].':'.$answerValue,
5910
                                    $quesId,
5911
                                    $exeId,
5912
                                    $i,
5913
                                    $this->getId(),
5914
                                    $updateResults,
5915
                                    $questionDuration
5916
                                );
5917
                            }
5918
                        } else {
5919
                            Event::saveQuestionAttempt(
5920
                                $this,
5921
                                $questionScore,
5922
                                $chosenAnswer.':'.$choice[$chosenAnswer],
5923
                                $quesId,
5924
                                $exeId,
5925
                                $i,
5926
                                $this->getId(),
5927
                                $updateResults,
5928
                                $questionDuration
5929
                            );
5930
                        }
5931
                        if ($debug) {
5932
                            error_log('result =>'.$questionScore.' '.$chosenAnswer.':'.$choice[$chosenAnswer]);
5933
                        }
5934
                    }
5935
                } else {
5936
                    Event::saveQuestionAttempt(
5937
                        $this,
5938
                        $questionScore,
5939
                        0,
5940
                        $quesId,
5941
                        $exeId,
5942
                        0,
5943
                        $this->getId(),
5944
                        false,
5945
                        $questionDuration
5946
                    );
5947
                }
5948
            } elseif (MULTIPLE_ANSWER == $answerType || GLOBAL_MULTIPLE_ANSWER == $answerType) {
5949
                if (0 != $choice) {
5950
                    $reply = array_keys($choice);
5951
                    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...
5952
                        $ans = $reply[$i];
5953
                        Event::saveQuestionAttempt(
5954
                            $this,
5955
                            $questionScore,
5956
                            $ans,
5957
                            $quesId,
5958
                            $exeId,
5959
                            $i,
5960
                            $this->id,
5961
                            false,
5962
                            $questionDuration
5963
                        );
5964
                    }
5965
                } else {
5966
                    Event::saveQuestionAttempt(
5967
                        $this,
5968
                        $questionScore,
5969
                        0,
5970
                        $quesId,
5971
                        $exeId,
5972
                        0,
5973
                        $this->id,
5974
                        false,
5975
                        $questionDuration
5976
                    );
5977
                }
5978
            } elseif (MULTIPLE_ANSWER_COMBINATION == $answerType) {
5979
                if (0 != $choice) {
5980
                    $reply = array_keys($choice);
5981
                    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...
5982
                        $ans = $reply[$i];
5983
                        Event::saveQuestionAttempt(
5984
                            $this,
5985
                            $questionScore,
5986
                            $ans,
5987
                            $quesId,
5988
                            $exeId,
5989
                            $i,
5990
                            $this->id,
5991
                            false,
5992
                            $questionDuration
5993
                        );
5994
                    }
5995
                } else {
5996
                    Event::saveQuestionAttempt(
5997
                        $this,
5998
                        $questionScore,
5999
                        0,
6000
                        $quesId,
6001
                        $exeId,
6002
                        0,
6003
                        $this->id,
6004
                        false,
6005
                        $questionDuration
6006
                    );
6007
                }
6008
            } elseif (in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE])) {
6009
                if (isset($matching)) {
6010
                    foreach ($matching as $j => $val) {
6011
                        Event::saveQuestionAttempt(
6012
                            $this,
6013
                            $questionScore,
6014
                            $val,
6015
                            $quesId,
6016
                            $exeId,
6017
                            $j,
6018
                            $this->id,
6019
                            false,
6020
                            $questionDuration
6021
                        );
6022
                    }
6023
                }
6024
            } elseif (FREE_ANSWER == $answerType) {
6025
                $answer = $choice;
6026
                Event::saveQuestionAttempt(
6027
                    $this,
6028
                    $questionScore,
6029
                    $answer,
6030
                    $quesId,
6031
                    $exeId,
6032
                    0,
6033
                    $this->id,
6034
                    false,
6035
                    $questionDuration
6036
                );
6037
            } elseif (ORAL_EXPRESSION == $answerType) {
6038
                $answer = $choice;
6039
                /** @var OralExpression $objQuestionTmp */
6040
                $questionAttemptId = Event::saveQuestionAttempt(
6041
                    $this,
6042
                    $questionScore,
6043
                    $answer,
6044
                    $quesId,
6045
                    $exeId,
6046
                    0,
6047
                    $this->id,
6048
                    false,
6049
                    $questionDuration
6050
                );
6051
6052
                if (false !== $questionAttemptId) {
6053
                    OralExpression::saveAssetInQuestionAttempt($questionAttemptId);
6054
                }
6055
            } elseif (
6056
            in_array(
6057
                $answerType,
6058
                [UNIQUE_ANSWER, UNIQUE_ANSWER_IMAGE, UNIQUE_ANSWER_NO_OPTION, READING_COMPREHENSION]
6059
            )
6060
            ) {
6061
                $answer = $choice;
6062
                Event::saveQuestionAttempt(
6063
                    $this,
6064
                    $questionScore,
6065
                    $answer,
6066
                    $quesId,
6067
                    $exeId,
6068
                    0,
6069
                    $this->id,
6070
                    false,
6071
                    $questionDuration
6072
                );
6073
            } elseif (HOT_SPOT == $answerType || ANNOTATION == $answerType) {
6074
                $answer = [];
6075
                if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) {
6076
                    if ($debug) {
6077
                        error_log('Checking result coordinates');
6078
                    }
6079
                    Database::delete(
6080
                        Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
6081
                        [
6082
                            'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
6083
                                $exeId,
6084
                                $questionId,
6085
                                api_get_course_int_id(),
6086
                            ],
6087
                        ]
6088
                    );
6089
6090
                    foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
6091
                        $answer[] = $val;
6092
                        $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
6093
                        if ($debug) {
6094
                            error_log('Hotspot value: '.$hotspotValue);
6095
                        }
6096
                        Event::saveExerciseAttemptHotspot(
6097
                            $this,
6098
                            $exeId,
6099
                            $quesId,
6100
                            $idx,
6101
                            $hotspotValue,
6102
                            $val,
6103
                            false,
6104
                            $this->id,
6105
                            $learnpath_id,
6106
                            $learnpath_item_id
6107
                        );
6108
                    }
6109
                } else {
6110
                    if ($debug) {
6111
                        error_log('Empty: exerciseResultCoordinates');
6112
                    }
6113
                }
6114
                Event::saveQuestionAttempt(
6115
                    $this,
6116
                    $questionScore,
6117
                    implode('|', $answer),
6118
                    $quesId,
6119
                    $exeId,
6120
                    0,
6121
                    $this->id,
6122
                    false,
6123
                    $questionDuration
6124
                );
6125
            } else {
6126
                Event::saveQuestionAttempt(
6127
                    $this,
6128
                    $questionScore,
6129
                    $answer,
6130
                    $quesId,
6131
                    $exeId,
6132
                    0,
6133
                    $this->id,
6134
                    false,
6135
                    $questionDuration
6136
                );
6137
            }
6138
        }
6139
6140
        if (0 == $propagate_neg && $questionScore < 0) {
6141
            $questionScore = 0;
6142
        }
6143
6144
        if ($save_results) {
6145
            $statsTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6146
            $sql = "UPDATE $statsTable SET
6147
                        score = score + ".(float) $questionScore."
6148
                    WHERE exe_id = $exeId";
6149
            Database::query($sql);
6150
        }
6151
6152
        return [
6153
            'score' => $questionScore,
6154
            'weight' => $questionWeighting,
6155
            'extra' => $extra_data,
6156
            'open_question' => $arrques,
6157
            'open_answer' => $arrans,
6158
            'answer_type' => $answerType,
6159
            'generated_oral_file' => $generatedFilesHtml,
6160
            'user_answered' => $userAnsweredQuestion,
6161
            'correct_answer_id' => $correctAnswerId,
6162
            'answer_destination' => $answerDestination,
6163
        ];
6164
    }
6165
6166
    /**
6167
     * Sends a notification when a user ends an examn.
6168
     *
6169
     * @param string $type                  'start' or 'end' of an exercise
6170
     * @param array  $question_list_answers
6171
     * @param string $origin
6172
     * @param int    $exe_id
6173
     * @param float  $score
6174
     * @param float  $weight
6175
     *
6176
     * @return bool
6177
     */
6178
    public function send_mail_notification_for_exam(
6179
        $type,
6180
        $question_list_answers,
6181
        $origin,
6182
        $exe_id,
6183
        $score = null,
6184
        $weight = null
6185
    ) {
6186
        $setting = api_get_course_setting('email_alert_manager_on_new_quiz');
6187
6188
        if ((empty($setting) || !is_array($setting)) && empty($this->getNotifications())) {
6189
            return false;
6190
        }
6191
6192
        $settingFromExercise = $this->getNotifications();
6193
        if (!empty($settingFromExercise)) {
6194
            $setting = $settingFromExercise;
6195
        }
6196
6197
        // Email configuration settings
6198
        $courseCode = api_get_course_id();
6199
        $courseInfo = api_get_course_info($courseCode);
6200
6201
        if (empty($courseInfo)) {
6202
            return false;
6203
        }
6204
6205
        $sessionId = api_get_session_id();
6206
6207
        $sessionData = '';
6208
        if (!empty($sessionId)) {
6209
            $sessionInfo = api_get_session_info($sessionId);
6210
            if (!empty($sessionInfo)) {
6211
                $sessionData = '<tr>'
6212
                    .'<td>'.get_lang('Session name').'</td>'
6213
                    .'<td>'.$sessionInfo['name'].'</td>'
6214
                    .'</tr>';
6215
            }
6216
        }
6217
6218
        $sendStart = false;
6219
        $sendEnd = false;
6220
        $sendEndOpenQuestion = false;
6221
        $sendEndOralQuestion = false;
6222
6223
        foreach ($setting as $option) {
6224
            switch ($option) {
6225
                case 0:
6226
                    return false;
6227
6228
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
6229
                case 1: // End
6230
                    if ('end' == $type) {
6231
                        $sendEnd = true;
6232
                    }
6233
6234
                    break;
6235
                case 2: // start
6236
                    if ('start' == $type) {
6237
                        $sendStart = true;
6238
                    }
6239
6240
                    break;
6241
                case 3: // end + open
6242
                    if ('end' == $type) {
6243
                        $sendEndOpenQuestion = true;
6244
                    }
6245
6246
                    break;
6247
                case 4: // end + oral
6248
                    if ('end' == $type) {
6249
                        $sendEndOralQuestion = true;
6250
                    }
6251
6252
                    break;
6253
            }
6254
        }
6255
6256
        $user_info = api_get_user_info(api_get_user_id());
6257
        $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_show.php?'.
6258
            api_get_cidreq(true, true, 'qualify').'&id='.$exe_id.'&action=qualify';
6259
6260
        if (!empty($sessionId)) {
6261
            $addGeneralCoach = true;
6262
            $setting = ('true' === api_get_setting('exercise.block_quiz_mail_notification_general_coach'));
6263
            if (true === $setting) {
6264
                $addGeneralCoach = false;
6265
            }
6266
            $teachers = CourseManager::get_coach_list_from_course_code(
6267
                $courseCode,
6268
                $sessionId,
6269
                $addGeneralCoach
6270
            );
6271
        } else {
6272
            $teachers = CourseManager::get_teacher_list_from_course_code($courseCode);
6273
        }
6274
6275
        if ($sendEndOpenQuestion) {
6276
            $this->sendNotificationForOpenQuestions(
6277
                $question_list_answers,
6278
                $origin,
6279
                $user_info,
6280
                $url,
6281
                $teachers
6282
            );
6283
        }
6284
6285
        if ($sendEndOralQuestion) {
6286
            $this->sendNotificationForOralQuestions(
6287
                $question_list_answers,
6288
                $origin,
6289
                $exe_id,
6290
                $user_info,
6291
                $url,
6292
                $teachers
6293
            );
6294
        }
6295
6296
        if (!$sendEnd && !$sendStart) {
6297
            return false;
6298
        }
6299
6300
        $scoreLabel = '';
6301
        if ($sendEnd &&
6302
            ('true' === api_get_setting('exercise.send_score_in_exam_notification_mail_to_manager'))
6303
        ) {
6304
            $notificationPercentage = ('true' === api_get_setting('mail.send_notification_score_in_percentage'));
6305
            $scoreLabel = ExerciseLib::show_score($score, $weight, $notificationPercentage, true);
6306
            $scoreLabel = '<tr>
6307
                            <td>'.get_lang('Score')."</td>
6308
                            <td>&nbsp;$scoreLabel</td>
6309
                        </tr>";
6310
        }
6311
6312
        if ($sendEnd) {
6313
            $msg = get_lang('A learner attempted an exercise').'<br /><br />';
6314
        } else {
6315
            $msg = get_lang('Student just started an exercise').'<br /><br />';
6316
        }
6317
6318
        $msg .= get_lang('Attempt details').' : <br /><br />
6319
                    <table>
6320
                        <tr>
6321
                            <td>'.get_lang('Course name').'</td>
6322
                            <td>#course#</td>
6323
                        </tr>
6324
                        '.$sessionData.'
6325
                        <tr>
6326
                            <td>'.get_lang('Test').'</td>
6327
                            <td>&nbsp;#exercise#</td>
6328
                        </tr>
6329
                        <tr>
6330
                            <td>'.get_lang('Learner name').'</td>
6331
                            <td>&nbsp;#student_complete_name#</td>
6332
                        </tr>
6333
                        <tr>
6334
                            <td>'.get_lang('Learner e-mail').'</td>
6335
                            <td>&nbsp;#email#</td>
6336
                        </tr>
6337
                        '.$scoreLabel.'
6338
                    </table>';
6339
6340
        $variables = [
6341
            '#email#' => $user_info['email'],
6342
            '#exercise#' => $this->exercise,
6343
            '#student_complete_name#' => $user_info['complete_name'],
6344
            '#course#' => Display::url(
6345
                $courseInfo['title'],
6346
                $courseInfo['course_public_url'].'?sid='.$sessionId
6347
            ),
6348
        ];
6349
6350
        if ($sendEnd) {
6351
            $msg .= '<br /><a href="#url#">'.get_lang(
6352
                    'Click this link to check the answer and/or give feedback'
6353
                ).'</a>';
6354
            $variables['#url#'] = $url;
6355
        }
6356
6357
        $content = str_replace(array_keys($variables), array_values($variables), $msg);
6358
6359
        if ($sendEnd) {
6360
            $subject = get_lang('A learner attempted an exercise');
6361
        } else {
6362
            $subject = get_lang('Student just started an exercise');
6363
        }
6364
6365
        if (!empty($teachers)) {
6366
            foreach ($teachers as $user_id => $teacher_data) {
6367
                MessageManager::send_message_simple(
6368
                    $user_id,
6369
                    $subject,
6370
                    $content
6371
                );
6372
            }
6373
        }
6374
    }
6375
6376
    /**
6377
     * @param array $user_data         result of api_get_user_info()
6378
     * @param array $trackExerciseInfo result of get_stat_track_exercise_info
6379
     * @param bool  $saveUserResult
6380
     * @param bool  $allowSignature
6381
     * @param bool  $allowExportPdf
6382
     *
6383
     * @return string
6384
     */
6385
    public function showExerciseResultHeader(
6386
        $user_data,
6387
        $trackExerciseInfo,
6388
        $saveUserResult,
6389
        $allowSignature = false,
6390
        $allowExportPdf = false
6391
    ) {
6392
        if ('true' === api_get_setting('exercise.hide_user_info_in_quiz_result')) {
6393
            return '';
6394
        }
6395
6396
        $start_date = null;
6397
        if (isset($trackExerciseInfo['start_date'])) {
6398
            $start_date = api_convert_and_format_date($trackExerciseInfo['start_date']);
6399
        }
6400
        $duration = isset($trackExerciseInfo['duration_formatted']) ? $trackExerciseInfo['duration_formatted'] : null;
6401
        $ip = isset($trackExerciseInfo['user_ip']) ? $trackExerciseInfo['user_ip'] : null;
6402
6403
        if (!empty($user_data)) {
6404
            $userFullName = $user_data['complete_name'];
6405
            if (api_is_teacher() || api_is_platform_admin(true, true)) {
6406
                $userFullName = '<a href="'.$user_data['profile_url'].'" title="'.get_lang('GoToStudentDetails').'">'.
6407
                    $user_data['complete_name'].'</a>';
6408
            }
6409
6410
            $data = [
6411
                'name_url' => $userFullName,
6412
                'complete_name' => $user_data['complete_name'],
6413
                'username' => $user_data['username'],
6414
                'avatar' => $user_data['avatar_medium'],
6415
                'url' => $user_data['profile_url'],
6416
            ];
6417
6418
            if (!empty($user_data['official_code'])) {
6419
                $data['code'] = $user_data['official_code'];
6420
            }
6421
        }
6422
        // Description can be very long and is generally meant to explain
6423
        //   rules *before* the exam. Leaving here to make display easier if
6424
        //   necessary
6425
        /*
6426
        if (!empty($this->description)) {
6427
            $array[] = array('title' => get_lang("Description"), 'content' => $this->description);
6428
        }
6429
        */
6430
6431
        $data['start_date'] = $start_date;
6432
        $data['duration'] = $duration;
6433
        $data['ip'] = $ip;
6434
6435
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
6436
            $data['title'] = $this->get_formated_title().get_lang('Result');
6437
        } else {
6438
            $data['title'] = PHP_EOL.$this->exercise.' : '.get_lang('Result');
6439
        }
6440
6441
        $questionsCount = count(explode(',', $trackExerciseInfo['data_tracking']));
6442
        $savedAnswersCount = $this->countUserAnswersSavedInExercise($trackExerciseInfo['exe_id']);
6443
6444
        $data['number_of_answers'] = $questionsCount;
6445
        $data['number_of_answers_saved'] = $savedAnswersCount;
6446
        $exeId = $trackExerciseInfo['exe_id'];
6447
6448
        if ('true' === api_get_setting('exercise.quiz_confirm_saved_answers')) {
6449
            $em = Database::getManager();
6450
6451
            if ($saveUserResult) {
6452
                $trackConfirmation = new TrackEExerciseConfirmation();
6453
                $trackConfirmation
6454
                    ->setUser(api_get_user_entity($trackExerciseInfo['exe_user_id']))
6455
                    ->setQuizId($trackExerciseInfo['exe_exo_id'])
6456
                    ->setAttemptId($trackExerciseInfo['exe_id'])
6457
                    ->setQuestionsCount($questionsCount)
6458
                    ->setSavedAnswersCount($savedAnswersCount)
6459
                    ->setCourseId($trackExerciseInfo['c_id'])
6460
                    ->setSessionId($trackExerciseInfo['session_id'])
6461
                    ->setCreatedAt(api_get_utc_datetime(null, false, true));
6462
6463
                $em->persist($trackConfirmation);
6464
                $em->flush();
6465
            } else {
6466
                $trackConfirmation = $em
6467
                    ->getRepository(TrackEExerciseConfirmation::class)
6468
                    ->findOneBy(
6469
                        [
6470
                            'attemptId' => $trackExerciseInfo['exe_id'],
6471
                            'quizId' => $trackExerciseInfo['exe_exo_id'],
6472
                            'courseId' => $trackExerciseInfo['c_id'],
6473
                            'sessionId' => $trackExerciseInfo['session_id'],
6474
                        ]
6475
                    );
6476
            }
6477
6478
            $data['track_confirmation'] = $trackConfirmation;
6479
        }
6480
6481
        $signature = '';
6482
        if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($this)) {
6483
            $signature = ExerciseSignaturePlugin::getSignature($trackExerciseInfo['exe_user_id'], $trackExerciseInfo);
6484
        }
6485
        $tpl = new Template(null, false, false, false, false, false, false);
6486
        $tpl->assign('data', $data);
6487
        $tpl->assign('allow_signature', $allowSignature);
6488
        $tpl->assign('signature', $signature);
6489
        $tpl->assign('allow_export_pdf', $allowExportPdf);
6490
        $tpl->assign(
6491
            'export_url',
6492
            api_get_path(WEB_CODE_PATH).'exercise/result.php?action=export&id='.$exeId.'&'.api_get_cidreq()
6493
        );
6494
        $layoutTemplate = $tpl->get_template('exercise/partials/result_exercise.tpl');
6495
6496
        return $tpl->fetch($layoutTemplate);
6497
    }
6498
6499
    /**
6500
     * Returns the exercise result.
6501
     *
6502
     * @param int        attempt id
6503
     *
6504
     * @return array
6505
     */
6506
    public function get_exercise_result($exe_id)
6507
    {
6508
        $result = [];
6509
        $track_exercise_info = ExerciseLib::get_exercise_track_exercise_info($exe_id);
6510
6511
        if (!empty($track_exercise_info)) {
6512
            $totalScore = 0;
6513
            $objExercise = new self();
6514
            $objExercise->read($track_exercise_info['exe_exo_id']);
6515
            if (!empty($track_exercise_info['data_tracking'])) {
6516
                $question_list = explode(',', $track_exercise_info['data_tracking']);
6517
            }
6518
            foreach ($question_list as $questionId) {
6519
                $question_result = $objExercise->manage_answer(
6520
                    $exe_id,
6521
                    $questionId,
6522
                    '',
6523
                    'exercise_show',
6524
                    [],
6525
                    false,
6526
                    true,
6527
                    false,
6528
                    $objExercise->selectPropagateNeg()
6529
                );
6530
                $totalScore += $question_result['score'];
6531
            }
6532
6533
            if (0 == $objExercise->selectPropagateNeg() && $totalScore < 0) {
6534
                $totalScore = 0;
6535
            }
6536
            $result = [
6537
                'score' => $totalScore,
6538
                'weight' => $track_exercise_info['max_score'],
6539
            ];
6540
        }
6541
6542
        return $result;
6543
    }
6544
6545
    /**
6546
     * Checks if the exercise is visible due a lot of conditions
6547
     * visibility, time limits, student attempts
6548
     * Return associative array
6549
     * value : true if exercise visible
6550
     * message : HTML formatted message
6551
     * rawMessage : text message.
6552
     *
6553
     * @param int  $lpId
6554
     * @param int  $lpItemId
6555
     * @param int  $lpItemViewId
6556
     * @param bool $filterByAdmin
6557
     *
6558
     * @return array
6559
     */
6560
    public function is_visible(
6561
        $lpId = 0,
6562
        $lpItemId = 0,
6563
        $lpItemViewId = 0,
6564
        $filterByAdmin = true
6565
    ) {
6566
        // 1. By default the exercise is visible
6567
        $isVisible = true;
6568
        $message = null;
6569
6570
        // 1.1 Admins and teachers can access to the exercise
6571
        if ($filterByAdmin) {
6572
            if (api_is_platform_admin() || api_is_course_admin() || api_is_course_tutor()) {
6573
                return ['value' => true, 'message' => ''];
6574
            }
6575
        }
6576
6577
        // Deleted exercise.
6578
        if (-1 == $this->active) {
6579
            return [
6580
                'value' => false,
6581
                'message' => Display::return_message(
6582
                    get_lang('TestNotFound'),
6583
                    'warning',
6584
                    false
6585
                ),
6586
                'rawMessage' => get_lang('TestNotFound'),
6587
            ];
6588
        }
6589
6590
        $repo = Container::getQuizRepository();
6591
        $exercise = $repo->find($this->iId);
6592
6593
        if (null === $exercise) {
6594
            return [];
6595
        }
6596
6597
        $course = api_get_course_entity($this->course_id);
6598
        $link = $exercise->getFirstResourceLinkFromCourseSession($course);
6599
6600
        if ($link && $link->isDraft()) {
6601
            $this->active = 0;
6602
        }
6603
6604
        // 2. If the exercise is not active.
6605
        if (empty($lpId)) {
6606
            // 2.1 LP is OFF
6607
            if (0 == $this->active) {
6608
                return [
6609
                    'value' => false,
6610
                    'message' => Display::return_message(
6611
                        get_lang('TestNotFound'),
6612
                        'warning',
6613
                        false
6614
                    ),
6615
                    'rawMessage' => get_lang('TestNotFound'),
6616
                ];
6617
            }
6618
        } else {
6619
            $lp = Container::getLpRepository()->find($lpId);
6620
            // 2.1 LP is loaded
6621
            if ($lp && 0 == $this->active &&
6622
                !learnpath::is_lp_visible_for_student($lp, api_get_user_id(), $course)
6623
            ) {
6624
                return [
6625
                    'value' => false,
6626
                    'message' => Display::return_message(
6627
                        get_lang('TestNotFound'),
6628
                        'warning',
6629
                        false
6630
                    ),
6631
                    'rawMessage' => get_lang('TestNotFound'),
6632
                ];
6633
            }
6634
        }
6635
6636
        // 3. We check if the time limits are on
6637
        $limitTimeExists = false;
6638
        if (!empty($this->start_time) || !empty($this->end_time)) {
6639
            $limitTimeExists = true;
6640
        }
6641
6642
        if ($limitTimeExists) {
6643
            $timeNow = time();
6644
            $existsStartDate = false;
6645
            $nowIsAfterStartDate = true;
6646
            $existsEndDate = false;
6647
            $nowIsBeforeEndDate = true;
6648
6649
            if (!empty($this->start_time)) {
6650
                $existsStartDate = true;
6651
            }
6652
6653
            if (!empty($this->end_time)) {
6654
                $existsEndDate = true;
6655
            }
6656
6657
            // check if we are before-or-after end-or-start date
6658
            if ($existsStartDate && $timeNow < api_strtotime($this->start_time, 'UTC')) {
6659
                $nowIsAfterStartDate = false;
6660
            }
6661
6662
            if ($existsEndDate & $timeNow >= api_strtotime($this->end_time, 'UTC')) {
6663
                $nowIsBeforeEndDate = false;
6664
            }
6665
6666
            // lets check all cases
6667
            if ($existsStartDate && !$existsEndDate) {
6668
                // exists start date and dont exists end date
6669
                if ($nowIsAfterStartDate) {
6670
                    // after start date, no end date
6671
                    $isVisible = true;
6672
                    $message = sprintf(
6673
                        get_lang('TestAvailableSinceX'),
6674
                        api_convert_and_format_date($this->start_time)
6675
                    );
6676
                } else {
6677
                    // before start date, no end date
6678
                    $isVisible = false;
6679
                    $message = sprintf(
6680
                        get_lang('TestAvailableFromX'),
6681
                        api_convert_and_format_date($this->start_time)
6682
                    );
6683
                }
6684
            } elseif (!$existsStartDate && $existsEndDate) {
6685
                // doesnt exist start date, exists end date
6686
                if ($nowIsBeforeEndDate) {
6687
                    // before end date, no start date
6688
                    $isVisible = true;
6689
                    $message = sprintf(
6690
                        get_lang('TestAvailableUntilX'),
6691
                        api_convert_and_format_date($this->end_time)
6692
                    );
6693
                } else {
6694
                    // after end date, no start date
6695
                    $isVisible = false;
6696
                    $message = sprintf(
6697
                        get_lang('TestAvailableUntilX'),
6698
                        api_convert_and_format_date($this->end_time)
6699
                    );
6700
                }
6701
            } elseif ($existsStartDate && $existsEndDate) {
6702
                // exists start date and end date
6703
                if ($nowIsAfterStartDate) {
6704
                    if ($nowIsBeforeEndDate) {
6705
                        // after start date and before end date
6706
                        $isVisible = true;
6707
                        $message = sprintf(
6708
                            get_lang('TestIsActivatedFromXToY'),
6709
                            api_convert_and_format_date($this->start_time),
6710
                            api_convert_and_format_date($this->end_time)
6711
                        );
6712
                    } else {
6713
                        // after start date and after end date
6714
                        $isVisible = false;
6715
                        $message = sprintf(
6716
                            get_lang('TestWasActivatedFromXToY'),
6717
                            api_convert_and_format_date($this->start_time),
6718
                            api_convert_and_format_date($this->end_time)
6719
                        );
6720
                    }
6721
                } else {
6722
                    if ($nowIsBeforeEndDate) {
6723
                        // before start date and before end date
6724
                        $isVisible = false;
6725
                        $message = sprintf(
6726
                            get_lang('TestWillBeActivatedFromXToY'),
6727
                            api_convert_and_format_date($this->start_time),
6728
                            api_convert_and_format_date($this->end_time)
6729
                        );
6730
                    }
6731
                    // case before start date and after end date is impossible
6732
                }
6733
            } elseif (!$existsStartDate && !$existsEndDate) {
6734
                // doesnt exist start date nor end date
6735
                $isVisible = true;
6736
                $message = '';
6737
            }
6738
        }
6739
6740
        // 4. We check if the student have attempts
6741
        if ($isVisible) {
6742
            $exerciseAttempts = $this->selectAttempts();
6743
6744
            if ($exerciseAttempts > 0) {
6745
                $attemptCount = Event::get_attempt_count(
6746
                    api_get_user_id(),
6747
                    $this->getId(),
6748
                    (int) $lpId,
6749
                    (int) $lpItemId,
6750
                    (int) $lpItemViewId
6751
                );
6752
6753
                if ($attemptCount >= $exerciseAttempts) {
6754
                    $message = sprintf(
6755
                        get_lang('You cannot take test <b>%s</b> because you have already reached the maximum of %s attempts.'),
6756
                        $this->name,
6757
                        $exerciseAttempts
6758
                    );
6759
                    $isVisible = false;
6760
                } else {
6761
                    // Check blocking exercise.
6762
                    $extraFieldValue = new ExtraFieldValue('exercise');
6763
                    $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
6764
                        $this->iId,
6765
                        'blocking_percentage'
6766
                    );
6767
                    if ($blockExercise && isset($blockExercise['value']) && !empty($blockExercise['value'])) {
6768
                        $blockPercentage = (int) $blockExercise['value'];
6769
                        $userAttempts = Event::getExerciseResultsByUser(
6770
                            api_get_user_id(),
6771
                            $this->iId,
6772
                            $this->course_id,
6773
                            $this->sessionId,
6774
                            $lpId,
6775
                            $lpItemId
6776
                        );
6777
6778
                        if (!empty($userAttempts)) {
6779
                            $currentAttempt = current($userAttempts);
6780
                            if ($currentAttempt['total_percentage'] <= $blockPercentage) {
6781
                                $message = sprintf(
6782
                                    get_lang('ExerciseBlockBecausePercentageX'),
6783
                                    $blockPercentage
6784
                                );
6785
                                $isVisible = false;
6786
                            }
6787
                        }
6788
                    }
6789
                }
6790
            }
6791
        }
6792
6793
        $rawMessage = '';
6794
        if (!empty($message)) {
6795
            $rawMessage = $message;
6796
            $message = Display::return_message($message, 'warning', false);
6797
        }
6798
6799
        return [
6800
            'value' => $isVisible,
6801
            'message' => $message,
6802
            'rawMessage' => $rawMessage,
6803
        ];
6804
    }
6805
6806
    /**
6807
     * @return bool
6808
     */
6809
    public function added_in_lp()
6810
    {
6811
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
6812
        $sql = "SELECT max_score FROM $TBL_LP_ITEM
6813
                WHERE
6814
                    item_type = '".TOOL_QUIZ."' AND
6815
                    path = '{$this->getId()}'";
6816
        $result = Database::query($sql);
6817
        if (Database::num_rows($result) > 0) {
6818
            return true;
6819
        }
6820
6821
        return false;
6822
    }
6823
6824
    /**
6825
     * Returns an array with this form.
6826
     *
6827
     * @return array
6828
     *
6829
     * @example
6830
     * <code>
6831
     * array (size=3)
6832
     * 999 =>
6833
     * array (size=3)
6834
     * 0 => int 3422
6835
     * 1 => int 3423
6836
     * 2 => int 3424
6837
     * 100 =>
6838
     * array (size=2)
6839
     * 0 => int 3469
6840
     * 1 => int 3470
6841
     * 101 =>
6842
     * array (size=1)
6843
     * 0 => int 3482
6844
     * </code>
6845
     * The array inside the key 999 means the question list that belongs to the media id = 999,
6846
     * this case is special because 999 means "no media".
6847
     */
6848
    public function getMediaList()
6849
    {
6850
        return $this->mediaList;
6851
    }
6852
6853
    /**
6854
     * Is media question activated?
6855
     *
6856
     * @return bool
6857
     */
6858
    public function mediaIsActivated()
6859
    {
6860
        $mediaQuestions = $this->getMediaList();
6861
        $active = false;
6862
        if (isset($mediaQuestions) && !empty($mediaQuestions)) {
6863
            $media_count = count($mediaQuestions);
6864
            if ($media_count > 1) {
6865
                return true;
6866
            } elseif (1 == $media_count) {
6867
                if (isset($mediaQuestions[999])) {
6868
                    return false;
6869
                } else {
6870
                    return true;
6871
                }
6872
            }
6873
        }
6874
6875
        return $active;
6876
    }
6877
6878
    /**
6879
     * Gets question list from the exercise.
6880
     *
6881
     * @return array
6882
     */
6883
    public function getQuestionList()
6884
    {
6885
        return $this->questionList;
6886
    }
6887
6888
    /**
6889
     * Question list with medias compressed like this.
6890
     *
6891
     * @return array
6892
     *
6893
     * @example
6894
     *      <code>
6895
     *      array(
6896
     *      question_id_1,
6897
     *      question_id_2,
6898
     *      media_id, <- this media id contains question ids
6899
     *      question_id_3,
6900
     *      )
6901
     *      </code>
6902
     */
6903
    public function getQuestionListWithMediasCompressed()
6904
    {
6905
        return $this->questionList;
6906
    }
6907
6908
    /**
6909
     * Question list with medias uncompressed like this.
6910
     *
6911
     * @return array
6912
     *
6913
     * @example
6914
     *      <code>
6915
     *      array(
6916
     *      question_id,
6917
     *      question_id,
6918
     *      question_id, <- belongs to a media id
6919
     *      question_id, <- belongs to a media id
6920
     *      question_id,
6921
     *      )
6922
     *      </code>
6923
     */
6924
    public function getQuestionListWithMediasUncompressed()
6925
    {
6926
        return $this->questionListUncompressed;
6927
    }
6928
6929
    /**
6930
     * Sets the question list when the exercise->read() is executed.
6931
     *
6932
     * @param bool $adminView Whether to view the set the list of *all* questions or just the normal student view
6933
     */
6934
    public function setQuestionList($adminView = false)
6935
    {
6936
        // Getting question list.
6937
        $questionList = $this->selectQuestionList(true, $adminView);
6938
        $this->setMediaList($questionList);
6939
        $this->questionList = $this->transformQuestionListWithMedias($questionList, false);
6940
        $this->questionListUncompressed = $this->transformQuestionListWithMedias(
6941
            $questionList,
6942
            true
6943
        );
6944
    }
6945
6946
    /**
6947
     * @params array question list
6948
     * @params bool expand or not question list (true show all questions,
6949
     * false show media question id instead of the question ids)
6950
     */
6951
    public function transformQuestionListWithMedias(
6952
        $question_list,
6953
        $expand_media_questions = false
6954
    ) {
6955
        $new_question_list = [];
6956
        if (!empty($question_list)) {
6957
            $media_questions = $this->getMediaList();
6958
            $media_active = $this->mediaIsActivated($media_questions);
6959
6960
            if ($media_active) {
6961
                $counter = 1;
6962
                foreach ($question_list as $question_id) {
6963
                    $add_question = true;
6964
                    foreach ($media_questions as $media_id => $question_list_in_media) {
6965
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
6966
                            $add_question = false;
6967
                            if (!in_array($media_id, $new_question_list)) {
6968
                                $new_question_list[$counter] = $media_id;
6969
                                $counter++;
6970
                            }
6971
6972
                            break;
6973
                        }
6974
                    }
6975
                    if ($add_question) {
6976
                        $new_question_list[$counter] = $question_id;
6977
                        $counter++;
6978
                    }
6979
                }
6980
                if ($expand_media_questions) {
6981
                    $media_key_list = array_keys($media_questions);
6982
                    foreach ($new_question_list as &$question_id) {
6983
                        if (in_array($question_id, $media_key_list)) {
6984
                            $question_id = $media_questions[$question_id];
6985
                        }
6986
                    }
6987
                    $new_question_list = array_flatten($new_question_list);
6988
                }
6989
            } else {
6990
                $new_question_list = $question_list;
6991
            }
6992
        }
6993
6994
        return $new_question_list;
6995
    }
6996
6997
    /**
6998
     * Get question list depend on the random settings.
6999
     *
7000
     * @return array
7001
     */
7002
    public function get_validated_question_list()
7003
    {
7004
        $isRandomByCategory = $this->isRandomByCat();
7005
        if (0 == $isRandomByCategory) {
7006
            if ($this->isRandom()) {
7007
                return $this->getRandomList();
7008
            }
7009
7010
            return $this->selectQuestionList();
7011
        }
7012
7013
        if ($this->isRandom()) {
7014
            // USE question categories
7015
            // get questions by category for this exercise
7016
            // we have to choice $objExercise->random question in each array values of $tabCategoryQuestions
7017
            // key of $tabCategoryQuestions are the categopy id (0 for not in a category)
7018
            // value is the array of question id of this category
7019
            $questionList = [];
7020
            $categoryQuestions = TestCategory::getQuestionsByCat($this->id);
7021
            $isRandomByCategory = $this->getRandomByCategory();
7022
            // We sort categories based on the term between [] in the head
7023
            // of the category's description
7024
            /* examples of categories :
7025
             * [biologie] Maitriser les mecanismes de base de la genetique
7026
             * [biologie] Relier les moyens de depenses et les agents infectieux
7027
             * [biologie] Savoir ou est produite l'enrgie dans les cellules et sous quelle forme
7028
             * [chimie] Classer les molles suivant leur pouvoir oxydant ou reacteur
7029
             * [chimie] Connaître la denition de la theoie acide/base selon Brönsted
7030
             * [chimie] Connaître les charges des particules
7031
             * We want that in the order of the groups defined by the term
7032
             * between brackets at the beginning of the category title
7033
            */
7034
            // If test option is Grouped By Categories
7035
            if (2 == $isRandomByCategory) {
7036
                $categoryQuestions = TestCategory::sortTabByBracketLabel($categoryQuestions);
7037
            }
7038
            foreach ($categoryQuestions as $question) {
7039
                $number_of_random_question = $this->random;
7040
                if (-1 == $this->random) {
7041
                    $number_of_random_question = count($this->questionList);
7042
                }
7043
                $questionList = array_merge(
7044
                    $questionList,
7045
                    TestCategory::getNElementsFromArray(
7046
                        $question,
7047
                        $number_of_random_question
7048
                    )
7049
                );
7050
            }
7051
            // shuffle the question list if test is not grouped by categories
7052
            if (1 == $isRandomByCategory) {
7053
                shuffle($questionList); // or not
7054
            }
7055
7056
            return $questionList;
7057
        }
7058
7059
        // Problem, random by category has been selected and
7060
        // we have no $this->isRandom number of question selected
7061
        // Should not happened
7062
7063
        return [];
7064
    }
7065
7066
    public function get_question_list($expand_media_questions = false)
7067
    {
7068
        $question_list = $this->get_validated_question_list();
7069
        $question_list = $this->transform_question_list_with_medias($question_list, $expand_media_questions);
7070
7071
        return $question_list;
7072
    }
7073
7074
    public function transform_question_list_with_medias($question_list, $expand_media_questions = false)
7075
    {
7076
        $new_question_list = [];
7077
        if (!empty($question_list)) {
7078
            $media_questions = $this->getMediaList();
7079
            $media_active = $this->mediaIsActivated($media_questions);
7080
7081
            if ($media_active) {
7082
                $counter = 1;
7083
                foreach ($question_list as $question_id) {
7084
                    $add_question = true;
7085
                    foreach ($media_questions as $media_id => $question_list_in_media) {
7086
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
7087
                            $add_question = false;
7088
                            if (!in_array($media_id, $new_question_list)) {
7089
                                $new_question_list[$counter] = $media_id;
7090
                                $counter++;
7091
                            }
7092
7093
                            break;
7094
                        }
7095
                    }
7096
                    if ($add_question) {
7097
                        $new_question_list[$counter] = $question_id;
7098
                        $counter++;
7099
                    }
7100
                }
7101
                if ($expand_media_questions) {
7102
                    $media_key_list = array_keys($media_questions);
7103
                    foreach ($new_question_list as &$question_id) {
7104
                        if (in_array($question_id, $media_key_list)) {
7105
                            $question_id = $media_questions[$question_id];
7106
                        }
7107
                    }
7108
                    $new_question_list = array_flatten($new_question_list);
7109
                }
7110
            } else {
7111
                $new_question_list = $question_list;
7112
            }
7113
        }
7114
7115
        return $new_question_list;
7116
    }
7117
7118
    /**
7119
     * @param int $exe_id
7120
     *
7121
     * @return array
7122
     */
7123
    public function get_stat_track_exercise_info_by_exe_id($exe_id)
7124
    {
7125
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7126
        $exe_id = (int) $exe_id;
7127
        $sql_track = "SELECT * FROM $table WHERE exe_id = $exe_id ";
7128
        $result = Database::query($sql_track);
7129
        $new_array = [];
7130
        if (Database::num_rows($result) > 0) {
7131
            $new_array = Database::fetch_assoc($result);
7132
            $start_date = api_get_utc_datetime($new_array['start_date'], true);
7133
            $end_date = api_get_utc_datetime($new_array['exe_date'], true);
7134
            $new_array['duration_formatted'] = '';
7135
            if (!empty($new_array['exe_duration']) && !empty($start_date) && !empty($end_date)) {
7136
                $time = api_format_time($new_array['exe_duration'], 'js');
7137
                $new_array['duration_formatted'] = $time;
7138
            }
7139
        }
7140
7141
        return $new_array;
7142
    }
7143
7144
    /**
7145
     * @param int $exeId
7146
     *
7147
     * @return bool
7148
     */
7149
    public function removeAllQuestionToRemind($exeId)
7150
    {
7151
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7152
        $exeId = (int) $exeId;
7153
        if (empty($exeId)) {
7154
            return false;
7155
        }
7156
        $sql = "UPDATE $table
7157
                SET questions_to_check = ''
7158
                WHERE exe_id = $exeId ";
7159
        Database::query($sql);
7160
7161
        return true;
7162
    }
7163
7164
    /**
7165
     * @param int   $exeId
7166
     * @param array $questionList
7167
     *
7168
     * @return bool
7169
     */
7170
    public function addAllQuestionToRemind($exeId, $questionList = [])
7171
    {
7172
        $exeId = (int) $exeId;
7173
        if (empty($questionList)) {
7174
            return false;
7175
        }
7176
7177
        $questionListToString = implode(',', $questionList);
7178
        $questionListToString = Database::escape_string($questionListToString);
7179
7180
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7181
        $sql = "UPDATE $table
7182
                SET questions_to_check = '$questionListToString'
7183
                WHERE exe_id = $exeId";
7184
        Database::query($sql);
7185
7186
        return true;
7187
    }
7188
7189
    /**
7190
     * @param int    $exeId
7191
     * @param int    $questionId
7192
     * @param string $action
7193
     */
7194
    public function editQuestionToRemind($exeId, $questionId, $action = 'add')
7195
    {
7196
        $exercise_info = self::get_stat_track_exercise_info_by_exe_id($exeId);
7197
        $questionId = (int) $questionId;
7198
        $exeId = (int) $exeId;
7199
7200
        if ($exercise_info) {
7201
            $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7202
            if (empty($exercise_info['questions_to_check'])) {
7203
                if ('add' == $action) {
7204
                    $sql = "UPDATE $track_exercises
7205
                            SET questions_to_check = '$questionId'
7206
                            WHERE exe_id = $exeId ";
7207
                    Database::query($sql);
7208
                }
7209
            } else {
7210
                $remind_list = explode(',', $exercise_info['questions_to_check']);
7211
                $remind_list_string = '';
7212
                if ('add' === $action) {
7213
                    if (!in_array($questionId, $remind_list)) {
7214
                        $newRemindList = [];
7215
                        $remind_list[] = $questionId;
7216
                        $questionListInSession = Session::read('questionList');
7217
                        if (!empty($questionListInSession)) {
7218
                            foreach ($questionListInSession as $originalQuestionId) {
7219
                                if (in_array($originalQuestionId, $remind_list)) {
7220
                                    $newRemindList[] = $originalQuestionId;
7221
                                }
7222
                            }
7223
                        }
7224
                        $remind_list_string = implode(',', $newRemindList);
7225
                    }
7226
                } elseif ('delete' == $action) {
7227
                    if (!empty($remind_list)) {
7228
                        if (in_array($questionId, $remind_list)) {
7229
                            $remind_list = array_flip($remind_list);
7230
                            unset($remind_list[$questionId]);
7231
                            $remind_list = array_flip($remind_list);
7232
7233
                            if (!empty($remind_list)) {
7234
                                sort($remind_list);
7235
                                array_filter($remind_list);
7236
                                $remind_list_string = implode(',', $remind_list);
7237
                            }
7238
                        }
7239
                    }
7240
                }
7241
                $value = Database::escape_string($remind_list_string);
7242
                $sql = "UPDATE $track_exercises
7243
                        SET questions_to_check = '$value'
7244
                        WHERE exe_id = $exeId ";
7245
                Database::query($sql);
7246
            }
7247
        }
7248
    }
7249
7250
    /**
7251
     * @param string $answer
7252
     */
7253
    public function fill_in_blank_answer_to_array($answer)
7254
    {
7255
        $list = null;
7256
        api_preg_match_all('/\[[^]]+\]/', $answer, $list);
7257
7258
        if (empty($list)) {
7259
            return '';
7260
        }
7261
7262
        return $list[0];
7263
    }
7264
7265
    /**
7266
     * @param string $answer
7267
     *
7268
     * @return string
7269
     */
7270
    public function fill_in_blank_answer_to_string($answer)
7271
    {
7272
        $teacher_answer_list = $this->fill_in_blank_answer_to_array($answer);
7273
        $result = '';
7274
        if (!empty($teacher_answer_list)) {
7275
            foreach ($teacher_answer_list as $teacher_item) {
7276
                //Cleaning student answer list
7277
                $value = strip_tags($teacher_item);
7278
                $value = api_substr($value, 1, api_strlen($value) - 2);
7279
                $value = explode('/', $value);
7280
                if (!empty($value[0])) {
7281
                    $value = trim($value[0]);
7282
                    $value = str_replace('&nbsp;', '', $value);
7283
                    $result .= $value;
7284
                }
7285
            }
7286
        }
7287
7288
        return $result;
7289
    }
7290
7291
    /**
7292
     * @return string
7293
     */
7294
    public function returnTimeLeftDiv()
7295
    {
7296
        $html = '<div id="clock_warning" style="display:none">';
7297
        $html .= Display::return_message(
7298
            get_lang('Time limit reached'),
7299
            'warning'
7300
        );
7301
        $html .= ' ';
7302
        $html .= sprintf(
7303
            get_lang('Just a moment, please. You will be redirected in %s seconds...'),
7304
            '<span id="counter_to_redirect" class="red_alert"></span>'
7305
        );
7306
        $html .= '</div>';
7307
        $icon = Display::getMdiIcon('clock-outline', 'ch-tool-icon');
7308
        $html .= '<div class="count_down">
7309
                    '.get_lang('RemainingTimeToFinishExercise').'
7310
                    '.$icon.'<span id="exercise_clock_warning"></span>
7311
                </div>';
7312
7313
        return $html;
7314
    }
7315
7316
    /**
7317
     * Get categories added in the exercise--category matrix.
7318
     *
7319
     * @return array
7320
     */
7321
    public function getCategoriesInExercise()
7322
    {
7323
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7324
        if (!empty($this->getId())) {
7325
            $sql = "SELECT * FROM $table
7326
                    WHERE exercise_id = {$this->getId()} ";
7327
            $result = Database::query($sql);
7328
            $list = [];
7329
            if (Database::num_rows($result)) {
7330
                while ($row = Database::fetch_assoc($result)) {
7331
                    $list[$row['category_id']] = $row;
7332
                }
7333
7334
                return $list;
7335
            }
7336
        }
7337
7338
        return [];
7339
    }
7340
7341
    /**
7342
     * Get total number of question that will be parsed when using the category/exercise.
7343
     *
7344
     * @return int
7345
     */
7346
    public function getNumberQuestionExerciseCategory()
7347
    {
7348
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7349
        if (!empty($this->getId())) {
7350
            $sql = "SELECT SUM(count_questions) count_questions
7351
                    FROM $table
7352
                    WHERE exercise_id = {$this->getId()}";
7353
            $result = Database::query($sql);
7354
            if (Database::num_rows($result)) {
7355
                $row = Database::fetch_array($result);
7356
7357
                return (int) $row['count_questions'];
7358
            }
7359
        }
7360
7361
        return 0;
7362
    }
7363
7364
    /**
7365
     * Save categories in the TABLE_QUIZ_REL_CATEGORY table
7366
     *
7367
     * @param array $categories
7368
     */
7369
    public function saveCategoriesInExercise($categories)
7370
    {
7371
        if (!empty($categories) && !empty($this->getId())) {
7372
            $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7373
            $sql = "DELETE FROM $table
7374
                    WHERE exercise_id = {$this->getId()}";
7375
            Database::query($sql);
7376
            foreach ($categories as $categoryId => $countQuestions) {
7377
                if ($categoryId !== 0) {
7378
                    $params = [
7379
                        'exercise_id' => $this->getId(),
7380
                        'category_id' => $categoryId,
7381
                        'count_questions' => $countQuestions,
7382
                    ];
7383
                    Database::insert($table, $params);
7384
                }
7385
            }
7386
        }
7387
    }
7388
7389
    /**
7390
     * @param array  $questionList
7391
     * @param int    $currentQuestion
7392
     * @param array  $conditions
7393
     * @param string $link
7394
     *
7395
     * @return string
7396
     */
7397
    public function progressExercisePaginationBar(
7398
        $questionList,
7399
        $currentQuestion,
7400
        $conditions,
7401
        $link
7402
    ) {
7403
        $mediaQuestions = $this->getMediaList();
7404
7405
        $html = '<div class="exercise_pagination pagination pagination-mini"><ul>';
7406
        $counter = 0;
7407
        $nextValue = 0;
7408
        $wasMedia = false;
7409
        $before = 0;
7410
        $counterNoMedias = 0;
7411
        foreach ($questionList as $questionId) {
7412
            $isCurrent = $currentQuestion == $counterNoMedias + 1 ? true : false;
7413
7414
            if (!empty($nextValue)) {
7415
                if ($wasMedia) {
7416
                    $nextValue = $nextValue - $before + 1;
7417
                }
7418
            }
7419
7420
            if (isset($mediaQuestions) && isset($mediaQuestions[$questionId])) {
7421
                $fixedValue = $counterNoMedias;
7422
7423
                $html .= Display::progressPaginationBar(
7424
                    $nextValue,
7425
                    $mediaQuestions[$questionId],
7426
                    $currentQuestion,
7427
                    $fixedValue,
7428
                    $conditions,
7429
                    $link,
7430
                    true,
7431
                    true
7432
                );
7433
7434
                $counter += count($mediaQuestions[$questionId]) - 1;
7435
                $before = count($questionList);
7436
                $wasMedia = true;
7437
                $nextValue += count($questionList);
7438
            } else {
7439
                $html .= Display::parsePaginationItem(
7440
                    $questionId,
7441
                    $isCurrent,
7442
                    $conditions,
7443
                    $link,
7444
                    $counter
7445
                );
7446
                $counter++;
7447
                $nextValue++;
7448
                $wasMedia = false;
7449
            }
7450
            $counterNoMedias++;
7451
        }
7452
        $html .= '</ul></div>';
7453
7454
        return $html;
7455
    }
7456
7457
    /**
7458
     *  Shows a list of numbers that represents the question to answer in a exercise.
7459
     *
7460
     * @param array  $categories
7461
     * @param int    $current
7462
     * @param array  $conditions
7463
     * @param string $link
7464
     *
7465
     * @return string
7466
     */
7467
    public function progressExercisePaginationBarWithCategories(
7468
        $categories,
7469
        $current,
7470
        $conditions = [],
7471
        $link = null
7472
    ) {
7473
        $html = null;
7474
        $counterNoMedias = 0;
7475
        $nextValue = 0;
7476
        $wasMedia = false;
7477
        $before = 0;
7478
7479
        if (!empty($categories)) {
7480
            $selectionType = $this->getQuestionSelectionType();
7481
            $useRootAsCategoryTitle = false;
7482
7483
            // Grouping questions per parent category see BT#6540
7484
            if (in_array(
7485
                $selectionType,
7486
                [
7487
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED,
7488
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM,
7489
                ]
7490
            )) {
7491
                $useRootAsCategoryTitle = true;
7492
            }
7493
7494
            // If the exercise is set to only show the titles of the categories
7495
            // at the root of the tree, then pre-order the categories tree by
7496
            // removing children and summing their questions into the parent
7497
            // categories
7498
            if ($useRootAsCategoryTitle) {
7499
                // The new categories list starts empty
7500
                $newCategoryList = [];
7501
                foreach ($categories as $category) {
7502
                    $rootElement = $category['root'];
7503
7504
                    if (isset($category['parent_info'])) {
7505
                        $rootElement = $category['parent_info']['id'];
7506
                    }
7507
7508
                    //$rootElement = $category['id'];
7509
                    // If the current category's ancestor was never seen
7510
                    // before, then declare it and assign the current
7511
                    // category to it.
7512
                    if (!isset($newCategoryList[$rootElement])) {
7513
                        $newCategoryList[$rootElement] = $category;
7514
                    } else {
7515
                        // If it was already seen, then merge the previous with
7516
                        // the current category
7517
                        $oldQuestionList = $newCategoryList[$rootElement]['question_list'];
7518
                        $category['question_list'] = array_merge($oldQuestionList, $category['question_list']);
7519
                        $newCategoryList[$rootElement] = $category;
7520
                    }
7521
                }
7522
                // Now use the newly built categories list, with only parents
7523
                $categories = $newCategoryList;
7524
            }
7525
7526
            foreach ($categories as $category) {
7527
                $questionList = $category['question_list'];
7528
                // Check if in this category there questions added in a media
7529
                $mediaQuestionId = $category['media_question'];
7530
                $isMedia = false;
7531
                $fixedValue = null;
7532
7533
                // Media exists!
7534
                if (999 != $mediaQuestionId) {
7535
                    $isMedia = true;
7536
                    $fixedValue = $counterNoMedias;
7537
                }
7538
7539
                //$categoryName = $category['path']; << show the path
7540
                $categoryName = $category['name'];
7541
7542
                if ($useRootAsCategoryTitle) {
7543
                    if (isset($category['parent_info'])) {
7544
                        $categoryName = $category['parent_info']['title'];
7545
                    }
7546
                }
7547
                $html .= '<div class="row">';
7548
                $html .= '<div class="span2">'.$categoryName.'</div>';
7549
                $html .= '<div class="span8">';
7550
7551
                if (!empty($nextValue)) {
7552
                    if ($wasMedia) {
7553
                        $nextValue = $nextValue - $before + 1;
7554
                    }
7555
                }
7556
                $html .= Display::progressPaginationBar(
7557
                    $nextValue,
7558
                    $questionList,
7559
                    $current,
7560
                    $fixedValue,
7561
                    $conditions,
7562
                    $link,
7563
                    $isMedia,
7564
                    true
7565
                );
7566
                $html .= '</div>';
7567
                $html .= '</div>';
7568
7569
                if (999 == $mediaQuestionId) {
7570
                    $counterNoMedias += count($questionList);
7571
                } else {
7572
                    $counterNoMedias++;
7573
                }
7574
7575
                $nextValue += count($questionList);
7576
                $before = count($questionList);
7577
7578
                if (999 != $mediaQuestionId) {
7579
                    $wasMedia = true;
7580
                } else {
7581
                    $wasMedia = false;
7582
                }
7583
            }
7584
        }
7585
7586
        return $html;
7587
    }
7588
7589
    /**
7590
     * Renders a question list.
7591
     *
7592
     * @param array $questionList    (with media questions compressed)
7593
     * @param int   $currentQuestion
7594
     * @param array $exerciseResult
7595
     * @param array $attemptList
7596
     * @param array $remindList
7597
     */
7598
    public function renderQuestionList(
7599
        $questionList,
7600
        $currentQuestion,
7601
        $exerciseResult,
7602
        $attemptList,
7603
        $remindList
7604
    ) {
7605
        $mediaQuestions = $this->getMediaList();
7606
        $i = 0;
7607
7608
        // Normal question list render (medias compressed)
7609
        foreach ($questionList as $questionId) {
7610
            $i++;
7611
            // For sequential exercises
7612
7613
            if (ONE_PER_PAGE == $this->type) {
7614
                // If it is not the right question, goes to the next loop iteration
7615
                if ($currentQuestion != $i) {
7616
                    continue;
7617
                } else {
7618
                    if (!in_array(
7619
                        $this->getFeedbackType(),
7620
                        [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
7621
                    )) {
7622
                        // if the user has already answered this question
7623
                        if (isset($exerciseResult[$questionId])) {
7624
                            echo Display::return_message(
7625
                                get_lang('You already answered the question'),
7626
                                'normal'
7627
                            );
7628
7629
                            break;
7630
                        }
7631
                    }
7632
                }
7633
            }
7634
7635
            // The $questionList contains the media id we check
7636
            // if this questionId is a media question type
7637
            if (isset($mediaQuestions[$questionId]) &&
7638
                999 != $mediaQuestions[$questionId]
7639
            ) {
7640
                // The question belongs to a media
7641
                $mediaQuestionList = $mediaQuestions[$questionId];
7642
                $objQuestionTmp = Question::read($questionId);
7643
7644
                $counter = 1;
7645
                if (MEDIA_QUESTION == $objQuestionTmp->type) {
7646
                    echo $objQuestionTmp->show_media_content();
7647
7648
                    $countQuestionsInsideMedia = count($mediaQuestionList);
7649
7650
                    // Show questions that belongs to a media
7651
                    if (!empty($mediaQuestionList)) {
7652
                        // In order to parse media questions we use letters a, b, c, etc.
7653
                        $letterCounter = 97;
7654
                        foreach ($mediaQuestionList as $questionIdInsideMedia) {
7655
                            $isLastQuestionInMedia = false;
7656
                            if ($counter == $countQuestionsInsideMedia) {
7657
                                $isLastQuestionInMedia = true;
7658
                            }
7659
                            $this->renderQuestion(
7660
                                $questionIdInsideMedia,
7661
                                $attemptList,
7662
                                $remindList,
7663
                                chr($letterCounter),
7664
                                $currentQuestion,
7665
                                $mediaQuestionList,
7666
                                $isLastQuestionInMedia,
7667
                                $questionList
7668
                            );
7669
                            $letterCounter++;
7670
                            $counter++;
7671
                        }
7672
                    }
7673
                } else {
7674
                    $this->renderQuestion(
7675
                        $questionId,
7676
                        $attemptList,
7677
                        $remindList,
7678
                        $i,
7679
                        $currentQuestion,
7680
                        null,
7681
                        null,
7682
                        $questionList
7683
                    );
7684
                    $i++;
7685
                }
7686
            } else {
7687
                // Normal question render.
7688
                $this->renderQuestion(
7689
                    $questionId,
7690
                    $attemptList,
7691
                    $remindList,
7692
                    $i,
7693
                    $currentQuestion,
7694
                    null,
7695
                    null,
7696
                    $questionList
7697
                );
7698
            }
7699
7700
            // For sequential exercises.
7701
            if (ONE_PER_PAGE == $this->type) {
7702
                // quits the loop
7703
                break;
7704
            }
7705
        }
7706
        // end foreach()
7707
7708
        if (ALL_ON_ONE_PAGE == $this->type) {
7709
            $exercise_actions = $this->show_button($questionId, $currentQuestion);
7710
            echo Display::div($exercise_actions, ['class' => 'exercise_actions']);
7711
        }
7712
    }
7713
7714
    /**
7715
     * Not implemented in 1.11.x.
7716
     *
7717
     * @param int   $questionId
7718
     * @param array $attemptList
7719
     * @param array $remindList
7720
     * @param int   $i
7721
     * @param int   $current_question
7722
     * @param array $questions_in_media
7723
     * @param bool  $last_question_in_media
7724
     * @param array $realQuestionList
7725
     * @param bool  $generateJS
7726
     */
7727
    public function renderQuestion(
7728
        $questionId,
7729
        $attemptList,
7730
        $remindList,
7731
        $i,
7732
        $current_question,
7733
        $questions_in_media = [],
7734
        $last_question_in_media = false,
7735
        $realQuestionList = [],
7736
        $generateJS = true
7737
    ) {
7738
        // With this option on the question is loaded via AJAX
7739
        //$generateJS = true;
7740
        //$this->loadQuestionAJAX = true;
7741
7742
        if ($generateJS && $this->loadQuestionAJAX) {
7743
            $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?a=get_question&id='.$questionId.'&'.api_get_cidreq();
7744
            $params = [
7745
                'questionId' => $questionId,
7746
                'attemptList' => $attemptList,
7747
                'remindList' => $remindList,
7748
                'i' => $i,
7749
                'current_question' => $current_question,
7750
                'questions_in_media' => $questions_in_media,
7751
                'last_question_in_media' => $last_question_in_media,
7752
            ];
7753
            $params = json_encode($params);
7754
7755
            $script = '<script>
7756
            $(function(){
7757
                var params = '.$params.';
7758
                $.ajax({
7759
                    type: "GET",
7760
                    data: params,
7761
                    url: "'.$url.'",
7762
                    success: function(return_value) {
7763
                        $("#ajaxquestiondiv'.$questionId.'").html(return_value);
7764
                    }
7765
                });
7766
            });
7767
            </script>
7768
            <div id="ajaxquestiondiv'.$questionId.'"></div>';
7769
            echo $script;
7770
        } else {
7771
            $origin = api_get_origin();
7772
            $question_obj = Question::read($questionId);
7773
            $user_choice = isset($attemptList[$questionId]) ? $attemptList[$questionId] : null;
7774
            $remind_highlight = null;
7775
7776
            // Hides questions when reviewing a ALL_ON_ONE_PAGE exercise
7777
            // see #4542 no_remind_highlight class hide with jquery
7778
            if (ALL_ON_ONE_PAGE == $this->type && isset($_GET['reminder']) && 2 == $_GET['reminder']) {
7779
                $remind_highlight = 'no_remind_highlight';
7780
                // @todo not implemented in 1.11.x
7781
                /*if (in_array($question_obj->type, Question::question_type_no_review())) {
7782
                    return null;
7783
                }*/
7784
            }
7785
7786
            $attributes = ['id' => 'remind_list['.$questionId.']'];
7787
7788
            // Showing the question
7789
            $exercise_actions = null;
7790
            echo '<a id="questionanchor'.$questionId.'"></a><br />';
7791
            echo '<div id="question_div_'.$questionId.'" class="main_question '.$remind_highlight.'" >';
7792
7793
            // Shows the question + possible answers
7794
            $showTitle = 1 == $this->getHideQuestionTitle() ? false : true;
7795
            // @todo not implemented in 1.11.x
7796
            /*echo $this->showQuestion(
7797
                $question_obj,
7798
                false,
7799
                $origin,
7800
                $i,
7801
                $showTitle,
7802
                false,
7803
                $user_choice,
7804
                false,
7805
                null,
7806
                false,
7807
                $this->getModelType(),
7808
                $this->categoryMinusOne
7809
            );*/
7810
7811
            // Button save and continue
7812
            switch ($this->type) {
7813
                case ONE_PER_PAGE:
7814
                    $exercise_actions .= $this->show_button(
7815
                        $questionId,
7816
                        $current_question,
7817
                        null,
7818
                        $remindList
7819
                    );
7820
7821
                    break;
7822
                case ALL_ON_ONE_PAGE:
7823
                    if (api_is_allowed_to_session_edit()) {
7824
                        $button = [
7825
                            Display::button(
7826
                                'save_now',
7827
                                get_lang('Save and continue'),
7828
                                ['type' => 'button', 'class' => 'btn btn--primary', 'data-question' => $questionId]
7829
                            ),
7830
                            '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>',
7831
                        ];
7832
                        $exercise_actions .= Display::div(
7833
                            implode(PHP_EOL, $button),
7834
                            ['class' => 'exercise_save_now_button']
7835
                        );
7836
                    }
7837
7838
                    break;
7839
            }
7840
7841
            if (!empty($questions_in_media)) {
7842
                $count_of_questions_inside_media = count($questions_in_media);
7843
                if ($count_of_questions_inside_media > 1 && api_is_allowed_to_session_edit()) {
7844
                    $button = [
7845
                        Display::button(
7846
                            'save_now',
7847
                            get_lang('Save and continue'),
7848
                            ['type' => 'button', 'class' => 'btn btn--primary', 'data-question' => $questionId]
7849
                        ),
7850
                        '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>&nbsp;',
7851
                    ];
7852
                    $exercise_actions = Display::div(
7853
                        implode(PHP_EOL, $button),
7854
                        ['class' => 'exercise_save_now_button']
7855
                    );
7856
                }
7857
7858
                if ($last_question_in_media && ONE_PER_PAGE == $this->type) {
7859
                    $exercise_actions = $this->show_button($questionId, $current_question, $questions_in_media);
7860
                }
7861
            }
7862
7863
            // Checkbox review answers. Not implemented.
7864
            /*if ($this->review_answers &&
7865
                !in_array($question_obj->type, Question::question_type_no_review())
7866
            ) {
7867
                $remind_question_div = Display::tag(
7868
                    'label',
7869
                    Display::input(
7870
                        'checkbox',
7871
                        'remind_list['.$questionId.']',
7872
                        '',
7873
                        $attributes
7874
                    ).get_lang('Revise question later'),
7875
                    [
7876
                        'class' => 'checkbox',
7877
                        'for' => 'remind_list['.$questionId.']',
7878
                    ]
7879
                );
7880
                $exercise_actions .= Display::div(
7881
                    $remind_question_div,
7882
                    ['class' => 'exercise_save_now_button']
7883
                );
7884
            }*/
7885
7886
            echo Display::div(' ', ['class' => 'clear']);
7887
7888
            $paginationCounter = null;
7889
            if (ONE_PER_PAGE == $this->type) {
7890
                if (empty($questions_in_media)) {
7891
                    $paginationCounter = Display::paginationIndicator(
7892
                        $current_question,
7893
                        count($realQuestionList)
7894
                    );
7895
                } else {
7896
                    if ($last_question_in_media) {
7897
                        $paginationCounter = Display::paginationIndicator(
7898
                            $current_question,
7899
                            count($realQuestionList)
7900
                        );
7901
                    }
7902
                }
7903
            }
7904
7905
            echo '<div class="row"><div class="pull-right">'.$paginationCounter.'</div></div>';
7906
            echo Display::div($exercise_actions, ['class' => 'form-actions']);
7907
            echo '</div>';
7908
        }
7909
    }
7910
7911
    /**
7912
     * Returns an array of categories details for the questions of the current
7913
     * exercise.
7914
     *
7915
     * @return array
7916
     */
7917
    public function getQuestionWithCategories()
7918
    {
7919
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
7920
        $categoryRelTable = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
7921
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
7922
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
7923
        $sql = "SELECT DISTINCT cat.*
7924
                FROM $TBL_EXERCICE_QUESTION e
7925
                INNER JOIN $TBL_QUESTIONS q
7926
                ON (e.question_id = q.iid)
7927
                INNER JOIN $categoryRelTable catRel
7928
                ON (catRel.question_id = e.question_id)
7929
                INNER JOIN $categoryTable cat
7930
                ON (cat.iid = catRel.category_id)
7931
                WHERE
7932
                  e.quiz_id	= ".(int) ($this->getId());
7933
7934
        $result = Database::query($sql);
7935
        $categoriesInExercise = [];
7936
        if (Database::num_rows($result)) {
7937
            $categoriesInExercise = Database::store_result($result, 'ASSOC');
7938
        }
7939
7940
        return $categoriesInExercise;
7941
    }
7942
7943
    /**
7944
     * Calculate the max_score of the quiz, depending of question inside, and quiz advanced option.
7945
     */
7946
    public function getMaxScore()
7947
    {
7948
        $outMaxScore = 0;
7949
        // list of question's id !!! the array key start at 1 !!!
7950
        $questionList = $this->selectQuestionList(true);
7951
7952
        if ($this->random > 0 && $this->randomByCat > 0) {
7953
            // test is random by category
7954
            // get the $numberRandomQuestions best score question of each category
7955
            $numberRandomQuestions = $this->random;
7956
            $tabCategoriesScores = [];
7957
            foreach ($questionList as $questionId) {
7958
                $questionCategoryId = TestCategory::getCategoryForQuestion($questionId);
7959
                if (!is_array($tabCategoriesScores[$questionCategoryId])) {
7960
                    $tabCategoriesScores[$questionCategoryId] = [];
7961
                }
7962
                $tmpObjQuestion = Question::read($questionId);
7963
                if (is_object($tmpObjQuestion)) {
7964
                    $tabCategoriesScores[$questionCategoryId][] = $tmpObjQuestion->weighting;
7965
                }
7966
            }
7967
7968
            // here we've got an array with first key, the category_id, second key, score of question for this cat
7969
            foreach ($tabCategoriesScores as $tabScores) {
7970
                rsort($tabScores);
7971
                $tabScoresCount = count($tabScores);
7972
                for ($i = 0; $i < min($numberRandomQuestions, $tabScoresCount); $i++) {
7973
                    $outMaxScore += $tabScores[$i];
7974
                }
7975
            }
7976
7977
            return $outMaxScore;
7978
        }
7979
7980
        // standard test, just add each question score
7981
        foreach ($questionList as $questionId) {
7982
            $question = Question::read($questionId, $this->course);
7983
            $outMaxScore += $question->weighting;
7984
        }
7985
7986
        return $outMaxScore;
7987
    }
7988
7989
    /**
7990
     * @return string
7991
     */
7992
    public function get_formated_title()
7993
    {
7994
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
7995
        }
7996
7997
        return api_html_entity_decode($this->selectTitle());
7998
    }
7999
8000
    /**
8001
     * @param string $title
8002
     *
8003
     * @return string
8004
     */
8005
    public static function get_formated_title_variable($title)
8006
    {
8007
        return api_html_entity_decode($title);
8008
    }
8009
8010
    /**
8011
     * @return string
8012
     */
8013
    public function format_title()
8014
    {
8015
        return api_htmlentities($this->title);
8016
    }
8017
8018
    /**
8019
     * @param string $title
8020
     *
8021
     * @return string
8022
     */
8023
    public static function format_title_variable($title)
8024
    {
8025
        return api_htmlentities($title);
8026
    }
8027
8028
    /**
8029
     * @param int $courseId
8030
     * @param int $sessionId
8031
     *
8032
     * @return array exercises
8033
     */
8034
    public function getExercisesByCourseSession($courseId, $sessionId)
8035
    {
8036
        $courseId = (int) $courseId;
8037
        $sessionId = (int) $sessionId;
8038
8039
        $tbl_quiz = Database::get_course_table(TABLE_QUIZ_TEST);
8040
        $sql = "SELECT * FROM $tbl_quiz cq
8041
                WHERE
8042
                    cq.c_id = %s AND
8043
                    (cq.session_id = %s OR cq.session_id = 0) AND
8044
                    cq.active = 0
8045
                ORDER BY cq.iid";
8046
        $sql = sprintf($sql, $courseId, $sessionId);
8047
8048
        $result = Database::query($sql);
8049
8050
        $rows = [];
8051
        while ($row = Database::fetch_assoc($result)) {
8052
            $rows[] = $row;
8053
        }
8054
8055
        return $rows;
8056
    }
8057
8058
    /**
8059
     * @param int   $courseId
8060
     * @param int   $sessionId
8061
     * @param array $quizId
8062
     *
8063
     * @return array exercises
8064
     */
8065
    public function getExerciseAndResult($courseId, $sessionId, $quizId = [])
8066
    {
8067
        if (empty($quizId)) {
8068
            return [];
8069
        }
8070
8071
        $sessionId = (int) $sessionId;
8072
        $courseId = (int) $courseId;
8073
8074
        $ids = is_array($quizId) ? $quizId : [$quizId];
8075
        $ids = array_map('intval', $ids);
8076
        $ids = implode(',', $ids);
8077
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8078
        if (0 != $sessionId) {
8079
            $sql = "SELECT * FROM $track_exercises te
8080
              INNER JOIN c_quiz cq
8081
              ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id
8082
              WHERE
8083
              te.id = %s AND
8084
              te.session_id = %s AND
8085
              cq.id IN (%s)
8086
              ORDER BY cq.id";
8087
8088
            $sql = sprintf($sql, $courseId, $sessionId, $ids);
8089
        } else {
8090
            $sql = "SELECT * FROM $track_exercises te
8091
              INNER JOIN c_quiz cq ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id
8092
              WHERE
8093
              te.id = %s AND
8094
              cq.id IN (%s)
8095
              ORDER BY cq.id";
8096
            $sql = sprintf($sql, $courseId, $ids);
8097
        }
8098
        $result = Database::query($sql);
8099
        $rows = [];
8100
        while ($row = Database::fetch_assoc($result)) {
8101
            $rows[] = $row;
8102
        }
8103
8104
        return $rows;
8105
    }
8106
8107
    /**
8108
     * @param $exeId
8109
     * @param $exercise_stat_info
8110
     * @param $remindList
8111
     * @param $currentQuestion
8112
     *
8113
     * @return int|null
8114
     */
8115
    public static function getNextQuestionId(
8116
        $exeId,
8117
        $exercise_stat_info,
8118
        $remindList,
8119
        $currentQuestion
8120
    ) {
8121
        $result = Event::get_exercise_results_by_attempt($exeId, 'incomplete');
8122
8123
        if (isset($result[$exeId])) {
8124
            $result = $result[$exeId];
8125
        } else {
8126
            return null;
8127
        }
8128
8129
        $data_tracking = $exercise_stat_info['data_tracking'];
8130
        $data_tracking = explode(',', $data_tracking);
8131
8132
        // if this is the final question do nothing.
8133
        if ($currentQuestion == count($data_tracking)) {
8134
            return null;
8135
        }
8136
8137
        $currentQuestion--;
8138
8139
        if (!empty($result['question_list'])) {
8140
            $answeredQuestions = [];
8141
            foreach ($result['question_list'] as $question) {
8142
                if (!empty($question['answer'])) {
8143
                    $answeredQuestions[] = $question['question_id'];
8144
                }
8145
            }
8146
8147
            // Checking answered questions
8148
            $counterAnsweredQuestions = 0;
8149
            foreach ($data_tracking as $questionId) {
8150
                if (!in_array($questionId, $answeredQuestions)) {
8151
                    if ($currentQuestion != $counterAnsweredQuestions) {
8152
                        break;
8153
                    }
8154
                }
8155
                $counterAnsweredQuestions++;
8156
            }
8157
8158
            $counterRemindListQuestions = 0;
8159
            // Checking questions saved in the reminder list
8160
            if (!empty($remindList)) {
8161
                foreach ($data_tracking as $questionId) {
8162
                    if (in_array($questionId, $remindList)) {
8163
                        // Skip the current question
8164
                        if ($currentQuestion != $counterRemindListQuestions) {
8165
                            break;
8166
                        }
8167
                    }
8168
                    $counterRemindListQuestions++;
8169
                }
8170
8171
                if ($counterRemindListQuestions < $currentQuestion) {
8172
                    return null;
8173
                }
8174
8175
                if (!empty($counterRemindListQuestions)) {
8176
                    if ($counterRemindListQuestions > $counterAnsweredQuestions) {
8177
                        return $counterAnsweredQuestions;
8178
                    } else {
8179
                        return $counterRemindListQuestions;
8180
                    }
8181
                }
8182
            }
8183
8184
            return $counterAnsweredQuestions;
8185
        }
8186
    }
8187
8188
    /**
8189
     * Gets the position of a questionId in the question list.
8190
     *
8191
     * @param $questionId
8192
     *
8193
     * @return int
8194
     */
8195
    public function getPositionInCompressedQuestionList($questionId)
8196
    {
8197
        $questionList = $this->getQuestionListWithMediasCompressed();
8198
        $mediaQuestions = $this->getMediaList();
8199
        $position = 1;
8200
        foreach ($questionList as $id) {
8201
            if (isset($mediaQuestions[$id]) && in_array($questionId, $mediaQuestions[$id])) {
8202
                $mediaQuestionList = $mediaQuestions[$id];
8203
                if (in_array($questionId, $mediaQuestionList)) {
8204
                    return $position;
8205
                } else {
8206
                    $position++;
8207
                }
8208
            } else {
8209
                if ($id == $questionId) {
8210
                    return $position;
8211
                } else {
8212
                    $position++;
8213
                }
8214
            }
8215
        }
8216
8217
        return 1;
8218
    }
8219
8220
    /**
8221
     * Get the correct answers in all attempts.
8222
     *
8223
     * @param int  $learnPathId
8224
     * @param int  $learnPathItemId
8225
     * @param bool $onlyCorrect
8226
     *
8227
     * @return array
8228
     */
8229
    public function getAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0, $onlyCorrect = true)
8230
    {
8231
        $attempts = Event::getExerciseResultsByUser(
8232
            api_get_user_id(),
8233
            $this->getId(),
8234
            api_get_course_int_id(),
8235
            api_get_session_id(),
8236
            $learnPathId,
8237
            $learnPathItemId,
8238
            'DESC'
8239
        );
8240
8241
        $list = [];
8242
        foreach ($attempts as $attempt) {
8243
            foreach ($attempt['question_list'] as $answers) {
8244
                foreach ($answers as $answer) {
8245
                    $objAnswer = new Answer($answer['question_id']);
8246
                    if ($onlyCorrect) {
8247
                        switch ($objAnswer->getQuestionType()) {
8248
                            case FILL_IN_BLANKS:
8249
                                $isCorrect = FillBlanks::isCorrect($answer['answer']);
8250
8251
                                break;
8252
                            case MATCHING:
8253
                            case DRAGGABLE:
8254
                            case MATCHING_DRAGGABLE:
8255
                                $isCorrect = Matching::isCorrect(
8256
                                    $answer['position'],
8257
                                    $answer['answer'],
8258
                                    $answer['question_id']
8259
                                );
8260
8261
                                break;
8262
                            case ORAL_EXPRESSION:
8263
                                $isCorrect = false;
8264
8265
                                break;
8266
                            default:
8267
                                $isCorrect = $objAnswer->isCorrectByAutoId($answer['answer']);
8268
                        }
8269
                        if ($isCorrect) {
8270
                            $list[$answer['question_id']][] = $answer;
8271
                        }
8272
                    } else {
8273
                        $list[$answer['question_id']][] = $answer;
8274
                    }
8275
                }
8276
            }
8277
8278
            if (false === $onlyCorrect) {
8279
                // Only take latest attempt
8280
                break;
8281
            }
8282
        }
8283
8284
        return $list;
8285
    }
8286
8287
    /**
8288
     * Get the correct answers in all attempts.
8289
     *
8290
     * @param int $learnPathId
8291
     * @param int $learnPathItemId
8292
     *
8293
     * @return array
8294
     */
8295
    public function getCorrectAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0)
8296
    {
8297
        return $this->getAnswersInAllAttempts($learnPathId, $learnPathItemId);
8298
    }
8299
8300
    /**
8301
     * @return bool
8302
     */
8303
    public function showPreviousButton()
8304
    {
8305
        $allow = ('true' === api_get_setting('exercise.allow_quiz_show_previous_button_setting'));
8306
        if (false === $allow) {
8307
            return true;
8308
        }
8309
8310
        return $this->showPreviousButton;
8311
    }
8312
8313
    public function getPreventBackwards()
8314
    {
8315
        return (int) $this->preventBackwards;
8316
    }
8317
8318
    /**
8319
     * @return int
8320
     */
8321
    public function getQuizCategoryId(): ?int
8322
    {
8323
        if (empty($this->quizCategoryId)) {
8324
            return null;
8325
        }
8326
8327
        return (int) $this->quizCategoryId;
8328
    }
8329
8330
    /**
8331
     * @param int $value
8332
     */
8333
    public function setQuizCategoryId($value): void
8334
    {
8335
        if (!empty($value)) {
8336
            $this->quizCategoryId = (int) $value;
8337
        }
8338
    }
8339
8340
    /**
8341
     * Set the value to 1 to hide the question number.
8342
     *
8343
     * @param int $value
8344
     */
8345
    public function setHideQuestionNumber($value = 0)
8346
    {
8347
        $this->hideQuestionNumber = (int) $value;
8348
    }
8349
8350
    /**
8351
     * Gets the value to hide or show the question number. If it does not exist, it is set to 0.
8352
     *
8353
     * @return int 1 if the question number must be hidden
8354
     */
8355
    public function getHideQuestionNumber()
8356
    {
8357
        return (int) $this->hideQuestionNumber;
8358
    }
8359
8360
    public function setPageResultConfiguration(array $values)
8361
    {
8362
        $pageConfig = ('true' === api_get_setting('exercise.allow_quiz_results_page_config'));
8363
        if ($pageConfig) {
8364
            $params = [
8365
                'hide_expected_answer' => $values['hide_expected_answer'] ?? '',
8366
                'hide_question_score' => $values['hide_question_score'] ?? '',
8367
                'hide_total_score' => $values['hide_total_score'] ?? '',
8368
                'hide_category_table' => $values['hide_category_table'] ?? '',
8369
                'hide_correct_answered_questions' => $values['hide_correct_answered_questions'] ?? '',
8370
            ];
8371
            $this->pageResultConfiguration = $params;
8372
        }
8373
    }
8374
8375
    /**
8376
     * @param array $defaults
8377
     */
8378
    public function setPageResultConfigurationDefaults(&$defaults)
8379
    {
8380
        $configuration = $this->getPageResultConfiguration();
8381
        if (!empty($configuration) && !empty($defaults)) {
8382
            $defaults = array_merge($defaults, $configuration);
8383
        }
8384
    }
8385
8386
    /**
8387
     * @return array
8388
     */
8389
    public function getPageResultConfiguration()
8390
    {
8391
        $pageConfig = ('true' === api_get_setting('exercise.allow_quiz_results_page_config'));
8392
        if ($pageConfig) {
8393
            return $this->pageResultConfiguration;
8394
        }
8395
8396
        return [];
8397
    }
8398
8399
    /**
8400
     * @param string $attribute
8401
     *
8402
     * @return mixed|null
8403
     */
8404
    public function getPageConfigurationAttribute($attribute)
8405
    {
8406
        $result = $this->getPageResultConfiguration();
8407
8408
        if (!empty($result)) {
8409
            return $result[$attribute] ?? null;
8410
        }
8411
8412
        return null;
8413
    }
8414
8415
    /**
8416
     * @param bool $showPreviousButton
8417
     *
8418
     * @return Exercise
8419
     */
8420
    public function setShowPreviousButton($showPreviousButton)
8421
    {
8422
        $this->showPreviousButton = $showPreviousButton;
8423
8424
        return $this;
8425
    }
8426
8427
    /**
8428
     * @param array $notifications
8429
     */
8430
    public function setNotifications($notifications)
8431
    {
8432
        $this->notifications = $notifications;
8433
    }
8434
8435
    /**
8436
     * @return array
8437
     */
8438
    public function getNotifications()
8439
    {
8440
        return $this->notifications;
8441
    }
8442
8443
    /**
8444
     * @return bool
8445
     */
8446
    public function showExpectedChoice()
8447
    {
8448
        return ('true' === api_get_setting('exercise.show_exercise_expected_choice'));
8449
    }
8450
8451
    /**
8452
     * @return bool
8453
     */
8454
    public function showExpectedChoiceColumn()
8455
    {
8456
        if (true === $this->forceShowExpectedChoiceColumn) {
8457
            return true;
8458
        }
8459
        if ($this->hideExpectedAnswer) {
8460
            return false;
8461
        }
8462
        if (!in_array(
8463
            $this->results_disabled,
8464
            [
8465
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
8466
            ]
8467
        )
8468
        ) {
8469
            $hide = (int) $this->getPageConfigurationAttribute('hide_expected_answer');
8470
            if (1 === $hide) {
8471
                return false;
8472
            }
8473
8474
            return true;
8475
        }
8476
8477
        return false;
8478
    }
8479
8480
    public function getQuestionRibbon(string $class, string $scoreLabel, ?string $result, array $array): string
8481
    {
8482
        $hide = (int) $this->getPageConfigurationAttribute('hide_question_score');
8483
        if (1 === $hide) {
8484
            return '';
8485
        }
8486
8487
        $ribbon = '<div class="question-answer-result__header-ribbon-title question-answer-result__header-ribbon-title--'.$class.'">'.$scoreLabel.'</div>';
8488
        if (!empty($result)) {
8489
            $ribbon .= '<div class="question-answer-result__header-ribbon-detail">'
8490
                .get_lang('Score').': '.$result
8491
                .'</div>';
8492
        }
8493
8494
        $ribbonClassModifier = '';
8495
8496
        if ($this->showExpectedChoice()) {
8497
            $hideLabel = ('true' === api_get_setting('exercise.exercise_hide_label'));
8498
            if (true === $hideLabel) {
8499
                $ribbonClassModifier = 'question-answer-result__header-ribbon--no-ribbon';
8500
                $html = '';
8501
                $answerUsed = (int) $array['used'];
8502
                $answerMissing = (int) $array['missing'] - $answerUsed;
8503
                for ($i = 1; $i <= $answerUsed; $i++) {
8504
                    $html .= Display::getMdiIcon(StateIcon::COMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL);
8505
                }
8506
                for ($i = 1; $i <= $answerMissing; $i++) {
8507
                    $html .= Display::getMdiIcon(StateIcon::INCOMPLETE, 'ch-tool-icon', null, ICON_SIZE_SMALL);
8508
                }
8509
                $ribbon = '<div class="question-answer-result__header-ribbon-title hide-label-title">'
8510
                    .get_lang('Correct answers').': '.$result.'</div>'
8511
                    .'<div class="question-answer-result__header-ribbon-detail">'.$html.'</div>';
8512
            }
8513
        }
8514
8515
        return Display::div(
8516
            $ribbon,
8517
            ['class' => "question-answer-result__header-ribbon $ribbonClassModifier"]
8518
        );
8519
    }
8520
8521
    /**
8522
     * @return int
8523
     */
8524
    public function getAutoLaunch()
8525
    {
8526
        return $this->autolaunch;
8527
    }
8528
8529
    /**
8530
     * Clean auto launch settings for all exercise in course/course-session.
8531
     */
8532
    public function enableAutoLaunch()
8533
    {
8534
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8535
        $sql = "UPDATE $table SET autolaunch = 1
8536
                WHERE iid = ".$this->iId;
8537
        Database::query($sql);
8538
    }
8539
8540
    /**
8541
     * Clean auto launch settings for all exercise in course/course-session.
8542
     */
8543
    public function cleanCourseLaunchSettings()
8544
    {
8545
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8546
        $sql = "UPDATE $table SET autolaunch = 0
8547
                WHERE c_id = ".$this->course_id.' AND session_id = '.$this->sessionId;
8548
        Database::query($sql);
8549
    }
8550
8551
    /**
8552
     * Get the title without HTML tags.
8553
     *
8554
     * @return string
8555
     */
8556
    public function getUnformattedTitle()
8557
    {
8558
        return strip_tags(api_html_entity_decode($this->title));
8559
    }
8560
8561
    /**
8562
     * Get the question IDs from quiz_rel_question for the current quiz,
8563
     * using the parameters as the arguments to the SQL's LIMIT clause.
8564
     * Because the exercise_id is known, it also comes with a filter on
8565
     * the session, so sessions are not specified here.
8566
     *
8567
     * @param int $start  At which question do we want to start the list
8568
     * @param int $length Up to how many results we want
8569
     *
8570
     * @return array A list of question IDs
8571
     */
8572
    public function getQuestionForTeacher($start = 0, $length = 10)
8573
    {
8574
        $start = (int) $start;
8575
        if ($start < 0) {
8576
            $start = 0;
8577
        }
8578
8579
        $length = (int) $length;
8580
8581
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8582
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
8583
        $sql = "SELECT DISTINCT e.question_id
8584
                FROM $quizRelQuestion e
8585
                INNER JOIN $question q
8586
                ON (e.question_id = q.iid)
8587
                WHERE
8588
8589
                    e.quiz_id = '".$this->getId()."'
8590
                ORDER BY question_order
8591
                LIMIT $start, $length
8592
            ";
8593
        $result = Database::query($sql);
8594
        $questionList = [];
8595
        while ($object = Database::fetch_object($result)) {
8596
            $questionList[] = $object->question_id;
8597
        }
8598
8599
        return $questionList;
8600
    }
8601
8602
    /**
8603
     * @param int   $exerciseId
8604
     * @param array $courseInfo
8605
     * @param int   $sessionId
8606
     *
8607
     * @return bool
8608
     */
8609
    public function generateStats($exerciseId, $courseInfo, $sessionId)
8610
    {
8611
        $allowStats = ('true' === api_get_setting('gradebook.allow_gradebook_stats'));
8612
        if (!$allowStats) {
8613
            return false;
8614
        }
8615
8616
        if (empty($courseInfo)) {
8617
            return false;
8618
        }
8619
8620
        $courseId = $courseInfo['real_id'];
8621
8622
        $sessionId = (int) $sessionId;
8623
        $exerciseId = (int) $exerciseId;
8624
8625
        $result = $this->read($exerciseId);
8626
8627
        if (empty($result)) {
8628
            api_not_allowed(true);
8629
        }
8630
8631
        $statusToFilter = empty($sessionId) ? STUDENT : 0;
8632
8633
        $studentList = CourseManager::get_user_list_from_course_code(
8634
            $courseInfo['code'],
8635
            $sessionId,
8636
            null,
8637
            null,
8638
            $statusToFilter
8639
        );
8640
8641
        if (empty($studentList)) {
8642
            Display::addFlash(Display::return_message(get_lang('No users in course')));
8643
            header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
8644
            exit;
8645
        }
8646
8647
        $tblStats = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8648
8649
        $studentIdList = [];
8650
        if (!empty($studentList)) {
8651
            $studentIdList = array_column($studentList, 'user_id');
8652
        }
8653
8654
        $sessionCondition = api_get_session_condition($sessionId);
8655
        if (false == $this->exercise_was_added_in_lp) {
8656
            $sql = "SELECT * FROM $tblStats
8657
                        WHERE
8658
                            exe_exo_id = $exerciseId AND
8659
                            orig_lp_id = 0 AND
8660
                            orig_lp_item_id = 0 AND
8661
                            status <> 'incomplete' AND
8662
                            c_id = $courseId
8663
                            $sessionCondition
8664
                        ";
8665
        } else {
8666
            $lpId = null;
8667
            if (!empty($this->lpList)) {
8668
                // Taking only the first LP
8669
                $lpId = $this->getLpBySession($sessionId);
8670
                $lpId = $lpId['lp_id'];
8671
            }
8672
8673
            $sql = "SELECT *
8674
                        FROM $tblStats
8675
                        WHERE
8676
                            exe_exo_id = $exerciseId AND
8677
                            orig_lp_id = $lpId AND
8678
                            status <> 'incomplete' AND
8679
                            session_id = $sessionId AND
8680
                            c_id = $courseId ";
8681
        }
8682
8683
        $sql .= ' ORDER BY exe_id DESC';
8684
8685
        $studentCount = 0;
8686
        $sum = 0;
8687
        $bestResult = 0;
8688
        $sumResult = 0;
8689
        $result = Database::query($sql);
8690
        while ($data = Database::fetch_assoc($result)) {
8691
            // Only take into account users in the current student list.
8692
            if (!empty($studentIdList)) {
8693
                if (!in_array($data['exe_user_id'], $studentIdList)) {
8694
                    continue;
8695
                }
8696
            }
8697
8698
            if (!isset($students[$data['exe_user_id']])) {
8699
                if (0 != $data['max_score']) {
8700
                    $students[$data['exe_user_id']] = $data['score'];
8701
                    if ($data['score'] > $bestResult) {
8702
                        $bestResult = $data['score'];
8703
                    }
8704
                    $sumResult += $data['score'];
8705
                }
8706
            }
8707
        }
8708
8709
        $count = count($studentList);
8710
        $average = $sumResult / $count;
8711
        $em = Database::getManager();
8712
8713
        $links = AbstractLink::getGradebookLinksFromItem(
8714
            $this->getId(),
8715
            LINK_EXERCISE,
8716
            $courseInfo['real_id'],
8717
            $sessionId
8718
        );
8719
8720
        if (empty($links)) {
8721
            $links = AbstractLink::getGradebookLinksFromItem(
8722
                $this->iId,
8723
                LINK_EXERCISE,
8724
                $courseInfo['real_id'],
8725
                $sessionId
8726
            );
8727
        }
8728
8729
        if (!empty($links)) {
8730
            $repo = $em->getRepository(GradebookLink::class);
8731
8732
            foreach ($links as $link) {
8733
                $linkId = $link['id'];
8734
                /** @var GradebookLink $exerciseLink */
8735
                $exerciseLink = $repo->find($linkId);
8736
                if ($exerciseLink) {
8737
                    $exerciseLink
8738
                        ->setUserScoreList($students)
8739
                        ->setBestScore($bestResult)
8740
                        ->setAverageScore($average)
8741
                        ->setScoreWeight($this->getMaxScore());
8742
                    $em->persist($exerciseLink);
8743
                    $em->flush();
8744
                }
8745
            }
8746
        }
8747
    }
8748
8749
    /**
8750
     * Return an HTML table of exercises for on-screen printing, including
8751
     * action icons. If no exercise is present and the user can edit the
8752
     * course, show a "create test" button.
8753
     *
8754
     * @param int    $categoryId
8755
     * @param string $keyword
8756
     * @param int    $userId
8757
     * @param int    $courseId
8758
     * @param int    $sessionId
8759
     * @param bool   $returnData
8760
     * @param int    $minCategoriesInExercise
8761
     * @param int    $filterByResultDisabled
8762
     * @param int    $filterByAttempt
8763
     *
8764
     * @return string|SortableTableFromArrayConfig
8765
     */
8766
    public static function exerciseGridResource(
8767
        $categoryId,
8768
        $keyword = '',
8769
        $userId = 0,
8770
        $courseId = 0,
8771
        $sessionId = 0,
8772
        $returnData = false,
8773
        $minCategoriesInExercise = 0,
8774
        $filterByResultDisabled = 0,
8775
        $filterByAttempt = 0,
8776
        $myActions = null,
8777
        $returnTable = false
8778
    ) {
8779
        $is_allowedToEdit = api_is_allowed_to_edit(null, true);
8780
        $courseId = $courseId ? (int) $courseId : api_get_course_int_id();
8781
        $sessionId = $sessionId ? (int) $sessionId : api_get_session_id();
8782
8783
        $course = api_get_course_entity($courseId);
8784
        $session = api_get_session_entity($sessionId);
8785
8786
        $userId = $userId ? (int) $userId : api_get_user_id();
8787
        $user = api_get_user_entity($userId);
8788
8789
        $repo = Container::getQuizRepository();
8790
8791
        // 2. Get query builder from repo.
8792
        $qb = $repo->getResourcesByCourse($course, $session);
8793
8794
        if (!empty($categoryId)) {
8795
            $qb->andWhere($qb->expr()->eq('resource.quizCategory', $categoryId));
8796
        } else {
8797
            $qb->andWhere($qb->expr()->isNull('resource.quizCategory'));
8798
        }
8799
8800
        $allowDelete = self::allowAction('delete');
8801
        $allowClean = self::allowAction('clean_results');
8802
8803
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8804
8805
        $categoryId = (int) $categoryId;
8806
        $keyword = Database::escape_string($keyword);
8807
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
8808
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
8809
        $autoLaunchAvailable = false;
8810
        if (1 == api_get_course_setting('enable_exercise_auto_launch') &&
8811
            ('true' === api_get_setting('exercise.allow_exercise_auto_launch'))
8812
        ) {
8813
            $autoLaunchAvailable = true;
8814
        }
8815
8816
        $courseId = $course->getId();
8817
        $tableRows = [];
8818
        $origin = api_get_origin();
8819
        $charset = 'utf-8';
8820
        $token = Security::get_token();
8821
        $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh($userId, ['real_id' => $courseId]);
8822
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
8823
        $content = '';
8824
        $column = 0;
8825
        if ($is_allowedToEdit) {
8826
            $column = 1;
8827
        }
8828
8829
        $table = new SortableTableFromArrayConfig(
8830
            [],
8831
            $column,
8832
            self::PAGINATION_ITEMS_PER_PAGE,
8833
            'exercises_cat_'.$categoryId.'_'.api_get_course_int_id().'_'.api_get_session_id()
8834
        );
8835
8836
        $limit = $table->per_page;
8837
        $page = $table->page_nr;
8838
        $from = $limit * ($page - 1);
8839
8840
        $categoryCondition = '';
8841
        if ('true' === api_get_setting('exercise.allow_exercise_categories')) {
8842
            if (!empty($categoryId)) {
8843
                $categoryCondition = " AND quiz_category_id = $categoryId ";
8844
            } else {
8845
                $categoryCondition = ' AND quiz_category_id IS NULL ';
8846
            }
8847
        }
8848
8849
        if (!empty($keyword)) {
8850
            $qb->andWhere($qb->expr()->like('resource.title', ':keyword'));
8851
            $qb->setParameter('keyword', '%'.$keyword.'%');
8852
        }
8853
8854
        // Only for administrators
8855
        if ($is_allowedToEdit) {
8856
            $qb->andWhere($qb->expr()->neq('resource.active', -1));
8857
        } else {
8858
            $qb->andWhere($qb->expr()->eq('resource.active', 1));
8859
        }
8860
8861
        $qb->setFirstResult($from);
8862
        $qb->setMaxResults($limit);
8863
8864
        $filterByResultDisabledCondition = '';
8865
        $filterByResultDisabled = (int) $filterByResultDisabled;
8866
        if (!empty($filterByResultDisabled)) {
8867
            $filterByResultDisabledCondition = ' AND e.results_disabled = '.$filterByResultDisabled;
8868
        }
8869
        $filterByAttemptCondition = '';
8870
        $filterByAttempt = (int) $filterByAttempt;
8871
        if (!empty($filterByAttempt)) {
8872
            $filterByAttemptCondition = ' AND e.max_attempt = '.$filterByAttempt;
8873
        }
8874
8875
        $exerciseList = $qb->getQuery()->getResult();
8876
8877
        $total = $repo->getCount($qb);
8878
8879
        $webPath = api_get_path(WEB_CODE_PATH);
8880
        if (!empty($exerciseList)) {
8881
            $visibilitySetting = ('true' === api_get_setting('lp.show_hidden_exercise_added_to_lp'));
8882
            //avoid sending empty parameters
8883
            $mylpid = empty($learnpath_id) ? '' : '&learnpath_id='.$learnpath_id;
8884
            $mylpitemid = empty($learnpath_item_id) ? '' : '&learnpath_item_id='.$learnpath_item_id;
8885
8886
            /** @var CQuiz $exerciseEntity */
8887
            foreach ($exerciseList as $exerciseEntity) {
8888
                $currentRow = [];
8889
                $exerciseId = $exerciseEntity->getIid();
8890
                $actions = '';
8891
                $attempt_text = '';
8892
                $exercise = new Exercise($courseId);
8893
                $exercise->read($exerciseId, false);
8894
8895
                if (empty($exercise->iId)) {
8896
                    continue;
8897
                }
8898
8899
                $allowToEditBaseCourse = true;
8900
                $visibility = $visibilityInCourse = $exerciseEntity->isVisible($course);
8901
                $visibilityInSession = false;
8902
                if (!empty($sessionId)) {
8903
                    // If we are in a session, the test is invisible
8904
                    // in the base course, it is included in a LP
8905
                    // *and* the setting to show it is *not*
8906
                    // specifically set to true, then hide it.
8907
                    if (false === $visibility) {
8908
                        if (!$visibilitySetting) {
8909
                            if ($exercise->exercise_was_added_in_lp) {
8910
                                continue;
8911
                            }
8912
                        }
8913
                    }
8914
8915
                    $visibility = $visibilityInSession = $exerciseEntity->isVisible($course, $session);
8916
                }
8917
8918
                // Validation when belongs to a session
8919
                $isBaseCourseExercise = true;
8920
                if (!($visibilityInCourse && $visibilityInSession)) {
8921
                    $isBaseCourseExercise = false;
8922
                }
8923
8924
                if (!empty($sessionId) && $isBaseCourseExercise) {
8925
                    $allowToEditBaseCourse = false;
8926
                }
8927
8928
                $sessionStar = null;
8929
                if (!$isBaseCourseExercise) {
8930
                    $sessionStar = api_get_session_image($sessionId, $user);
8931
                }
8932
8933
                $locked = $exercise->is_gradebook_locked;
8934
8935
                $startTime = $exerciseEntity->getStartTime();
8936
                $endTime = $exerciseEntity->getEndTime();
8937
                $time_limits = false;
8938
                if (!empty($startTime) || !empty($endTime)) {
8939
                    $time_limits = true;
8940
                }
8941
8942
                $is_actived_time = false;
8943
                if ($time_limits) {
8944
                    // check if start time
8945
                    $start_time = false;
8946
                    if (!empty($startTime)) {
8947
                        $start_time = api_strtotime($startTime->format('Y-m-d H:i:s'), 'UTC');
8948
                    }
8949
                    $end_time = false;
8950
                    if (!empty($endTime)) {
8951
                        $end_time = api_strtotime($endTime->format('Y-m-d H:i:s'), 'UTC');
8952
                    }
8953
                    $now = time();
8954
                    //If both "clocks" are enable
8955
                    if ($start_time && $end_time) {
8956
                        if ($now > $start_time && $end_time > $now) {
8957
                            $is_actived_time = true;
8958
                        }
8959
                    } else {
8960
                        //we check the start and end
8961
                        if ($start_time) {
8962
                            if ($now > $start_time) {
8963
                                $is_actived_time = true;
8964
                            }
8965
                        }
8966
                        if ($end_time) {
8967
                            if ($end_time > $now) {
8968
                                $is_actived_time = true;
8969
                            }
8970
                        }
8971
                    }
8972
                }
8973
8974
                // Blocking empty start times see BT#2800
8975
                // @todo replace global
8976
                /*global $_custom;
8977
                if (isset($_custom['exercises_hidden_when_no_start_date']) &&
8978
                    $_custom['exercises_hidden_when_no_start_date']
8979
                ) {
8980
                    if (empty($startTime)) {
8981
                        $time_limits = true;
8982
                        $is_actived_time = false;
8983
                    }
8984
                }*/
8985
8986
                $cut_title = $exercise->getCutTitle();
8987
                $alt_title = '';
8988
                if ($cut_title != $exerciseEntity->getTitle()) {
8989
                    $alt_title = ' title = "'.$exercise->getUnformattedTitle().'" ';
8990
                }
8991
8992
                // Teacher only.
8993
                if ($is_allowedToEdit) {
8994
                    $lp_blocked = null;
8995
                    if (true == $exercise->exercise_was_added_in_lp) {
8996
                        $lp_blocked = Display::div(
8997
                            get_lang(
8998
                                '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.'
8999
                            ),
9000
                            ['class' => 'lp_content_type_label']
9001
                        );
9002
                    }
9003
9004
                    $style = '';
9005
                    if (!$visibility) {
9006
                        $style = 'color:grey';
9007
                    }
9008
9009
                    $title = $cut_title;
9010
9011
                    $url = '<a
9012
                        '.$alt_title.'
9013
                        id="tooltip_'.$exerciseId.'"
9014
                        href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$exerciseId.'"
9015
                        style = "'.$style.';float:left;"
9016
                        >
9017
                         '.Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, ICON_SIZE_SMALL, $title).$title.
9018
                        '</a>';
9019
9020
                    if (ExerciseLib::isQuizEmbeddable($exerciseEntity)) {
9021
                        $embeddableIcon = Display::getMdiIcon('book-music-outline', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('ThisQuizCanBeEmbeddable'));
9022
                        $url .= Display::div($embeddableIcon, ['class' => 'pull-right']);
9023
                    }
9024
9025
                    $currentRow['title'] = $url.' '.$sessionStar.$lp_blocked;
9026
                    $rowi = $exerciseEntity->getQuestions()->count();
9027
9028
                    if ($repo->isGranted('EDIT', $exerciseEntity) && $allowToEditBaseCourse) {
9029
                        // Questions list
9030
                        $actions = Display::url(
9031
                            Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit')),
9032
                            'admin.php?'.api_get_cidreq().'&exerciseId='.$exerciseId
9033
                        );
9034
9035
                        // Test settings
9036
                        $settings = Display::url(
9037
                            Display::getMdiIcon(ToolIcon::SETTINGS, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Configure')),
9038
                            'exercise_admin.php?'.api_get_cidreq().'&exerciseId='.$exerciseId
9039
                        );
9040
9041
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9042
                            $settings = '';
9043
                        }
9044
                        $actions .= $settings;
9045
9046
                        // Exercise results
9047
                        $resultsLink = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9048
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).'</a>';
9049
9050
                        if ($limitTeacherAccess) {
9051
                            if (api_is_platform_admin()) {
9052
                                $actions .= $resultsLink;
9053
                            }
9054
                        } else {
9055
                            // Exercise results
9056
                            $actions .= $resultsLink;
9057
                        }
9058
9059
                        // Auto launch
9060
                        if ($autoLaunchAvailable) {
9061
                            $autoLaunch = $exercise->getAutoLaunch();
9062
                            if (empty($autoLaunch)) {
9063
                                $actions .= Display::url(
9064
                                    Display::getMdiIcon('rocket-launch', 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang('Enable')),
9065
                                    'exercise.php?'.api_get_cidreq(
9066
                                    ).'&action=enable_launch&sec_token='.$token.'&exerciseId='.$exerciseId
9067
                                );
9068
                            } else {
9069
                                $actions .= Display::url(
9070
                                    Display::getMdiIcon('rocket-launch', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Disable')),
9071
                                    'exercise.php?'.api_get_cidreq(
9072
                                    ).'&action=disable_launch&sec_token='.$token.'&exerciseId='.$exerciseId
9073
                                );
9074
                            }
9075
                        }
9076
9077
                        // Export
9078
                        $actions .= Display::url(
9079
                            Display::getMdiIcon('disc', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Copy this exercise as a new one')),
9080
                            '',
9081
                            [
9082
                                'onclick' => "javascript:if(!confirm('".addslashes(
9083
                                        api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES)
9084
                                    )." ".addslashes($title)."?"."')) return false;",
9085
                                'href' => 'exercise.php?'.api_get_cidreq(
9086
                                    ).'&action=copy_exercise&sec_token='.$token.'&exerciseId='.$exerciseId,
9087
                            ]
9088
                        );
9089
9090
                        // Clean exercise
9091
                        $clean = '';
9092
                        if (true === $allowClean) {
9093
                            if (!$locked) {
9094
                                $clean = Display::url(
9095
                                    Display::getMdiIcon(ActionIcon::RESET, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('CleanStudentResults')
9096
                                    ),
9097
                                    '',
9098
                                    [
9099
                                        'onclick' => "javascript:if(!confirm('".
9100
                                            addslashes(
9101
                                                api_htmlentities(
9102
                                                    get_lang('Are you sure to delete results'),
9103
                                                    ENT_QUOTES
9104
                                                )
9105
                                            )." ".addslashes($title)."?"."')) return false;",
9106
                                        'href' => 'exercise.php?'.api_get_cidreq(
9107
                                            ).'&action=clean_results&sec_token='.$token.'&exerciseId='.$exerciseId,
9108
                                    ]
9109
                                );
9110
                            } else {
9111
                                $clean = Display::getMdiIcon(ActionIcon::RESET, 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang('ResourceLockedByGradebook')
9112
                                );
9113
                            }
9114
                        }
9115
9116
                        $actions .= $clean;
9117
                        // Visible / invisible
9118
                        // Check if this exercise was added in a LP
9119
                        if ($exercise->exercise_was_added_in_lp) {
9120
                            $visibility = Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('AddedToLPCannotBeAccessed')
9121
                            );
9122
                        } else {
9123
                            if (!$exerciseEntity->isVisible($course, $session)) {
9124
                                $visibility = Display::url(
9125
                                    Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Activate')
9126
                                    ),
9127
                                    'exercise.php?'.api_get_cidreq(
9128
                                    ).'&action=enable&sec_token='.$token.'&exerciseId='.$exerciseId
9129
                                );
9130
                            } else {
9131
                                // else if not active
9132
                                $visibility = Display::url(
9133
                                    Display::getMdiIcon(StateIcon::PUBLIC_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Deactivate')
9134
                                    ),
9135
                                    'exercise.php?'.api_get_cidreq(
9136
                                    ).'&action=disable&sec_token='.$token.'&exerciseId='.$exerciseId
9137
                                );
9138
                            }
9139
                        }
9140
9141
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9142
                            $visibility = '';
9143
                        }
9144
9145
                        $actions .= $visibility;
9146
9147
                        // Export qti ...
9148
                        $export = Display::url(
9149
                            Display::getMdiIcon(
9150
                                'database',
9151
                                'ch-tool-icon',
9152
                                null,
9153
                                ICON_SIZE_SMALL,
9154
                                'IMS/QTI'
9155
                            ),
9156
                            'exercise.php?action=exportqti2&exerciseId='.$exerciseId.'&'.api_get_cidreq()
9157
                        );
9158
9159
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9160
                            $export = '';
9161
                        }
9162
9163
                        $actions .= $export;
9164
                    } else {
9165
                        // not session
9166
                        $actions = Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, get_lang('ExerciseEditionNotAvailableInSession')
9167
                        );
9168
9169
                        // Check if this exercise was added in a LP
9170
                        if ($exercise->exercise_was_added_in_lp) {
9171
                            $visibility = Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('AddedToLPCannotBeAccessed')
9172
                            );
9173
                        } else {
9174
                            if (0 === $exerciseEntity->getActive() || 0 == $visibility) {
9175
                                $visibility = Display::url(
9176
                                    Display::getMdiIcon(StateIcon::PRIVATE_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Activate')
9177
                                    ),
9178
                                    'exercise.php?'.api_get_cidreq(
9179
                                    ).'&action=enable&sec_token='.$token.'&exerciseId='.$exerciseId
9180
                                );
9181
                            } else {
9182
                                // else if not active
9183
                                $visibility = Display::url(
9184
                                    Display::getMdiIcon(StateIcon::PUBLIC_VISIBILITY, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Deactivate')
9185
                                    ),
9186
                                    'exercise.php?'.api_get_cidreq(
9187
                                    ).'&action=disable&sec_token='.$token.'&exerciseId='.$exerciseId
9188
                                );
9189
                            }
9190
                        }
9191
9192
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9193
                            $visibility = '';
9194
                        }
9195
9196
                        $actions .= $visibility;
9197
                        $actions .= '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9198
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).'</a>';
9199
                        $actions .= Display::url(
9200
                            Display::getMdiIcon('disc', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Copy this exercise as a new one')),
9201
                            '',
9202
                            [
9203
                                'onclick' => "javascript:if(!confirm('".addslashes(
9204
                                        api_htmlentities(get_lang('Are you sure to copy'), ENT_QUOTES)
9205
                                    )." ".addslashes($title)."?"."')) return false;",
9206
                                'href' => 'exercise.php?'.api_get_cidreq(
9207
                                    ).'&action=copy_exercise&sec_token='.$token.'&exerciseId='.$exerciseId,
9208
                            ]
9209
                        );
9210
                    }
9211
9212
                    // Delete
9213
                    $delete = '';
9214
                    if ($repo->isGranted('DELETE', $exerciseEntity) && $allowToEditBaseCourse) {
9215
                        if (!$locked) {
9216
                            $deleteUrl = 'exercise.php?'.api_get_cidreq().'&action=delete&sec_token='.$token.'&exerciseId='.$exerciseId;
9217
                            $delete = Display::url(
9218
                                Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete')),
9219
                                '',
9220
                                [
9221
                                    'onclick' => "javascript:if(!confirm('".
9222
                                        addslashes(api_htmlentities(get_lang('Are you sure to delete?')))." ".
9223
                                        addslashes($exercise->getUnformattedTitle())."?"."')) return false;",
9224
                                    'href' => $deleteUrl,
9225
                                ]
9226
                            );
9227
                        } else {
9228
                            $delete = Display::getMdiIcon(
9229
                                ActionIcon::DELETE,
9230
                                'ch-tool-icon-disabled',
9231
                                null,
9232
                                ICON_SIZE_SMALL,
9233
                                get_lang(
9234
                                    '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.'
9235
                                )
9236
                            );
9237
                        }
9238
                    }
9239
9240
                    if ($limitTeacherAccess && !api_is_platform_admin()) {
9241
                        $delete = '';
9242
                    }
9243
9244
                    if (!empty($minCategoriesInExercise)) {
9245
                        $cats = TestCategory::getListOfCategoriesForTest($exercise);
9246
                        if (!(count($cats) >= $minCategoriesInExercise)) {
9247
                            continue;
9248
                        }
9249
                    }
9250
                    $actions .= $delete;
9251
9252
                    // Number of questions.
9253
                    $random = $exerciseEntity->getRandom();
9254
                    if ($random > 0 || -1 == $random) {
9255
                        // if random == -1 means use random questions with all questions
9256
                        $random_number_of_question = $random;
9257
                        if (-1 == $random_number_of_question) {
9258
                            $random_number_of_question = $rowi;
9259
                        }
9260
                        if ($exerciseEntity->getRandomByCategory() > 0) {
9261
                            $nbQuestionsTotal = TestCategory::getNumberOfQuestionRandomByCategory(
9262
                                $exerciseId,
9263
                                $random_number_of_question
9264
                            );
9265
                            $number_of_questions = $nbQuestionsTotal.' ';
9266
                            $number_of_questions .= ($nbQuestionsTotal > 1) ? get_lang('QuestionsLowerCase') : get_lang(
9267
                                'QuestionLowerCase'
9268
                            );
9269
                            $number_of_questions .= ' - ';
9270
                            $number_of_questions .= min(
9271
                                    TestCategory::getNumberMaxQuestionByCat($exerciseId),
9272
                                    $random_number_of_question
9273
                                ).' '.get_lang('QuestionByCategory');
9274
                        } else {
9275
                            $random_label = ' ('.get_lang('Random').') ';
9276
                            $number_of_questions = $random_number_of_question.' '.$random_label.' / '.$rowi;
9277
                            // Bug if we set a random value bigger than the real number of questions
9278
                            if ($random_number_of_question > $rowi) {
9279
                                $number_of_questions = $rowi.' '.$random_label;
9280
                            }
9281
                        }
9282
                    } else {
9283
                        $number_of_questions = $rowi;
9284
                    }
9285
9286
                    $currentRow['count_questions'] = $number_of_questions;
9287
                } else {
9288
                    // Student only.
9289
                    $visibility = $exerciseEntity->isVisible($course, null);
9290
                    if (false === $visibility && !empty($sessionId)) {
9291
                        $visibility = $exerciseEntity->isVisible($course, $session);
9292
                    }
9293
9294
                    if (false === $visibility) {
9295
                        continue;
9296
                    }
9297
9298
                    $url = '<a '.$alt_title.'
9299
                        href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$exerciseId.'">'.
9300
                        $cut_title.'</a>';
9301
9302
                    // Link of the exercise.
9303
                    $currentRow['title'] = $url.' '.$sessionStar;
9304
                    // This query might be improved later on by ordering by the new "tms" field rather than by exe_id
9305
                    if ($returnData) {
9306
                        $currentRow['title'] = $exercise->getUnformattedTitle();
9307
                    }
9308
9309
                    $sessionCondition = api_get_session_condition(api_get_session_id());
9310
                    // Don't remove this marker: note-query-exe-results
9311
                    $sql = "SELECT * FROM $TBL_TRACK_EXERCISES
9312
                            WHERE
9313
                                exe_exo_id = ".$exerciseId." AND
9314
                                exe_user_id = $userId AND
9315
                                c_id = ".api_get_course_int_id()." AND
9316
                                status <> 'incomplete' AND
9317
                                orig_lp_id = 0 AND
9318
                                orig_lp_item_id = 0
9319
                                $sessionCondition
9320
                            ORDER BY exe_id DESC";
9321
9322
                    $qryres = Database::query($sql);
9323
                    $num = Database:: num_rows($qryres);
9324
9325
                    // Hide the results.
9326
                    $my_result_disabled = $exerciseEntity->getResultsDisabled();
9327
                    $attempt_text = '-';
9328
                    // Time limits are on
9329
                    if ($time_limits) {
9330
                        // Exam is ready to be taken
9331
                        if ($is_actived_time) {
9332
                            // Show results
9333
                            if (
9334
                            in_array(
9335
                                $my_result_disabled,
9336
                                [
9337
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9338
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9339
                                    RESULT_DISABLE_SHOW_SCORE_ONLY,
9340
                                    RESULT_DISABLE_RANKING,
9341
                                ]
9342
                            )
9343
                            ) {
9344
                                // More than one attempt
9345
                                if ($num > 0) {
9346
                                    $row_track = Database:: fetch_array($qryres);
9347
                                    $attempt_text = get_lang('Latest attempt').' : ';
9348
                                    $attempt_text .= ExerciseLib::show_score(
9349
                                        $row_track['score'],
9350
                                        $row_track['max_score']
9351
                                    );
9352
                                } else {
9353
                                    //No attempts
9354
                                    $attempt_text = get_lang('Not attempted');
9355
                                }
9356
                            } else {
9357
                                $attempt_text = '-';
9358
                            }
9359
                        } else {
9360
                            // Quiz not ready due to time limits
9361
                            //@todo use the is_visible function
9362
                            if (!empty($startTime) && !empty($endTime)) {
9363
                                $today = time();
9364
                                if ($today < $start_time) {
9365
                                    $attempt_text = sprintf(
9366
                                        get_lang('ExerciseWillBeActivatedFromXToY'),
9367
                                        api_convert_and_format_date($start_time),
9368
                                        api_convert_and_format_date($end_time)
9369
                                    );
9370
                                } else {
9371
                                    if ($today > $end_time) {
9372
                                        $attempt_text = sprintf(
9373
                                            get_lang('ExerciseWasActivatedFromXToY'),
9374
                                            api_convert_and_format_date($start_time),
9375
                                            api_convert_and_format_date($end_time)
9376
                                        );
9377
                                    }
9378
                                }
9379
                            } else {
9380
                                if (!empty($startTime)) {
9381
                                    $attempt_text = sprintf(
9382
                                        get_lang('ExerciseAvailableFromX'),
9383
                                        api_convert_and_format_date($start_time)
9384
                                    );
9385
                                }
9386
                                if (!empty($endTime)) {
9387
                                    $attempt_text = sprintf(
9388
                                        get_lang('ExerciseAvailableUntilX'),
9389
                                        api_convert_and_format_date($end_time)
9390
                                    );
9391
                                }
9392
                            }
9393
                        }
9394
                    } else {
9395
                        // Normal behaviour.
9396
                        // Show results.
9397
                        if (
9398
                        in_array(
9399
                            $my_result_disabled,
9400
                            [
9401
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9402
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9403
                                RESULT_DISABLE_SHOW_SCORE_ONLY,
9404
                                RESULT_DISABLE_RANKING,
9405
                                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
9406
                            ]
9407
                        )
9408
                        ) {
9409
                            if ($num > 0) {
9410
                                $row_track = Database::fetch_array($qryres);
9411
                                $attempt_text = get_lang('Latest attempt').' : ';
9412
                                $attempt_text .= ExerciseLib::show_score(
9413
                                    $row_track['score'],
9414
                                    $row_track['max_score']
9415
                                );
9416
                            } else {
9417
                                $attempt_text = get_lang('Not attempted');
9418
                            }
9419
                        }
9420
                    }
9421
                    if ($returnData) {
9422
                        $attempt_text = $num;
9423
                    }
9424
                }
9425
9426
                $currentRow['attempt'] = $attempt_text;
9427
9428
                if ($is_allowedToEdit) {
9429
                    $additionalActions = ExerciseLib::getAdditionalTeacherActions($exerciseId);
9430
9431
                    if (!empty($additionalActions)) {
9432
                        $actions .= $additionalActions.PHP_EOL;
9433
                    }
9434
9435
                    if (!empty($myActions) && is_callable($myActions)) {
9436
                        $actions = $myActions($row);
9437
                    }
9438
                    $currentRow = [
9439
                        $exerciseId,
9440
                        $currentRow['title'],
9441
                        $currentRow['count_questions'],
9442
                        $actions,
9443
                    ];
9444
                } else {
9445
                    $currentRow = [
9446
                        $currentRow['title'],
9447
                        $currentRow['attempt'],
9448
                    ];
9449
9450
                    if ($isDrhOfCourse) {
9451
                        $currentRow[] = '<a
9452
                            href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'">'.
9453
                            Display::getMdiIcon(ToolIcon::TRACKING, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Results')).
9454
                            '</a>';
9455
                    }
9456
                    if ($returnData) {
9457
                        $currentRow['id'] = $exercise->id;
9458
                        $currentRow['url'] = $webPath.'exercise/overview.php?'
9459
                            .api_get_cidreq().'&'
9460
                            ."$mylpid$mylpitemid&exerciseId={$exercise->id}";
9461
                        $currentRow['name'] = $currentRow[0];
9462
                    }
9463
                }
9464
                $tableRows[] = $currentRow;
9465
            }
9466
        }
9467
9468
        if (empty($tableRows) && empty($categoryId)) {
9469
            if ($is_allowedToEdit && 'learnpath' !== $origin) {
9470
                if (!empty($_GET['keyword'])) {
9471
                    $content .= Display::return_message(
9472
                        get_lang('No Results for keyword: ').Security::remove_XSS($_GET['keyword']),
9473
                        'warning'
9474
                    );
9475
                } else {
9476
                    $content .= Display::noDataView(
9477
                        get_lang('Quiz'),
9478
                        Display::getMdiIcon(ToolIcon::QUIZ, 'ch-tool-icon', null, ICON_SIZE_BIG),
9479
                        get_lang('Create a new test'),
9480
                        'exercise_admin.php?'.api_get_cidreq()
9481
                    );
9482
                }
9483
            }
9484
        } else {
9485
            if (empty($tableRows)) {
9486
                return '';
9487
            }
9488
            $table->setTableData($tableRows);
9489
            $table->setTotalNumberOfItems($total);
9490
            $table->set_additional_parameters(
9491
                [
9492
                    'cid' => api_get_course_int_id(),
9493
                    'sid' => api_get_session_id(),
9494
                    'category_id' => $categoryId,
9495
                ]
9496
            );
9497
9498
            if ($is_allowedToEdit) {
9499
                $formActions = [];
9500
                $formActions['visible'] = get_lang('Activate');
9501
                $formActions['invisible'] = get_lang('Deactivate');
9502
                $formActions['delete'] = get_lang('Delete');
9503
                $table->set_form_actions($formActions);
9504
            }
9505
9506
            $i = 0;
9507
            if ($is_allowedToEdit) {
9508
                $table->set_header($i++, '', false, 'width="18px"');
9509
            }
9510
            $table->set_header($i++, get_lang('Test name'), false);
9511
9512
            if ($is_allowedToEdit) {
9513
                $table->set_header($i++, get_lang('Questions'), false, [], ['class' => 'text-center']);
9514
                $table->set_header($i++, get_lang('Actions'), false, [], ['class' => 'text-center']);
9515
            } else {
9516
                $table->set_header($i++, get_lang('Status'), false);
9517
                if ($isDrhOfCourse) {
9518
                    $table->set_header($i++, get_lang('Actions'), false, [], ['class' => 'text-center']);
9519
                }
9520
            }
9521
9522
            if ($returnTable) {
9523
                return $table;
9524
            }
9525
            $content .= $table->return_table();
9526
        }
9527
9528
        return $content;
9529
    }
9530
9531
    /**
9532
     * @return int value in minutes
9533
     */
9534
    public function getResultAccess()
9535
    {
9536
        $extraFieldValue = new ExtraFieldValue('exercise');
9537
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9538
            $this->iId,
9539
            'results_available_for_x_minutes'
9540
        );
9541
9542
        if (!empty($value) && isset($value['value'])) {
9543
            return (int) $value['value'];
9544
        }
9545
9546
        return 0;
9547
    }
9548
9549
    /**
9550
     * @param array $exerciseResultInfo
9551
     *
9552
     * @return bool
9553
     */
9554
    public function getResultAccessTimeDiff($exerciseResultInfo)
9555
    {
9556
        $value = $this->getResultAccess();
9557
        if (!empty($value)) {
9558
            $endDate = new DateTime($exerciseResultInfo['exe_date'], new DateTimeZone('UTC'));
9559
            $endDate->add(new DateInterval('PT'.$value.'M'));
9560
            $now = time();
9561
            if ($endDate->getTimestamp() > $now) {
9562
                return (int) $endDate->getTimestamp() - $now;
9563
            }
9564
        }
9565
9566
        return 0;
9567
    }
9568
9569
    /**
9570
     * @param array $exerciseResultInfo
9571
     *
9572
     * @return bool
9573
     */
9574
    public function hasResultsAccess($exerciseResultInfo)
9575
    {
9576
        $diff = $this->getResultAccessTimeDiff($exerciseResultInfo);
9577
        if (0 === $diff) {
9578
            return false;
9579
        }
9580
9581
        return true;
9582
    }
9583
9584
    /**
9585
     * @return int
9586
     */
9587
    public function getResultsAccess()
9588
    {
9589
        $extraFieldValue = new ExtraFieldValue('exercise');
9590
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9591
            $this->iId,
9592
            'results_available_for_x_minutes'
9593
        );
9594
        if (!empty($value)) {
9595
            return (int) $value;
9596
        }
9597
9598
        return 0;
9599
    }
9600
9601
    /**
9602
     * @param int   $questionId
9603
     * @param bool  $show_results
9604
     * @param array $question_result
9605
     */
9606
    public function getDelineationResult(Question $objQuestionTmp, $questionId, $show_results, $question_result)
9607
    {
9608
        $id = (int) $objQuestionTmp->id;
9609
        $questionId = (int) $questionId;
9610
9611
        $final_overlap = $question_result['extra']['final_overlap'];
9612
        $final_missing = $question_result['extra']['final_missing'];
9613
        $final_excess = $question_result['extra']['final_excess'];
9614
9615
        $overlap_color = $question_result['extra']['overlap_color'];
9616
        $missing_color = $question_result['extra']['missing_color'];
9617
        $excess_color = $question_result['extra']['excess_color'];
9618
9619
        $threadhold1 = $question_result['extra']['threadhold1'];
9620
        $threadhold2 = $question_result['extra']['threadhold2'];
9621
        $threadhold3 = $question_result['extra']['threadhold3'];
9622
9623
        if ($show_results) {
9624
            if ($overlap_color) {
9625
                $overlap_color = 'green';
9626
            } else {
9627
                $overlap_color = 'red';
9628
            }
9629
9630
            if ($missing_color) {
9631
                $missing_color = 'green';
9632
            } else {
9633
                $missing_color = 'red';
9634
            }
9635
            if ($excess_color) {
9636
                $excess_color = 'green';
9637
            } else {
9638
                $excess_color = 'red';
9639
            }
9640
9641
            if (!is_numeric($final_overlap)) {
9642
                $final_overlap = 0;
9643
            }
9644
9645
            if (!is_numeric($final_missing)) {
9646
                $final_missing = 0;
9647
            }
9648
            if (!is_numeric($final_excess)) {
9649
                $final_excess = 0;
9650
            }
9651
9652
            if ($final_excess > 100) {
9653
                $final_excess = 100;
9654
            }
9655
9656
            $table_resume = '
9657
                    <table class="table table-hover table-striped data_table">
9658
                        <tr class="row_odd" >
9659
                            <td>&nbsp;</td>
9660
                            <td><b>'.get_lang('Requirements').'</b></td>
9661
                            <td><b>'.get_lang('YourAnswer').'</b></td>
9662
                        </tr>
9663
                        <tr class="row_even">
9664
                            <td><b>'.get_lang('Overlap').'</b></td>
9665
                            <td>'.get_lang('Min').' '.$threadhold1.'</td>
9666
                            <td>
9667
                                <div style="color:'.$overlap_color.'">
9668
                                    '.(($final_overlap < 0) ? 0 : intval($final_overlap)).'
9669
                                </div>
9670
                            </td>
9671
                        </tr>
9672
                        <tr>
9673
                            <td><b>'.get_lang('Excess').'</b></td>
9674
                            <td>'.get_lang('Max').' '.$threadhold2.'</td>
9675
                            <td>
9676
                                <div style="color:'.$excess_color.'">
9677
                                    '.(($final_excess < 0) ? 0 : intval($final_excess)).'
9678
                                </div>
9679
                            </td>
9680
                        </tr>
9681
                        <tr class="row_even">
9682
                            <td><b>'.get_lang('Missing').'</b></td>
9683
                            <td>'.get_lang('Max').' '.$threadhold3.'</td>
9684
                            <td>
9685
                                <div style="color:'.$missing_color.'">
9686
                                    '.(($final_missing < 0) ? 0 : intval($final_missing)).'
9687
                                </div>
9688
                            </td>
9689
                        </tr>
9690
                    </table>
9691
                ';
9692
9693
            $answerType = $objQuestionTmp->selectType();
9694
            /*if ($next == 0) {
9695
                $try = $try_hotspot;
9696
                $lp = $lp_hotspot;
9697
                $destinationid = $select_question_hotspot;
9698
                $url = $url_hotspot;
9699
            } else {
9700
                //show if no error
9701
                $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
9702
                $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
9703
            }
9704
            echo '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>';
9705
            if ($organs_at_risk_hit > 0) {
9706
                $message = '<br />'.get_lang('ResultIs').' <b>'.$result_comment.'</b><br />';
9707
                $message .= '<p style="color:#DC0A0A;"><b>'.get_lang('OARHit').'</b></p>';
9708
            } else {
9709
                $message = '<p>'.get_lang('YourDelineation').'</p>';
9710
                $message .= $table_resume;
9711
                $message .= '<br />'.get_lang('ResultIs').' <b>'.$result_comment.'</b><br />';
9712
            }
9713
            $message .= '<p>'.$comment.'</p>';
9714
            echo $message;*/
9715
9716
            // Showing the score
9717
            /*$queryfree = "SELECT marks FROM $TBL_TRACK_ATTEMPT
9718
                          WHERE exe_id = $id AND question_id =  $questionId";
9719
            $resfree = Database::query($queryfree);
9720
            $questionScore = Database::result($resfree, 0, 'marks');
9721
            $totalScore += $questionScore;*/
9722
            $relPath = api_get_path(REL_CODE_PATH);
9723
            echo '</table></td></tr>';
9724
            echo "
9725
                        <tr>
9726
                            <td colspan=\"2\">
9727
                                <div id=\"hotspot-solution\"></div>
9728
                                <script>
9729
                                    $(function() {
9730
                                        new HotspotQuestion({
9731
                                            questionId: $questionId,
9732
                                            exerciseId: {$this->id},
9733
                                            exeId: $id,
9734
                                            selector: '#hotspot-solution',
9735
                                            for: 'solution',
9736
                                            relPath: '$relPath'
9737
                                        });
9738
                                    });
9739
                                </script>
9740
                            </td>
9741
                        </tr>
9742
                    </table>
9743
                ";
9744
        }
9745
    }
9746
9747
    /**
9748
     * Clean exercise session variables.
9749
     */
9750
    public static function cleanSessionVariables()
9751
    {
9752
        Session::erase('objExercise');
9753
        Session::erase('exe_id');
9754
        Session::erase('calculatedAnswerId');
9755
        Session::erase('duration_time_previous');
9756
        Session::erase('duration_time');
9757
        Session::erase('objQuestion');
9758
        Session::erase('objAnswer');
9759
        Session::erase('questionList');
9760
        Session::erase('categoryList');
9761
        Session::erase('exerciseResult');
9762
        Session::erase('firstTime');
9763
9764
        Session::erase('time_per_question');
9765
        Session::erase('question_start');
9766
        Session::erase('exerciseResultCoordinates');
9767
        Session::erase('hotspot_coord');
9768
        Session::erase('hotspot_dest');
9769
        Session::erase('hotspot_delineation_result');
9770
    }
9771
9772
    /**
9773
     * Get the first LP found matching the session ID.
9774
     *
9775
     * @param int $sessionId
9776
     *
9777
     * @return array
9778
     */
9779
    public function getLpBySession($sessionId)
9780
    {
9781
        if (!empty($this->lpList)) {
9782
            $sessionId = (int) $sessionId;
9783
9784
            foreach ($this->lpList as $lp) {
9785
                if (isset($lp['session_id']) && (int) $lp['session_id'] == $sessionId) {
9786
                    return $lp;
9787
                }
9788
            }
9789
9790
            return current($this->lpList);
9791
        }
9792
9793
        return [
9794
            'lp_id' => 0,
9795
            'max_score' => 0,
9796
            'session_id' => 0,
9797
        ];
9798
    }
9799
9800
    public static function saveExerciseInLp($safe_item_id, $safe_exe_id, $course_id = null)
9801
    {
9802
        $lp = Session::read('oLP');
9803
9804
        $safe_exe_id = (int) $safe_exe_id;
9805
        $safe_item_id = (int) $safe_item_id;
9806
9807
        if (empty($lp) || empty($safe_exe_id) || empty($safe_item_id)) {
9808
            return false;
9809
        }
9810
9811
        $viewId = $lp->get_view_id();
9812
        if (!isset($course_id)) {
9813
            $course_id = api_get_course_int_id();
9814
        }
9815
        $userId = (int) api_get_user_id();
9816
        $viewId = (int) $viewId;
9817
9818
        $TBL_TRACK_EXERCICES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
9819
        $TBL_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
9820
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
9821
9822
        $sql = "SELECT start_date, exe_date, score, max_score, exe_exo_id, exe_duration
9823
                FROM $TBL_TRACK_EXERCICES
9824
                WHERE exe_id = $safe_exe_id AND exe_user_id = $userId";
9825
        $res = Database::query($sql);
9826
        $row_dates = Database::fetch_array($res);
9827
9828
        if (empty($row_dates)) {
9829
            return false;
9830
        }
9831
9832
        $duration = (int) $row_dates['exe_duration'];
9833
        $score = (float) $row_dates['score'];
9834
        $max_score = (float) $row_dates['max_score'];
9835
9836
        $sql = "UPDATE $TBL_LP_ITEM SET
9837
                    max_score = '$max_score'
9838
                WHERE iid = $safe_item_id";
9839
        Database::query($sql);
9840
9841
        $sql = "SELECT iid FROM $TBL_LP_ITEM_VIEW
9842
                WHERE
9843
                    lp_item_id = $safe_item_id AND
9844
                    lp_view_id = $viewId
9845
                ORDER BY iid DESC
9846
                LIMIT 1";
9847
        $res_last_attempt = Database::query($sql);
9848
9849
        if (Database::num_rows($res_last_attempt) && !api_is_invitee()) {
9850
            $row_last_attempt = Database::fetch_row($res_last_attempt);
9851
            $lp_item_view_id = $row_last_attempt[0];
9852
9853
            $exercise = new Exercise($course_id);
9854
            $exercise->read($row_dates['exe_exo_id']);
9855
            $status = 'completed';
9856
9857
            if (!empty($exercise->pass_percentage)) {
9858
                $status = 'failed';
9859
                $success = ExerciseLib::isSuccessExerciseResult(
9860
                    $score,
9861
                    $max_score,
9862
                    $exercise->pass_percentage
9863
                );
9864
                if ($success) {
9865
                    $status = 'passed';
9866
                }
9867
            }
9868
9869
            $sql = "UPDATE $TBL_LP_ITEM_VIEW SET
9870
                        status = '$status',
9871
                        score = '$score',
9872
                        total_time = '$duration'
9873
                    WHERE iid = $lp_item_view_id";
9874
            Database::query($sql);
9875
9876
            $sql = "UPDATE $TBL_TRACK_EXERCICES SET
9877
                        orig_lp_item_view_id = '$lp_item_view_id'
9878
                    WHERE exe_id = ".$safe_exe_id;
9879
            Database::query($sql);
9880
        }
9881
    }
9882
9883
    /**
9884
     * Get the user answers saved in exercise.
9885
     *
9886
     * @param int $attemptId
9887
     *
9888
     * @return array
9889
     */
9890
    public function getUserAnswersSavedInExercise($attemptId)
9891
    {
9892
        $exerciseResult = [];
9893
9894
        $attemptList = Event::getAllExerciseEventByExeId($attemptId);
9895
9896
        foreach ($attemptList as $questionId => $options) {
9897
            foreach ($options as $option) {
9898
                $question = Question::read($option['question_id']);
9899
9900
                if ($question) {
9901
                    switch ($question->type) {
9902
                        case FILL_IN_BLANKS:
9903
                            $option['answer'] = $this->fill_in_blank_answer_to_string($option['answer']);
9904
                            break;
9905
                    }
9906
                }
9907
9908
                if (!empty($option['answer'])) {
9909
                    $exerciseResult[] = $questionId;
9910
9911
                    break;
9912
                }
9913
            }
9914
        }
9915
9916
        return $exerciseResult;
9917
    }
9918
9919
    /**
9920
     * Get the number of user answers saved in exercise.
9921
     *
9922
     * @param int $attemptId
9923
     *
9924
     * @return int
9925
     */
9926
    public function countUserAnswersSavedInExercise($attemptId)
9927
    {
9928
        $answers = $this->getUserAnswersSavedInExercise($attemptId);
9929
9930
        return count($answers);
9931
    }
9932
9933
    public static function allowAction($action)
9934
    {
9935
        if (api_is_platform_admin()) {
9936
            return true;
9937
        }
9938
9939
        $limitTeacherAccess = ('true' === api_get_setting('exercise.limit_exercise_teacher_access'));
9940
        $disableClean = ('true' === api_get_setting('exercise.disable_clean_exercise_results_for_teachers'));
9941
9942
        switch ($action) {
9943
            case 'delete':
9944
                if (api_is_allowed_to_edit(null, true)) {
9945
                    if ($limitTeacherAccess) {
9946
                        return false;
9947
                    }
9948
9949
                    return true;
9950
                }
9951
                break;
9952
            case 'clean_results':
9953
                if (api_is_allowed_to_edit(null, true)) {
9954
                    if ($limitTeacherAccess) {
9955
                        return false;
9956
                    }
9957
9958
                    if ($disableClean) {
9959
                        return false;
9960
                    }
9961
9962
                    return true;
9963
                }
9964
9965
                break;
9966
        }
9967
9968
        return false;
9969
    }
9970
9971
    public static function getLpListFromExercise($exerciseId, $courseId)
9972
    {
9973
        $tableLpItem = Database::get_course_table(TABLE_LP_ITEM);
9974
        $tblLp = Database::get_course_table(TABLE_LP_MAIN);
9975
9976
        $exerciseId = (int) $exerciseId;
9977
        $courseId = (int) $courseId;
9978
9979
        $sql = "SELECT
9980
                    lp.title,
9981
                    lpi.lp_id,
9982
                    lpi.max_score
9983
                FROM $tableLpItem lpi
9984
                INNER JOIN $tblLp lp
9985
                ON (lpi.lp_id = lp.iid)
9986
                WHERE
9987
                    lpi.item_type = '".TOOL_QUIZ."' AND
9988
                    lpi.path = '$exerciseId'";
9989
        $result = Database::query($sql);
9990
        $lpList = [];
9991
        if (Database::num_rows($result) > 0) {
9992
            $lpList = Database::store_result($result, 'ASSOC');
9993
        }
9994
9995
        return $lpList;
9996
    }
9997
9998
    public function getReminderTable($questionList, $exercise_stat_info, $disableCheckBoxes = false)
9999
    {
10000
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10001
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10002
        $learnpath_item_view_id = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10003
        $categoryId = isset($_REQUEST['category_id']) ? (int) $_REQUEST['category_id'] : 0;
10004
10005
        if (empty($exercise_stat_info)) {
10006
            return '';
10007
        }
10008
10009
        $remindList = $exercise_stat_info['questions_to_check'];
10010
        $remindList = explode(',', $remindList);
10011
10012
        $exeId = $exercise_stat_info['exe_id'];
10013
        $exerciseId = $exercise_stat_info['exe_exo_id'];
10014
        $exercise_result = $this->getUserAnswersSavedInExercise($exeId);
10015
10016
        $content = Display::label(get_lang('QuestionWithNoAnswer'), 'danger');
10017
        $content .= '<div class="clear"></div><br />';
10018
        $table = '';
10019
        $counter = 0;
10020
        // Loop over all question to show results for each of them, one by one
10021
        foreach ($questionList as $questionId) {
10022
            $objQuestionTmp = Question::read($questionId);
10023
            $check_id = 'remind_list['.$questionId.']';
10024
            $attributes = [
10025
                'id' => $check_id,
10026
                'onclick' => "save_remind_item(this, '$questionId');",
10027
                'data-question-id' => $questionId,
10028
            ];
10029
            if (in_array($questionId, $remindList)) {
10030
                $attributes['checked'] = 1;
10031
            }
10032
10033
            $checkbox = Display::input('checkbox', 'remind_list['.$questionId.']', '', $attributes);
10034
            $checkbox = '<div class="pretty p-svg p-curve">
10035
                        '.$checkbox.'
10036
                        <div class="state p-primary ">
10037
                         <svg class="svg svg-icon" viewBox="0 0 20 20">
10038
                            <path d="M7.629,14.566c0.125,0.125,0.291,0.188,0.456,0.188c0.164,0,0.329-0.062,0.456-0.188l8.219-8.221c0.252-0.252,0.252-0.659,0-0.911c-0.252-0.252-0.659-0.252-0.911,0l-7.764,7.763L4.152,9.267c-0.252-0.251-0.66-0.251-0.911,0c-0.252,0.252-0.252,0.66,0,0.911L7.629,14.566z" style="stroke: white;fill:white;"></path>
10039
                         </svg>
10040
                         <label>&nbsp;</label>
10041
                        </div>
10042
                    </div>';
10043
            $counter++;
10044
            $questionTitle = $counter.'. '.strip_tags($objQuestionTmp->selectTitle());
10045
            // Check if the question doesn't have an answer.
10046
            if (!in_array($questionId, $exercise_result)) {
10047
                $questionTitle = Display::label($questionTitle, 'danger');
10048
            }
10049
10050
            $label_attributes = [];
10051
            $label_attributes['for'] = $check_id;
10052
            if (false === $disableCheckBoxes) {
10053
                $questionTitle = Display::tag('label', $checkbox.$questionTitle, $label_attributes);
10054
            }
10055
            $table .= Display::div($questionTitle, ['class' => 'exercise_reminder_item ']);
10056
        }
10057
10058
        $content .= Display::div('', ['id' => 'message']).
10059
            Display::div($table, ['class' => 'question-check-test']);
10060
10061
        $content .= '<script>
10062
        var lp_data = $.param({
10063
            "learnpath_id": '.$learnpath_id.',
10064
            "learnpath_item_id" : '.$learnpath_item_id.',
10065
            "learnpath_item_view_id": '.$learnpath_item_view_id.'
10066
        });
10067
10068
        function final_submit() {
10069
            // Normal inputs.
10070
            window.location = "'.api_get_path(WEB_CODE_PATH).'exercise/exercise_result.php?'.api_get_cidreq().'&exe_id='.$exeId.'&" + lp_data;
10071
        }
10072
10073
        function selectAll() {
10074
            $("input[type=checkbox]").each(function () {
10075
                $(this).prop("checked", 1);
10076
                var question_id = $(this).data("question-id");
10077
                var action = "add";
10078
                $.ajax({
10079
                    url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10080
                    data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
10081
                    success: function(returnValue) {
10082
                    }
10083
                });
10084
            });
10085
        }
10086
10087
        function changeOptionStatus(status)
10088
        {
10089
            $("input[type=checkbox]").each(function () {
10090
                $(this).prop("checked", status);
10091
            });
10092
10093
            var action = "";
10094
            var option = "remove_all";
10095
            if (status == 1) {
10096
                option = "add_all";
10097
            }
10098
            $.ajax({
10099
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10100
                data: "option="+option+"&exe_id='.$exeId.'&action="+action,
10101
                success: function(returnValue) {
10102
                }
10103
            });
10104
        }
10105
10106
        function reviewQuestions() {
10107
            var isChecked = 1;
10108
            $("input[type=checkbox]").each(function () {
10109
                if ($(this).prop("checked")) {
10110
                    isChecked = 2;
10111
                    return false;
10112
                }
10113
            });
10114
10115
            if (isChecked == 1) {
10116
                $("#message").addClass("warning-message");
10117
                $("#message").html("'.addslashes(get_lang('SelectAQuestionToReview')).'");
10118
            } else {
10119
                window.location = "exercise_submit.php?'.api_get_cidreq().'&category_id='.$categoryId.'&exerciseId='.$exerciseId.'&reminder=2&" + lp_data;
10120
            }
10121
        }
10122
10123
        function save_remind_item(obj, question_id) {
10124
            var action = "";
10125
            if ($(obj).prop("checked")) {
10126
                action = "add";
10127
            } else {
10128
                action = "delete";
10129
            }
10130
            $.ajax({
10131
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10132
                data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
10133
                success: function(returnValue) {
10134
                }
10135
            });
10136
        }
10137
        </script>';
10138
10139
        return $content;
10140
    }
10141
10142
    public function getRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10143
    {
10144
        $dataSet = [];
10145
        $labels = [];
10146
        $labelsWithId = [];
10147
        /** @var Exercise $exercise */
10148
        foreach ($exercises as $exercise) {
10149
            if (empty($labels)) {
10150
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10151
                if (!empty($categoryNameList)) {
10152
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10153
                    asort($labelsWithId);
10154
                    $labels = array_values($labelsWithId);
10155
                }
10156
            }
10157
10158
            foreach ($userList as $userId) {
10159
                $results = Event::getExerciseResultsByUser(
10160
                    $userId,
10161
                    $exercise->iId,
10162
                    $courseId,
10163
                    $sessionId
10164
                );
10165
10166
                if ($results) {
10167
                    $firstAttempt = end($results);
10168
                    $exeId = $firstAttempt['exe_id'];
10169
10170
                    ob_start();
10171
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10172
                        $exercise,
10173
                        $exeId,
10174
                        false
10175
                    );
10176
                    ob_end_clean();
10177
10178
                    $categoryList = $stats['category_list'];
10179
                    $tempResult = [];
10180
                    foreach ($labelsWithId as $category_id => $title) {
10181
                        if (isset($categoryList[$category_id])) {
10182
                            $category_item = $categoryList[$category_id];
10183
                            $tempResult[] = round($category_item['score'] / $category_item['total'] * 10);
10184
                        } else {
10185
                            $tempResult[] = 0;
10186
                        }
10187
                    }
10188
                    $dataSet[] = $tempResult;
10189
                }
10190
            }
10191
        }
10192
10193
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10194
    }
10195
10196
    public function getAverageRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10197
    {
10198
        $dataSet = [];
10199
        $labels = [];
10200
        $labelsWithId = [];
10201
10202
        $tempResult = [];
10203
        /** @var Exercise $exercise */
10204
        foreach ($exercises as $exercise) {
10205
            $exerciseId = $exercise->iId;
10206
            if (empty($labels)) {
10207
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10208
                if (!empty($categoryNameList)) {
10209
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10210
                    asort($labelsWithId);
10211
                    $labels = array_values($labelsWithId);
10212
                }
10213
            }
10214
10215
            foreach ($userList as $userId) {
10216
                $results = Event::getExerciseResultsByUser(
10217
                    $userId,
10218
                    $exerciseId,
10219
                    $courseId,
10220
                    $sessionId
10221
                );
10222
10223
                if ($results) {
10224
                    $firstAttempt = end($results);
10225
                    $exeId = $firstAttempt['exe_id'];
10226
10227
                    ob_start();
10228
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10229
                        $exercise,
10230
                        $exeId,
10231
                        false
10232
                    );
10233
                    ob_end_clean();
10234
10235
                    $categoryList = $stats['category_list'];
10236
                    foreach ($labelsWithId as $category_id => $title) {
10237
                        if (isset($categoryList[$category_id])) {
10238
                            $category_item = $categoryList[$category_id];
10239
                            if (!isset($tempResult[$exerciseId][$category_id])) {
10240
                                $tempResult[$exerciseId][$category_id] = 0;
10241
                            }
10242
                            $tempResult[$exerciseId][$category_id] += $category_item['score'] / $category_item['total'] * 10;
10243
                        }
10244
                    }
10245
                }
10246
            }
10247
        }
10248
10249
        $totalUsers = count($userList);
10250
10251
        foreach ($exercises as $exercise) {
10252
            $exerciseId = $exercise->iId;
10253
            $data = [];
10254
            foreach ($labelsWithId as $category_id => $title) {
10255
                if (isset($tempResult[$exerciseId]) && isset($tempResult[$exerciseId][$category_id])) {
10256
                    $data[] = round($tempResult[$exerciseId][$category_id] / $totalUsers);
10257
                } else {
10258
                    $data[] = 0;
10259
                }
10260
            }
10261
            $dataSet[] = $data;
10262
        }
10263
10264
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10265
    }
10266
10267
    public function getRadar($labels, $dataSet, $dataSetLabels = [])
10268
    {
10269
        if (empty($labels) || empty($dataSet)) {
10270
            return '';
10271
        }
10272
10273
        $displayLegend = 0;
10274
        if (!empty($dataSetLabels)) {
10275
            $displayLegend = 1;
10276
        }
10277
10278
        $labels = json_encode($labels);
10279
10280
        $colorList = ChamiloApi::getColorPalette(true, true);
10281
10282
        $dataSetToJson = [];
10283
        $counter = 0;
10284
        foreach ($dataSet as $index => $resultsArray) {
10285
            $color = isset($colorList[$counter]) ? $colorList[$counter] : 'rgb('.rand(0, 255).', '.rand(0, 255).', '.rand(0, 255).', 1.0)';
10286
10287
            $label = isset($dataSetLabels[$index]) ? $dataSetLabels[$index] : '';
10288
            $background = str_replace('1.0', '0.2', $color);
10289
            $dataSetToJson[] = [
10290
                'fill' => false,
10291
                'label' => $label,
10292
                'backgroundColor' => $background,
10293
                'borderColor' => $color,
10294
                'pointBackgroundColor' => $color,
10295
                'pointBorderColor' => '#fff',
10296
                'pointHoverBackgroundColor' => '#fff',
10297
                'pointHoverBorderColor' => $color,
10298
                'pointRadius' => 6,
10299
                'pointBorderWidth' => 3,
10300
                'pointHoverRadius' => 10,
10301
                'data' => $resultsArray,
10302
            ];
10303
            $counter++;
10304
        }
10305
        $resultsToJson = json_encode($dataSetToJson);
10306
10307
        return "
10308
                <canvas id='categoryRadar' height='200'></canvas>
10309
                <script>
10310
                    var data = {
10311
                        labels: $labels,
10312
                        datasets: $resultsToJson
10313
                    }
10314
                    var options = {
10315
                        responsive: true,
10316
                        scale: {
10317
                            angleLines: {
10318
                                display: false
10319
                            },
10320
                            ticks: {
10321
                                beginAtZero: true,
10322
                                  min: 0,
10323
                                  max: 10,
10324
                                stepSize: 1,
10325
                            },
10326
                            pointLabels: {
10327
                              fontSize: 14,
10328
                              //fontStyle: 'bold'
10329
                            },
10330
                        },
10331
                        elements: {
10332
                            line: {
10333
                                tension: 0,
10334
                                borderWidth: 3
10335
                            }
10336
                        },
10337
                        legend: {
10338
                            //position: 'bottom'
10339
                            display: $displayLegend
10340
                        },
10341
                        animation: {
10342
                            animateScale: true,
10343
                            animateRotate: true
10344
                        },
10345
                    };
10346
                    var ctx = document.getElementById('categoryRadar').getContext('2d');
10347
                    var myRadarChart = new Chart(ctx, {
10348
                        type: 'radar',
10349
                        data: data,
10350
                        options: options
10351
                    });
10352
                </script>
10353
                ";
10354
    }
10355
10356
    /**
10357
     * Returns true if the exercise is locked by percentage. an exercise attempt must be passed.
10358
     */
10359
    public function isBlockedByPercentage(array $attempt = []): bool
10360
    {
10361
        if (empty($attempt)) {
10362
            return false;
10363
        }
10364
        $extraFieldValue = new ExtraFieldValue('exercise');
10365
        $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
10366
            $this->iId,
10367
            'blocking_percentage'
10368
        );
10369
10370
        if (empty($blockExercise['value'])) {
10371
            return false;
10372
        }
10373
10374
        $blockPercentage = (int) $blockExercise['value'];
10375
10376
        if (0 === $blockPercentage) {
10377
            return false;
10378
        }
10379
10380
        $resultPercentage = 0;
10381
10382
        if (isset($attempt['score']) && isset($attempt['max_score'])) {
10383
            $weight = (int) $attempt['max_score'];
10384
            $weight = (0 == $weight) ? 1 : $weight;
10385
            $resultPercentage = float_format(
10386
                ($attempt['score'] / $weight) * 100,
10387
                1
10388
            );
10389
        }
10390
        if ($resultPercentage <= $blockPercentage) {
10391
            return true;
10392
        }
10393
10394
        return false;
10395
    }
10396
10397
    /**
10398
     * Gets the question list ordered by the question_order setting (drag and drop).
10399
     *
10400
     * @param bool $adminView Optional.
10401
     *
10402
     * @return array
10403
     */
10404
    public function getQuestionOrderedList($adminView = false)
10405
    {
10406
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
10407
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
10408
10409
        // Getting question_order to verify that the question
10410
        // list is correct and all question_order's were set
10411
        $sql = "SELECT DISTINCT count(e.question_order) as count
10412
                FROM $TBL_EXERCICE_QUESTION e
10413
                INNER JOIN $TBL_QUESTIONS q
10414
                ON (e.question_id = q.iid)
10415
                WHERE
10416
                  e.quiz_id	= ".$this->getId();
10417
10418
        $result = Database::query($sql);
10419
        $row = Database::fetch_array($result);
10420
        $count_question_orders = $row['count'];
10421
10422
        // Getting question list from the order (question list drag n drop interface).
10423
        $sql = "SELECT DISTINCT e.question_id, e.question_order
10424
                FROM $TBL_EXERCICE_QUESTION e
10425
                INNER JOIN $TBL_QUESTIONS q
10426
                ON (e.question_id = q.iid)
10427
                WHERE
10428
10429
                    e.quiz_id = '".$this->getId()."'
10430
                ORDER BY question_order";
10431
        $result = Database::query($sql);
10432
10433
        // Fills the array with the question ID for this exercise
10434
        // the key of the array is the question position
10435
        $temp_question_list = [];
10436
        $counter = 1;
10437
        $questionList = [];
10438
        while ($new_object = Database::fetch_object($result)) {
10439
            if (!$adminView) {
10440
                // Correct order.
10441
                $questionList[$new_object->question_order] = $new_object->question_id;
10442
            } else {
10443
                $questionList[$counter] = $new_object->question_id;
10444
            }
10445
10446
            // Just in case we save the order in other array
10447
            $temp_question_list[$counter] = $new_object->question_id;
10448
            $counter++;
10449
        }
10450
10451
        if (!empty($temp_question_list)) {
10452
            /* If both array don't match it means that question_order was not correctly set
10453
               for all questions using the default mysql order */
10454
            if (count($temp_question_list) != $count_question_orders) {
10455
                $questionList = $temp_question_list;
10456
            }
10457
        }
10458
10459
        return $questionList;
10460
    }
10461
10462
    /**
10463
     * Get number of questions in exercise by user attempt.
10464
     *
10465
     * @return int
10466
     */
10467
    private function countQuestionsInExercise()
10468
    {
10469
        $lpId = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10470
        $lpItemId = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10471
        $lpItemViewId = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10472
10473
        $trackInfo = $this->get_stat_track_exercise_info($lpId, $lpItemId, $lpItemViewId);
10474
10475
        if (!empty($trackInfo)) {
10476
            $questionIds = explode(',', $trackInfo['data_tracking']);
10477
10478
            return count($questionIds);
10479
        }
10480
10481
        return $this->getQuestionCount();
10482
    }
10483
10484
    /**
10485
     * Select N values from the questions per category array.
10486
     *
10487
     * @param array $categoriesAddedInExercise
10488
     * @param array $question_list
10489
     * @param array $questions_by_category
10490
     * @param bool  $flatResult
10491
     * @param bool  $randomizeQuestions
10492
     * @param array $questionsByCategoryMandatory
10493
     *
10494
     * @return array
10495
     */
10496
    private function pickQuestionsPerCategory(
10497
        $categoriesAddedInExercise,
10498
        $question_list,
10499
        &$questions_by_category,
10500
        $flatResult = true,
10501
        $randomizeQuestions = false,
10502
        $questionsByCategoryMandatory = []
10503
    ) {
10504
        $addAll = true;
10505
        $categoryCountArray = [];
10506
10507
        // Getting how many questions will be selected per category.
10508
        if (!empty($categoriesAddedInExercise)) {
10509
            $addAll = false;
10510
            // Parsing question according the category rel exercise settings
10511
            foreach ($categoriesAddedInExercise as $category_info) {
10512
                $category_id = $category_info['category_id'];
10513
                if (isset($questions_by_category[$category_id])) {
10514
                    // How many question will be picked from this category.
10515
                    $count = $category_info['count_questions'];
10516
                    // -1 means all questions
10517
                    $categoryCountArray[$category_id] = $count;
10518
                    if (-1 == $count) {
10519
                        $categoryCountArray[$category_id] = 999;
10520
                    }
10521
                }
10522
            }
10523
        }
10524
10525
        if (!empty($questions_by_category)) {
10526
            $temp_question_list = [];
10527
            foreach ($questions_by_category as $category_id => &$categoryQuestionList) {
10528
                if (isset($categoryCountArray) && !empty($categoryCountArray)) {
10529
                    $numberOfQuestions = 0;
10530
                    if (isset($categoryCountArray[$category_id])) {
10531
                        $numberOfQuestions = $categoryCountArray[$category_id];
10532
                    }
10533
                }
10534
10535
                if ($addAll) {
10536
                    $numberOfQuestions = 999;
10537
                }
10538
                if (!empty($numberOfQuestions)) {
10539
                    $mandatoryQuestions = [];
10540
                    if (isset($questionsByCategoryMandatory[$category_id])) {
10541
                        $mandatoryQuestions = $questionsByCategoryMandatory[$category_id];
10542
                    }
10543
10544
                    $elements = TestCategory::getNElementsFromArray(
10545
                        $categoryQuestionList,
10546
                        $numberOfQuestions,
10547
                        $randomizeQuestions,
10548
                        $mandatoryQuestions
10549
                    );
10550
10551
                    if (!empty($elements)) {
10552
                        $temp_question_list[$category_id] = $elements;
10553
                        $categoryQuestionList = $elements;
10554
                    }
10555
                }
10556
            }
10557
10558
            if (!empty($temp_question_list)) {
10559
                if ($flatResult) {
10560
                    $temp_question_list = array_flatten($temp_question_list);
10561
                }
10562
                $question_list = $temp_question_list;
10563
            }
10564
        }
10565
10566
        return $question_list;
10567
    }
10568
10569
    /**
10570
     * Sends a notification when a user ends an examn.
10571
     *
10572
     * @param array  $question_list_answers
10573
     * @param string $origin
10574
     * @param array  $user_info
10575
     * @param string $url_email
10576
     * @param array  $teachers
10577
     */
10578
    private function sendNotificationForOpenQuestions(
10579
        $question_list_answers,
10580
        $origin,
10581
        $user_info,
10582
        $url_email,
10583
        $teachers
10584
    ) {
10585
        // Email configuration settings
10586
        $courseCode = api_get_course_id();
10587
        $courseInfo = api_get_course_info($courseCode);
10588
        $sessionId = api_get_session_id();
10589
        $sessionData = '';
10590
        if (!empty($sessionId)) {
10591
            $sessionInfo = api_get_session_info($sessionId);
10592
            if (!empty($sessionInfo)) {
10593
                $sessionData = '<tr>'
10594
                    .'<td><em>'.get_lang('Session name').'</em></td>'
10595
                    .'<td>&nbsp;<b>'.$sessionInfo['name'].'</b></td>'
10596
                    .'</tr>';
10597
            }
10598
        }
10599
10600
        $msg = get_lang('A learner has answered an open question').'<br /><br />'
10601
            .get_lang('Attempt details').' : <br /><br />'
10602
            .'<table>'
10603
            .'<tr>'
10604
            .'<td><em>'.get_lang('Course name').'</em></td>'
10605
            .'<td>&nbsp;<b>#course#</b></td>'
10606
            .'</tr>'
10607
            .$sessionData
10608
            .'<tr>'
10609
            .'<td>'.get_lang('Test attempted').'</td>'
10610
            .'<td>&nbsp;#exercise#</td>'
10611
            .'</tr>'
10612
            .'<tr>'
10613
            .'<td>'.get_lang('Learner name').'</td>'
10614
            .'<td>&nbsp;#firstName# #lastName#</td>'
10615
            .'</tr>'
10616
            .'<tr>'
10617
            .'<td>'.get_lang('Learner e-mail').'</td>'
10618
            .'<td>&nbsp;#mail#</td>'
10619
            .'</tr>'
10620
            .'</table>';
10621
10622
        $open_question_list = null;
10623
        foreach ($question_list_answers as $item) {
10624
            $question = $item['question'];
10625
            $answer = $item['answer'];
10626
            $answer_type = $item['answer_type'];
10627
10628
            if (!empty($question) && !empty($answer) && FREE_ANSWER == $answer_type) {
10629
                $open_question_list .=
10630
                    '<tr>
10631
                    <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
10632
                    <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
10633
                    </tr>
10634
                    <tr>
10635
                    <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
10636
                    <td valign="top" bgcolor="#F3F3F3">'.$answer.'</td>
10637
                    </tr>';
10638
            }
10639
        }
10640
10641
        if (!empty($open_question_list)) {
10642
            $msg .= '<p><br />'.get_lang('A learner has answered an open questionAre').' :</p>'.
10643
                '<table width="730" height="136" border="0" cellpadding="3" cellspacing="3">';
10644
            $msg .= $open_question_list;
10645
            $msg .= '</table><br />';
10646
10647
            $msg = str_replace('#exercise#', $this->exercise, $msg);
10648
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg);
10649
            $msg = str_replace('#lastName#', $user_info['lastname'], $msg);
10650
            $msg = str_replace('#mail#', $user_info['email'], $msg);
10651
            $msg = str_replace(
10652
                '#course#',
10653
                Display::url($courseInfo['title'], $courseInfo['course_public_url'].'?sid='.$sessionId),
10654
                $msg
10655
            );
10656
10657
            if ('learnpath' !== $origin) {
10658
                $msg .= '<br /><a href="#url#">'.get_lang(
10659
                        'Click this link to check the answer and/or give feedback'
10660
                    ).'</a>';
10661
            }
10662
            $msg = str_replace('#url#', $url_email, $msg);
10663
            $subject = get_lang('A learner has answered an open question');
10664
10665
            if (!empty($teachers)) {
10666
                foreach ($teachers as $user_id => $teacher_data) {
10667
                    MessageManager::send_message_simple(
10668
                        $user_id,
10669
                        $subject,
10670
                        $msg
10671
                    );
10672
                }
10673
            }
10674
        }
10675
    }
10676
10677
    /**
10678
     * Send notification for oral questions.
10679
     *
10680
     * @param array  $question_list_answers
10681
     * @param string $origin
10682
     * @param int    $exe_id
10683
     * @param array  $user_info
10684
     * @param string $url_email
10685
     * @param array  $teachers
10686
     */
10687
    private function sendNotificationForOralQuestions(
10688
        $question_list_answers,
10689
        $origin,
10690
        $exe_id,
10691
        $user_info,
10692
        $url_email,
10693
        $teachers
10694
    ): void {
10695
10696
        // Email configuration settings
10697
        $courseCode = api_get_course_id();
10698
        $courseInfo = api_get_course_info($courseCode);
10699
        $oral_question_list = null;
10700
        foreach ($question_list_answers as $item) {
10701
            $question = $item['question'];
10702
            $file = $item['generated_oral_file'];
10703
            $answer = $item['answer'];
10704
            if (0 == $answer) {
10705
                $answer = '';
10706
            }
10707
            $answer_type = $item['answer_type'];
10708
            if (!empty($question) && (!empty($answer) || !empty($file)) && ORAL_EXPRESSION == $answer_type) {
10709
                $oral_question_list .= '<br />
10710
                    <table width="730" height="136" border="0" cellpadding="3" cellspacing="3">
10711
                    <tr>
10712
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
10713
                        <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
10714
                    </tr>
10715
                    <tr>
10716
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
10717
                        <td valign="top" bgcolor="#F3F3F3"><p>'.$answer.'</p><p>'.$file.'</p></td>
10718
                    </tr></table>';
10719
            }
10720
        }
10721
10722
        if (!empty($oral_question_list)) {
10723
            $msg = get_lang('A learner has attempted one or more oral question').'<br /><br />
10724
                    '.get_lang('Attempt details').' : <br /><br />
10725
                    <table>
10726
                        <tr>
10727
                            <td><em>'.get_lang('Course name').'</em></td>
10728
                            <td>&nbsp;<b>#course#</b></td>
10729
                        </tr>
10730
                        <tr>
10731
                            <td>'.get_lang('Test attempted').'</td>
10732
                            <td>&nbsp;#exercise#</td>
10733
                        </tr>
10734
                        <tr>
10735
                            <td>'.get_lang('Learner name').'</td>
10736
                            <td>&nbsp;#firstName# #lastName#</td>
10737
                        </tr>
10738
                        <tr>
10739
                            <td>'.get_lang('Learner e-mail').'</td>
10740
                            <td>&nbsp;#mail#</td>
10741
                        </tr>
10742
                    </table>';
10743
            $msg .= '<br />'.sprintf(
10744
                    get_lang('The attempted oral questions are %s'),
10745
                    $oral_question_list
10746
                ).'<br />';
10747
            $msg1 = str_replace('#exercise#', $this->exercise, $msg);
10748
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg1);
10749
            $msg1 = str_replace('#lastName#', $user_info['lastname'], $msg);
10750
            $msg = str_replace('#mail#', $user_info['email'], $msg1);
10751
            $msg1 = str_replace('#course#', $courseInfo['name'], $msg);
10752
10753
            if (!in_array($origin, ['learnpath', 'embeddable'])) {
10754
                $msg1 .= '<br /><a href="#url#">'.get_lang(
10755
                        'Click this link to check the answer and/or give feedback'
10756
                    ).'</a>';
10757
            }
10758
            $msg = str_replace('#url#', $url_email, $msg1);
10759
            $mail_content = $msg;
10760
            $subject = get_lang('A learner has attempted one or more oral question');
10761
10762
            if (!empty($teachers)) {
10763
                foreach ($teachers as $user_id => $teacher_data) {
10764
                    MessageManager::send_message_simple(
10765
                        $user_id,
10766
                        $subject,
10767
                        $mail_content
10768
                    );
10769
                }
10770
            }
10771
        }
10772
    }
10773
10774
    /**
10775
     * Returns an array with the media list.
10776
     *
10777
     * @param array $questionList question list
10778
     *
10779
     * @example there's 1 question with iid 5 that belongs to the media question with iid = 100
10780
     * <code>
10781
     * array (size=2)
10782
     *  999 =>
10783
     *    array (size=3)
10784
     *      0 => int 7
10785
     *      1 => int 6
10786
     *      2 => int 3254
10787
     *  100 =>
10788
     *   array (size=1)
10789
     *      0 => int 5
10790
     *  </code>
10791
     */
10792
    private function setMediaList($questionList)
10793
    {
10794
        $mediaList = [];
10795
        /*
10796
         * Media feature is not activated in 1.11.x
10797
        if (!empty($questionList)) {
10798
            foreach ($questionList as $questionId) {
10799
                $objQuestionTmp = Question::read($questionId, $this->course_id);
10800
                // If a media question exists
10801
                if (isset($objQuestionTmp->parent_id) && $objQuestionTmp->parent_id != 0) {
10802
                    $mediaList[$objQuestionTmp->parent_id][] = $objQuestionTmp->id;
10803
                } else {
10804
                    // Always the last item
10805
                    $mediaList[999][] = $objQuestionTmp->id;
10806
                }
10807
            }
10808
        }*/
10809
10810
        $this->mediaList = $mediaList;
10811
    }
10812
10813
    /**
10814
     * @return HTML_QuickForm_group
10815
     */
10816
    private function setResultDisabledGroup(FormValidator $form)
10817
    {
10818
        $resultDisabledGroup = [];
10819
10820
        $resultDisabledGroup[] = $form->createElement(
10821
            'radio',
10822
            'results_disabled',
10823
            null,
10824
            get_lang('Auto-evaluation mode: show score and expected answers'),
10825
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
10826
            ['id' => 'result_disabled_0']
10827
        );
10828
10829
        $warning = sprintf(
10830
            get_lang('TheSettingXWillChangeToX'),
10831
            get_lang('FeedbackType'),
10832
            get_lang('NoFeedback')
10833
        );
10834
        $resultDisabledGroup[] = $form->createElement(
10835
            'radio',
10836
            'results_disabled',
10837
            null,
10838
            get_lang('Exam mode: Do not show score nor answers'),
10839
            RESULT_DISABLE_NO_SCORE_AND_EXPECTED_ANSWERS,
10840
            [
10841
                'id' => 'result_disabled_1',
10842
                //'onclick' => 'check_results_disabled()'
10843
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
10844
            ]
10845
        );
10846
10847
        $resultDisabledGroup[] = $form->createElement(
10848
            'radio',
10849
            'results_disabled',
10850
            null,
10851
            get_lang('Practice mode: Show score only, by category if at least one is used'),
10852
            RESULT_DISABLE_SHOW_SCORE_ONLY,
10853
            [
10854
                'id' => 'result_disabled_2',
10855
                //'onclick' => 'check_results_disabled()'
10856
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
10857
            ]
10858
        );
10859
10860
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
10861
            return $form->addGroup(
10862
                $resultDisabledGroup,
10863
                null,
10864
                get_lang(
10865
                    'Show score to learner'
10866
                )
10867
            );
10868
        }
10869
10870
        $resultDisabledGroup[] = $form->createElement(
10871
            'radio',
10872
            'results_disabled',
10873
            null,
10874
            get_lang(
10875
                'Show score on every attempt, show correct answers only on last attempt (only works with an attempts limit)'
10876
            ),
10877
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
10878
            ['id' => 'result_disabled_4']
10879
        );
10880
10881
        $resultDisabledGroup[] = $form->createElement(
10882
            'radio',
10883
            'results_disabled',
10884
            null,
10885
            get_lang(
10886
                'Do not show the score (only when user finishes all attempts) but show feedback for each attempt.'
10887
            ),
10888
            RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
10889
            [
10890
                'id' => 'result_disabled_5',
10891
                //'onclick' => 'check_results_disabled()'
10892
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
10893
            ]
10894
        );
10895
10896
        $resultDisabledGroup[] = $form->createElement(
10897
            'radio',
10898
            'results_disabled',
10899
            null,
10900
            get_lang(
10901
                'Ranking mode: Do not show results details question by question and show a table with the ranking of all other users.'
10902
            ),
10903
            RESULT_DISABLE_RANKING,
10904
            ['id' => 'result_disabled_6']
10905
        );
10906
10907
        $resultDisabledGroup[] = $form->createElement(
10908
            'radio',
10909
            'results_disabled',
10910
            null,
10911
            get_lang(
10912
                'Show only global score (not question score) and show only the correct answers, do not show incorrect answers at all'
10913
            ),
10914
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
10915
            ['id' => 'result_disabled_7']
10916
        );
10917
10918
        $resultDisabledGroup[] = $form->createElement(
10919
            'radio',
10920
            'results_disabled',
10921
            null,
10922
            get_lang('Auto-evaluation mode and ranking'),
10923
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
10924
            ['id' => 'result_disabled_8']
10925
        );
10926
10927
        $resultDisabledGroup[] = $form->createElement(
10928
            'radio',
10929
            'results_disabled',
10930
            null,
10931
            get_lang('Show score by category on a radar/spiderweb chart'),
10932
            RESULT_DISABLE_RADAR,
10933
            ['id' => 'result_disabled_9']
10934
        );
10935
10936
        $resultDisabledGroup[] = $form->createElement(
10937
            'radio',
10938
            'results_disabled',
10939
            null,
10940
            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.'),
10941
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
10942
            ['id' => 'result_disabled_10']
10943
        );
10944
10945
        return $form->addGroup(
10946
            $resultDisabledGroup,
10947
            null,
10948
            get_lang('Show score to learner')
10949
        );
10950
    }
10951
10952
    /**
10953
     * Return the text to display, based on the score and the max score.
10954
     * @param int|float $score
10955
     * @param int|float $maxScore
10956
     * @return string
10957
     */
10958
    public function getFinishText(int|float $score, int|float $maxScore): string
10959
    {
10960
        $passPercentage = $this->selectPassPercentage();
10961
        if (!empty($passPercentage)) {
10962
            $percentage = float_format(
10963
                ($score / (0 != $maxScore ? $maxScore : 1)) * 100,
10964
                1
10965
            );
10966
            if ($percentage >= $passPercentage) {
10967
                return $this->getTextWhenFinished();
10968
            } else {
10969
                return $this->getTextWhenFinishedFailure();
10970
            }
10971
        } else {
10972
            return $this->getTextWhenFinished();
10973
        }
10974
10975
        return '';
0 ignored issues
show
Unused Code introduced by
return '' is not reachable.

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

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

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

    return false;
}

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

Loading history...
10976
    }
10977
}
10978