Passed
Push — master ( d3e1de...ac2611 )
by Yannick
07:53
created

Exercise::cleanResults()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 70
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 42
c 0
b 0
f 0
nc 10
nop 2
dl 0
loc 70
rs 8.3146

How to fix   Long Method   

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:

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