Passed
Push — master ( 9c0d13...bb9315 )
by Julito
35:26
created

Exercise::transformQuestionListWithMedias()   C

Complexity

Conditions 12

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 27
nop 2
dl 0
loc 44
rs 6.9666
c 0
b 0
f 0

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