Passed
Push — master ( 2d9a22...0085e5 )
by Julito
10:53 queued 02:39
created

Exercise::getReminderTable()   B

Complexity

Conditions 8

Size

Total Lines 121
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 45
nop 2
dl 0
loc 121
rs 7.9555
c 0
b 0
f 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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