Passed
Push — master ( 7a35ae...609d23 )
by Julito
12:15 queued 20s
created

progressExercisePaginationBarWithCategories()   C

Complexity

Conditions 15

Size

Total Lines 120
Code Lines 66

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 66
nop 4
dl 0
loc 120
rs 5.9166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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