Passed
Push — 1.11.x ( 3568c3...5dede1 )
by Julito
10:38
created

Exercise::fill_in_blank_answer_to_array()   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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