Passed
Push — 1.11.x ( d6c56f...c7cd24 )
by Julito
09:39
created

Exercise::format_title_variable()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\GradebookLink;
6
use Chamilo\CoreBundle\Entity\TrackEExerciseConfirmation;
7
use Chamilo\CoreBundle\Entity\TrackEHotspot;
8
use Chamilo\CourseBundle\Entity\CExerciseCategory;
9
use ChamiloSession as Session;
10
use Doctrine\DBAL\Types\Type;
11
12
/**
13
 * Class Exercise.
14
 *
15
 * Allows to instantiate an object of type Exercise
16
 *
17
 * @todo use getters and setters correctly
18
 *
19
 * @author Olivier Brouckaert
20
 * @author Julio Montoya Cleaning exercises
21
 * Modified by Hubert Borderiou #294
22
 */
23
class Exercise
24
{
25
    const PAGINATION_ITEMS_PER_PAGE = 20;
26
    public $iId;
27
    public $id;
28
    public $name;
29
    public $title;
30
    public $exercise;
31
    public $description;
32
    public $sound;
33
    public $type; //ALL_ON_ONE_PAGE or ONE_PER_PAGE
34
    public $random;
35
    public $random_answers;
36
    public $active;
37
    public $timeLimit;
38
    public $attempts;
39
    public $feedback_type;
40
    public $end_time;
41
    public $start_time;
42
    public $questionList; // array with the list of this exercise's questions
43
    /* including question list of the media */
44
    public $questionListUncompressed;
45
    public $results_disabled;
46
    public $expired_time;
47
    public $course;
48
    public $course_id;
49
    public $propagate_neg;
50
    public $saveCorrectAnswers;
51
    public $review_answers;
52
    public $randomByCat;
53
    public $text_when_finished;
54
    public $display_category_name;
55
    public $pass_percentage;
56
    public $edit_exercise_in_lp = false;
57
    public $is_gradebook_locked = false;
58
    public $exercise_was_added_in_lp = false;
59
    public $lpList = [];
60
    public $force_edit_exercise_in_lp = false;
61
    public $categories;
62
    public $categories_grouping = true;
63
    public $endButton = 0;
64
    public $categoryWithQuestionList;
65
    public $mediaList;
66
    public $loadQuestionAJAX = false;
67
    // Notification send to the teacher.
68
    public $emailNotificationTemplate = null;
69
    // Notification send to the student.
70
    public $emailNotificationTemplateToUser = null;
71
    public $countQuestions = 0;
72
    public $fastEdition = false;
73
    public $modelType = 1;
74
    public $questionSelectionType = EX_Q_SELECTION_ORDERED;
75
    public $hideQuestionTitle = 0;
76
    public $scoreTypeModel = 0;
77
    public $categoryMinusOne = true; // Shows the category -1: See BT#6540
78
    public $globalCategoryId = null;
79
    public $onSuccessMessage = null;
80
    public $onFailedMessage = null;
81
    public $emailAlert;
82
    public $notifyUserByEmail = '';
83
    public $sessionId = 0;
84
    public $questionFeedbackEnabled = false;
85
    public $questionTypeWithFeedback;
86
    public $showPreviousButton;
87
    public $notifications;
88
    public $export = false;
89
    public $autolaunch;
90
    public $exerciseCategoryId;
91
    public $pageResultConfiguration;
92
    public $preventBackwards;
93
    public $currentQuestion;
94
    public $hideComment;
95
    public $hideNoAnswer;
96
    public $hideExpectedAnswer;
97
98
    /**
99
     * Constructor of the class.
100
     *
101
     * @param int $courseId
102
     *
103
     * @author Olivier Brouckaert
104
     */
105
    public function __construct($courseId = 0)
106
    {
107
        $this->iId = 0;
108
        $this->id = 0;
109
        $this->exercise = '';
110
        $this->description = '';
111
        $this->sound = '';
112
        $this->type = ALL_ON_ONE_PAGE;
113
        $this->random = 0;
114
        $this->random_answers = 0;
115
        $this->active = 1;
116
        $this->questionList = [];
117
        $this->timeLimit = 0;
118
        $this->end_time = '';
119
        $this->start_time = '';
120
        $this->results_disabled = 1;
121
        $this->expired_time = 0;
122
        $this->propagate_neg = 0;
123
        $this->saveCorrectAnswers = 0;
124
        $this->review_answers = false;
125
        $this->randomByCat = 0;
126
        $this->text_when_finished = '';
127
        $this->display_category_name = 0;
128
        $this->pass_percentage = 0;
129
        $this->modelType = 1;
130
        $this->questionSelectionType = EX_Q_SELECTION_ORDERED;
131
        $this->endButton = 0;
132
        $this->scoreTypeModel = 0;
133
        $this->globalCategoryId = null;
134
        $this->notifications = [];
135
        $this->exerciseCategoryId = null;
136
        $this->pageResultConfiguration;
137
        $this->preventBackwards = 0;
138
        $this->hideComment = false;
139
        $this->hideNoAnswer = false;
140
        $this->hideExpectedAnswer = false;
141
142
        if (!empty($courseId)) {
143
            $courseInfo = api_get_course_info_by_id($courseId);
144
        } else {
145
            $courseInfo = api_get_course_info();
146
        }
147
        $this->course_id = $courseInfo['real_id'];
148
        $this->course = $courseInfo;
149
        $this->sessionId = api_get_session_id();
150
151
        // ALTER TABLE c_quiz_question ADD COLUMN feedback text;
152
        $this->questionFeedbackEnabled = api_get_configuration_value('allow_quiz_question_feedback');
153
        $this->showPreviousButton = true;
154
    }
155
156
    /**
157
     * Reads exercise information from the data base.
158
     *
159
     * @author Olivier Brouckaert
160
     *
161
     * @param int  $id                - exercise Id
162
     * @param bool $parseQuestionList
163
     *
164
     * @return bool - true if exercise exists, otherwise false
165
     */
166
    public function read($id, $parseQuestionList = true)
167
    {
168
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
169
170
        $id = (int) $id;
171
        if (empty($this->course_id)) {
172
            return false;
173
        }
174
175
        $sql = "SELECT * FROM $table
176
                WHERE c_id = ".$this->course_id." AND id = ".$id;
177
        $result = Database::query($sql);
178
179
        // if the exercise has been found
180
        if ($object = Database::fetch_object($result)) {
181
            $this->iId = $object->iid;
182
            $this->id = $id;
183
            $this->exercise = $object->title;
184
            $this->name = $object->title;
185
            $this->title = $object->title;
186
            $this->description = $object->description;
187
            $this->sound = $object->sound;
188
            $this->type = $object->type;
189
            if (empty($this->type)) {
190
                $this->type = ONE_PER_PAGE;
191
            }
192
            $this->random = $object->random;
193
            $this->random_answers = $object->random_answers;
194
            $this->active = $object->active;
195
            $this->results_disabled = $object->results_disabled;
196
            $this->attempts = $object->max_attempt;
197
            $this->feedback_type = $object->feedback_type;
198
            $this->sessionId = $object->session_id;
199
            $this->propagate_neg = $object->propagate_neg;
200
            $this->saveCorrectAnswers = $object->save_correct_answers;
201
            $this->randomByCat = $object->random_by_category;
202
            $this->text_when_finished = $object->text_when_finished;
203
            $this->display_category_name = $object->display_category_name;
204
            $this->pass_percentage = $object->pass_percentage;
205
            $this->is_gradebook_locked = api_resource_is_locked_by_gradebook($id, LINK_EXERCISE);
206
            $this->review_answers = (isset($object->review_answers) && $object->review_answers == 1) ? true : false;
207
            $this->globalCategoryId = isset($object->global_category_id) ? $object->global_category_id : null;
208
            $this->questionSelectionType = isset($object->question_selection_type) ? (int) $object->question_selection_type : null;
209
            $this->hideQuestionTitle = isset($object->hide_question_title) ? (int) $object->hide_question_title : 0;
210
            $this->autolaunch = isset($object->autolaunch) ? (int) $object->autolaunch : 0;
211
            $this->exerciseCategoryId = isset($object->exercise_category_id) ? (int) $object->exercise_category_id : null;
212
            $this->preventBackwards = isset($object->prevent_backwards) ? (int) $object->prevent_backwards : 0;
213
            $this->exercise_was_added_in_lp = false;
214
            $this->lpList = [];
215
            $this->notifications = [];
216
            if (!empty($object->notifications)) {
217
                $this->notifications = explode(',', $object->notifications);
218
            }
219
220
            if (!empty($object->page_result_configuration)) {
221
                $this->pageResultConfiguration = $object->page_result_configuration;
222
            }
223
224
            if (isset($object->show_previous_button)) {
225
                $this->showPreviousButton = $object->show_previous_button == 1 ? true : false;
226
            }
227
228
            $list = self::getLpListFromExercise($id, $this->course_id);
229
            if (!empty($list)) {
230
                $this->exercise_was_added_in_lp = true;
231
                $this->lpList = $list;
232
            }
233
234
            $this->force_edit_exercise_in_lp = api_get_configuration_value('force_edit_exercise_in_lp');
235
            $this->edit_exercise_in_lp = true;
236
            if ($this->exercise_was_added_in_lp) {
237
                $this->edit_exercise_in_lp = $this->force_edit_exercise_in_lp == true;
238
            }
239
240
            if (!empty($object->end_time)) {
241
                $this->end_time = $object->end_time;
242
            }
243
            if (!empty($object->start_time)) {
244
                $this->start_time = $object->start_time;
245
            }
246
247
            // Control time
248
            $this->expired_time = $object->expired_time;
249
250
            // Checking if question_order is correctly set
251
            if ($parseQuestionList) {
252
                $this->setQuestionList(true);
253
            }
254
255
            //overload questions list with recorded questions list
256
            //load questions only for exercises of type 'one question per page'
257
            //this is needed only is there is no questions
258
259
            // @todo not sure were in the code this is used somebody mess with the exercise tool
260
            // @todo don't know who add that config and why $_configuration['live_exercise_tracking']
261
            /*global $_configuration, $questionList;
262
            if ($this->type == ONE_PER_PAGE && $_SERVER['REQUEST_METHOD'] != 'POST'
263
                && defined('QUESTION_LIST_ALREADY_LOGGED') &&
264
                isset($_configuration['live_exercise_tracking']) && $_configuration['live_exercise_tracking']
265
            ) {
266
                $this->questionList = $questionList;
267
            }*/
268
            return true;
269
        }
270
271
        return false;
272
    }
273
274
    /**
275
     * @return string
276
     */
277
    public function getCutTitle()
278
    {
279
        $title = $this->getUnformattedTitle();
280
281
        return cut($title, EXERCISE_MAX_NAME_SIZE);
282
    }
283
284
    /**
285
     * returns the exercise ID.
286
     *
287
     * @author Olivier Brouckaert
288
     *
289
     * @return int - exercise ID
290
     */
291
    public function selectId()
292
    {
293
        return $this->id;
294
    }
295
296
    /**
297
     * returns the exercise title.
298
     *
299
     * @author Olivier Brouckaert
300
     *
301
     * @param bool $unformattedText Optional. Get the title without HTML tags
302
     *
303
     * @return string - exercise title
304
     */
305
    public function selectTitle($unformattedText = false)
306
    {
307
        if ($unformattedText) {
308
            return $this->getUnformattedTitle();
309
        }
310
311
        return $this->exercise;
312
    }
313
314
    /**
315
     * returns the number of attempts setted.
316
     *
317
     * @return int - exercise attempts
318
     */
319
    public function selectAttempts()
320
    {
321
        return $this->attempts;
322
    }
323
324
    /**
325
     * Returns the number of FeedbackType
326
     *  0: Feedback , 1: DirectFeedback, 2: NoFeedback.
327
     *
328
     * @return int - exercise attempts
329
     */
330
    public function getFeedbackType()
331
    {
332
        return (int) $this->feedback_type;
333
    }
334
335
    /**
336
     * returns the time limit.
337
     *
338
     * @return int
339
     */
340
    public function selectTimeLimit()
341
    {
342
        return $this->timeLimit;
343
    }
344
345
    /**
346
     * returns the exercise description.
347
     *
348
     * @author Olivier Brouckaert
349
     *
350
     * @return string - exercise description
351
     */
352
    public function selectDescription()
353
    {
354
        return $this->description;
355
    }
356
357
    /**
358
     * returns the exercise sound file.
359
     *
360
     * @author Olivier Brouckaert
361
     *
362
     * @return string - exercise description
363
     */
364
    public function selectSound()
365
    {
366
        return $this->sound;
367
    }
368
369
    /**
370
     * returns the exercise type.
371
     *
372
     * @author Olivier Brouckaert
373
     *
374
     * @return int - exercise type
375
     */
376
    public function selectType()
377
    {
378
        return $this->type;
379
    }
380
381
    /**
382
     * @return int
383
     */
384
    public function getModelType()
385
    {
386
        return $this->modelType;
387
    }
388
389
    /**
390
     * @return int
391
     */
392
    public function selectEndButton()
393
    {
394
        return $this->endButton;
395
    }
396
397
    /**
398
     * @author hubert borderiou 30-11-11
399
     *
400
     * @return int : do we display the question category name for students
401
     */
402
    public function selectDisplayCategoryName()
403
    {
404
        return $this->display_category_name;
405
    }
406
407
    /**
408
     * @return int
409
     */
410
    public function selectPassPercentage()
411
    {
412
        return $this->pass_percentage;
413
    }
414
415
    /**
416
     * Modify object to update the switch display_category_name.
417
     *
418
     * @author hubert borderiou 30-11-11
419
     *
420
     * @param int $value is an integer 0 or 1
421
     */
422
    public function updateDisplayCategoryName($value)
423
    {
424
        $this->display_category_name = $value;
425
    }
426
427
    /**
428
     * @author hubert borderiou 28-11-11
429
     *
430
     * @return string html text : the text to display ay the end of the test
431
     */
432
    public function getTextWhenFinished()
433
    {
434
        return $this->text_when_finished;
435
    }
436
437
    /**
438
     * @param string $text
439
     *
440
     * @author hubert borderiou 28-11-11
441
     */
442
    public function updateTextWhenFinished($text)
443
    {
444
        $this->text_when_finished = $text;
445
    }
446
447
    /**
448
     * return 1 or 2 if randomByCat.
449
     *
450
     * @author hubert borderiou
451
     *
452
     * @return int - quiz random by category
453
     */
454
    public function getRandomByCategory()
455
    {
456
        return $this->randomByCat;
457
    }
458
459
    /**
460
     * return 0 if no random by cat
461
     * return 1 if random by cat, categories shuffled
462
     * return 2 if random by cat, categories sorted by alphabetic order.
463
     *
464
     * @author hubert borderiou
465
     *
466
     * @return int - quiz random by category
467
     */
468
    public function isRandomByCat()
469
    {
470
        $res = EXERCISE_CATEGORY_RANDOM_DISABLED;
471
        if ($this->randomByCat == EXERCISE_CATEGORY_RANDOM_SHUFFLED) {
472
            $res = EXERCISE_CATEGORY_RANDOM_SHUFFLED;
473
        } elseif ($this->randomByCat == EXERCISE_CATEGORY_RANDOM_ORDERED) {
474
            $res = EXERCISE_CATEGORY_RANDOM_ORDERED;
475
        }
476
477
        return $res;
478
    }
479
480
    /**
481
     * return nothing
482
     * update randomByCat value for object.
483
     *
484
     * @param int $random
485
     *
486
     * @author hubert borderiou
487
     */
488
    public function updateRandomByCat($random)
489
    {
490
        $this->randomByCat = EXERCISE_CATEGORY_RANDOM_DISABLED;
491
        if (in_array(
492
            $random,
493
            [
494
                EXERCISE_CATEGORY_RANDOM_SHUFFLED,
495
                EXERCISE_CATEGORY_RANDOM_ORDERED,
496
                EXERCISE_CATEGORY_RANDOM_DISABLED,
497
            ]
498
        )) {
499
            $this->randomByCat = $random;
500
        }
501
    }
502
503
    /**
504
     * Tells if questions are selected randomly, and if so returns the draws.
505
     *
506
     * @author Carlos Vargas
507
     *
508
     * @return int - results disabled exercise
509
     */
510
    public function selectResultsDisabled()
511
    {
512
        return $this->results_disabled;
513
    }
514
515
    /**
516
     * tells if questions are selected randomly, and if so returns the draws.
517
     *
518
     * @author Olivier Brouckaert
519
     *
520
     * @return bool
521
     */
522
    public function isRandom()
523
    {
524
        $isRandom = false;
525
        // "-1" means all questions will be random
526
        if ($this->random > 0 || $this->random == -1) {
527
            $isRandom = true;
528
        }
529
530
        return $isRandom;
531
    }
532
533
    /**
534
     * returns random answers status.
535
     *
536
     * @author Juan Carlos Rana
537
     */
538
    public function getRandomAnswers()
539
    {
540
        return $this->random_answers;
541
    }
542
543
    /**
544
     * Same as isRandom() but has a name applied to values different than 0 or 1.
545
     *
546
     * @return int
547
     */
548
    public function getShuffle()
549
    {
550
        return $this->random;
551
    }
552
553
    /**
554
     * returns the exercise status (1 = enabled ; 0 = disabled).
555
     *
556
     * @author Olivier Brouckaert
557
     *
558
     * @return int - 1 if enabled, otherwise 0
559
     */
560
    public function selectStatus()
561
    {
562
        return $this->active;
563
    }
564
565
    /**
566
     * If false the question list will be managed as always if true
567
     * the question will be filtered
568
     * depending of the exercise settings (table c_quiz_rel_category).
569
     *
570
     * @param bool $status active or inactive grouping
571
     */
572
    public function setCategoriesGrouping($status)
573
    {
574
        $this->categories_grouping = (bool) $status;
575
    }
576
577
    /**
578
     * @return int
579
     */
580
    public function getHideQuestionTitle()
581
    {
582
        return $this->hideQuestionTitle;
583
    }
584
585
    /**
586
     * @param $value
587
     */
588
    public function setHideQuestionTitle($value)
589
    {
590
        $this->hideQuestionTitle = (int) $value;
591
    }
592
593
    /**
594
     * @return int
595
     */
596
    public function getScoreTypeModel()
597
    {
598
        return $this->scoreTypeModel;
599
    }
600
601
    /**
602
     * @param int $value
603
     */
604
    public function setScoreTypeModel($value)
605
    {
606
        $this->scoreTypeModel = (int) $value;
607
    }
608
609
    /**
610
     * @return int
611
     */
612
    public function getGlobalCategoryId()
613
    {
614
        return $this->globalCategoryId;
615
    }
616
617
    /**
618
     * @param int $value
619
     */
620
    public function setGlobalCategoryId($value)
621
    {
622
        if (is_array($value) && isset($value[0])) {
623
            $value = $value[0];
624
        }
625
        $this->globalCategoryId = (int) $value;
626
    }
627
628
    /**
629
     * @param int    $start
630
     * @param int    $limit
631
     * @param int    $sidx
632
     * @param string $sord
633
     * @param array  $whereCondition
634
     * @param array  $extraFields
635
     *
636
     * @return array
637
     */
638
    public function getQuestionListPagination(
639
        $start,
640
        $limit,
641
        $sidx,
642
        $sord,
643
        $whereCondition = [],
644
        $extraFields = []
645
    ) {
646
        if (!empty($this->id)) {
647
            $category_list = TestCategory::getListOfCategoriesNameForTest(
648
                $this->id,
649
                false
650
            );
651
            $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
652
            $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
653
654
            $sql = "SELECT q.iid
655
                    FROM $TBL_EXERCICE_QUESTION e
656
                    INNER JOIN $TBL_QUESTIONS  q
657
                    ON (e.question_id = q.id AND e.c_id = ".$this->course_id." )
658
					WHERE e.exercice_id	= '".$this->id."' ";
659
660
            $orderCondition = ' ORDER BY question_order ';
661
662
            if (!empty($sidx) && !empty($sord)) {
663
                if ('question' === $sidx) {
664
                    if (in_array(strtolower($sord), ['desc', 'asc'])) {
665
                        $orderCondition = " ORDER BY q.$sidx $sord";
666
                    }
667
                }
668
            }
669
670
            $sql .= $orderCondition;
671
            $limitCondition = null;
672
            if (isset($start) && isset($limit)) {
673
                $start = (int) $start;
674
                $limit = (int) $limit;
675
                $limitCondition = " LIMIT $start, $limit";
676
            }
677
            $sql .= $limitCondition;
678
            $result = Database::query($sql);
679
            $questions = [];
680
            if (Database::num_rows($result)) {
681
                if (!empty($extraFields)) {
682
                    $extraFieldValue = new ExtraFieldValue('question');
683
                }
684
                while ($question = Database::fetch_array($result, 'ASSOC')) {
685
                    /** @var Question $objQuestionTmp */
686
                    $objQuestionTmp = Question::read($question['iid']);
687
                    $category_labels = TestCategory::return_category_labels(
688
                        $objQuestionTmp->category_list,
689
                        $category_list
690
                    );
691
692
                    if (empty($category_labels)) {
693
                        $category_labels = '-';
694
                    }
695
696
                    // Question type
697
                    $typeImg = $objQuestionTmp->getTypePicture();
698
                    $typeExpl = $objQuestionTmp->getExplanation();
699
700
                    $question_media = null;
701
                    if (!empty($objQuestionTmp->parent_id)) {
702
                        $objQuestionMedia = Question::read($objQuestionTmp->parent_id);
703
                        $question_media = Question::getMediaLabel($objQuestionMedia->question);
704
                    }
705
706
                    $questionType = Display::tag(
707
                        'div',
708
                        Display::return_icon($typeImg, $typeExpl, [], ICON_SIZE_MEDIUM).$question_media
709
                    );
710
711
                    $question = [
712
                        'id' => $question['iid'],
713
                        'question' => $objQuestionTmp->selectTitle(),
714
                        'type' => $questionType,
715
                        'category' => Display::tag(
716
                            'div',
717
                            '<a href="#" style="padding:0px; margin:0px;">'.$category_labels.'</a>'
718
                        ),
719
                        'score' => $objQuestionTmp->selectWeighting(),
720
                        'level' => $objQuestionTmp->level,
721
                    ];
722
723
                    if (!empty($extraFields)) {
724
                        foreach ($extraFields as $extraField) {
725
                            $value = $extraFieldValue->get_values_by_handler_and_field_id(
726
                                $question['id'],
727
                                $extraField['id']
728
                            );
729
                            $stringValue = null;
730
                            if ($value) {
731
                                $stringValue = $value['field_value'];
732
                            }
733
                            $question[$extraField['field_variable']] = $stringValue;
734
                        }
735
                    }
736
                    $questions[] = $question;
737
                }
738
            }
739
740
            return $questions;
741
        }
742
    }
743
744
    /**
745
     * Get question count per exercise from DB (any special treatment).
746
     *
747
     * @return int
748
     */
749
    public function getQuestionCount()
750
    {
751
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
752
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
753
        $sql = "SELECT count(q.id) as count
754
                FROM $TBL_EXERCICE_QUESTION e
755
                INNER JOIN $TBL_QUESTIONS q
756
                ON (e.question_id = q.id AND e.c_id = q.c_id)
757
                WHERE
758
                    e.c_id = {$this->course_id} AND
759
                    e.exercice_id = ".$this->id;
760
        $result = Database::query($sql);
761
762
        $count = 0;
763
        if (Database::num_rows($result)) {
764
            $row = Database::fetch_array($result);
765
            $count = (int) $row['count'];
766
        }
767
768
        return $count;
769
    }
770
771
    /**
772
     * @return array
773
     */
774
    public function getQuestionOrderedListByName()
775
    {
776
        if (empty($this->course_id) || empty($this->id)) {
777
            return [];
778
        }
779
780
        $exerciseQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
781
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
782
783
        // Getting question list from the order (question list drag n drop interface ).
784
        $sql = "SELECT e.question_id
785
                FROM $exerciseQuestionTable e
786
                INNER JOIN $questionTable q
787
                ON (e.question_id= q.id AND e.c_id = q.c_id)
788
                WHERE
789
                    e.c_id = {$this->course_id} AND
790
                    e.exercice_id = '".$this->id."'
791
                ORDER BY q.question";
792
        $result = Database::query($sql);
793
        $list = [];
794
        if (Database::num_rows($result)) {
795
            $list = Database::store_result($result, 'ASSOC');
796
        }
797
798
        return $list;
799
    }
800
801
    /**
802
     * Selecting question list depending in the exercise-category
803
     * relationship (category table in exercise settings).
804
     *
805
     * @param array $question_list
806
     * @param int   $questionSelectionType
807
     *
808
     * @return array
809
     */
810
    public function getQuestionListWithCategoryListFilteredByCategorySettings(
811
        $question_list,
812
        $questionSelectionType
813
    ) {
814
        $result = [
815
            'question_list' => [],
816
            'category_with_questions_list' => [],
817
        ];
818
819
        // Order/random categories
820
        $cat = new TestCategory();
821
        // Setting category order.
822
        switch ($questionSelectionType) {
823
            case EX_Q_SELECTION_ORDERED: // 1
824
            case EX_Q_SELECTION_RANDOM:  // 2
825
                // This options are not allowed here.
826
                break;
827
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED: // 3
828
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
829
                    $this,
830
                    $this->course['real_id'],
831
                    'title ASC',
832
                    false,
833
                    true
834
                );
835
836
                $questions_by_category = TestCategory::getQuestionsByCat(
837
                    $this->id,
838
                    $question_list,
839
                    $categoriesAddedInExercise
840
                );
841
842
                $question_list = $this->pickQuestionsPerCategory(
843
                    $categoriesAddedInExercise,
844
                    $question_list,
845
                    $questions_by_category,
846
                    true,
847
                    false
848
                );
849
                break;
850
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED: // 4
851
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7
852
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
853
                    $this,
854
                    $this->course['real_id'],
855
                    null,
856
                    true,
857
                    true
858
                );
859
                $questions_by_category = TestCategory::getQuestionsByCat(
860
                    $this->id,
861
                    $question_list,
862
                    $categoriesAddedInExercise
863
                );
864
                $question_list = $this->pickQuestionsPerCategory(
865
                    $categoriesAddedInExercise,
866
                    $question_list,
867
                    $questions_by_category,
868
                    true,
869
                    false
870
                );
871
                break;
872
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM: // 5
873
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
874
                    $this,
875
                    $this->course['real_id'],
876
                    'title ASC',
877
                    false,
878
                    true
879
                );
880
                $questions_by_category = TestCategory::getQuestionsByCat(
881
                    $this->id,
882
                    $question_list,
883
                    $categoriesAddedInExercise
884
                );
885
886
                $questionsByCategoryMandatory = [];
887
                if (EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM == $this->getQuestionSelectionType() &&
888
                    api_get_configuration_value('allow_mandatory_question_in_category')
889
                ) {
890
                    $questionsByCategoryMandatory = TestCategory::getQuestionsByCat(
891
                        $this->id,
892
                        $question_list,
893
                        $categoriesAddedInExercise,
894
                        true
895
                    );
896
                }
897
898
                $question_list = $this->pickQuestionsPerCategory(
899
                    $categoriesAddedInExercise,
900
                    $question_list,
901
                    $questions_by_category,
902
                    true,
903
                    true,
904
                    $questionsByCategoryMandatory
905
                );
906
                break;
907
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM: // 6
908
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED:
909
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
910
                    $this,
911
                    $this->course['real_id'],
912
                    null,
913
                    true,
914
                    true
915
                );
916
917
                $questions_by_category = TestCategory::getQuestionsByCat(
918
                    $this->id,
919
                    $question_list,
920
                    $categoriesAddedInExercise
921
                );
922
923
                $question_list = $this->pickQuestionsPerCategory(
924
                    $categoriesAddedInExercise,
925
                    $question_list,
926
                    $questions_by_category,
927
                    true,
928
                    true
929
                );
930
                break;
931
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7
932
                break;
933
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED: // 8
934
                break;
935
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED: // 9
936
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
937
                    $this,
938
                    $this->course['real_id'],
939
                    'root ASC, lft ASC',
940
                    false,
941
                    true
942
                );
943
                $questions_by_category = TestCategory::getQuestionsByCat(
944
                    $this->id,
945
                    $question_list,
946
                    $categoriesAddedInExercise
947
                );
948
                $question_list = $this->pickQuestionsPerCategory(
949
                    $categoriesAddedInExercise,
950
                    $question_list,
951
                    $questions_by_category,
952
                    true,
953
                    false
954
                );
955
                break;
956
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM: // 10
957
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
958
                    $this,
959
                    $this->course['real_id'],
960
                    'root, lft ASC',
961
                    false,
962
                    true
963
                );
964
                $questions_by_category = TestCategory::getQuestionsByCat(
965
                    $this->id,
966
                    $question_list,
967
                    $categoriesAddedInExercise
968
                );
969
                $question_list = $this->pickQuestionsPerCategory(
970
                    $categoriesAddedInExercise,
971
                    $question_list,
972
                    $questions_by_category,
973
                    true,
974
                    true
975
                );
976
                break;
977
        }
978
979
        $result['question_list'] = isset($question_list) ? $question_list : [];
980
        $result['category_with_questions_list'] = isset($questions_by_category) ? $questions_by_category : [];
981
        $parentsLoaded = [];
982
        // Adding category info in the category list with question list:
983
        if (!empty($questions_by_category)) {
984
            $newCategoryList = [];
985
            $em = Database::getManager();
986
            $repo = $em->getRepository('ChamiloCourseBundle:CQuizCategory');
987
988
            foreach ($questions_by_category as $categoryId => $questionList) {
989
                $cat = new TestCategory();
990
                $cat = $cat->getCategory($categoryId);
991
                if ($cat) {
992
                    $cat = (array) $cat;
993
                    $cat['iid'] = $cat['id'];
994
                }
995
996
                $categoryParentInfo = null;
997
                // Parent is not set no loop here
998
                if (isset($cat['parent_id']) && !empty($cat['parent_id'])) {
999
                    /** @var \Chamilo\CourseBundle\Entity\CQuizCategory $categoryEntity */
1000
                    if (!isset($parentsLoaded[$cat['parent_id']])) {
1001
                        $categoryEntity = $em->find('ChamiloCourseBundle:CQuizCategory', $cat['parent_id']);
1002
                        $parentsLoaded[$cat['parent_id']] = $categoryEntity;
1003
                    } else {
1004
                        $categoryEntity = $parentsLoaded[$cat['parent_id']];
1005
                    }
1006
                    $path = $repo->getPath($categoryEntity);
1007
1008
                    $index = 0;
1009
                    if ($this->categoryMinusOne) {
1010
                        //$index = 1;
1011
                    }
1012
1013
                    /** @var \Chamilo\CourseBundle\Entity\CQuizCategory $categoryParent */
1014
                    foreach ($path as $categoryParent) {
1015
                        $visibility = $categoryParent->getVisibility();
1016
                        if (0 == $visibility) {
1017
                            $categoryParentId = $categoryId;
1018
                            $categoryTitle = $cat['title'];
1019
                            if (count($path) > 1) {
1020
                                continue;
1021
                            }
1022
                        } else {
1023
                            $categoryParentId = $categoryParent->getIid();
1024
                            $categoryTitle = $categoryParent->getTitle();
1025
                        }
1026
1027
                        $categoryParentInfo['id'] = $categoryParentId;
1028
                        $categoryParentInfo['iid'] = $categoryParentId;
1029
                        $categoryParentInfo['parent_path'] = null;
1030
                        $categoryParentInfo['title'] = $categoryTitle;
1031
                        $categoryParentInfo['name'] = $categoryTitle;
1032
                        $categoryParentInfo['parent_id'] = null;
1033
                        break;
1034
                    }
1035
                }
1036
                $cat['parent_info'] = $categoryParentInfo;
1037
                $newCategoryList[$categoryId] = [
1038
                    'category' => $cat,
1039
                    'question_list' => $questionList,
1040
                ];
1041
            }
1042
1043
            $result['category_with_questions_list'] = $newCategoryList;
1044
        }
1045
1046
        return $result;
1047
    }
1048
1049
    /**
1050
     * returns the array with the question ID list.
1051
     *
1052
     * @param bool $fromDatabase Whether the results should be fetched in the database or just from memory
1053
     * @param bool $adminView    Whether we should return all questions (admin view) or
1054
     *                           just a list limited by the max number of random questions
1055
     *
1056
     * @author Olivier Brouckaert
1057
     *
1058
     * @return array - question ID list
1059
     */
1060
    public function selectQuestionList($fromDatabase = false, $adminView = false)
1061
    {
1062
        if ($fromDatabase && !empty($this->id)) {
1063
            $nbQuestions = $this->getQuestionCount();
1064
            $questionSelectionType = $this->getQuestionSelectionType();
1065
1066
            switch ($questionSelectionType) {
1067
                case EX_Q_SELECTION_ORDERED:
1068
                    $questionList = $this->getQuestionOrderedList($adminView);
1069
                    break;
1070
                case EX_Q_SELECTION_RANDOM:
1071
                    // Not a random exercise, or if there are not at least 2 questions
1072
                    if ($this->random == 0 || $nbQuestions < 2) {
1073
                        $questionList = $this->getQuestionOrderedList($adminView);
1074
                    } else {
1075
                        $questionList = $this->getRandomList($adminView);
1076
                    }
1077
                    break;
1078
                default:
1079
                    $questionList = $this->getQuestionOrderedList($adminView);
1080
                    $result = $this->getQuestionListWithCategoryListFilteredByCategorySettings(
1081
                        $questionList,
1082
                        $questionSelectionType
1083
                    );
1084
                    $this->categoryWithQuestionList = $result['category_with_questions_list'];
1085
                    $questionList = $result['question_list'];
1086
                    break;
1087
            }
1088
1089
            return $questionList;
1090
        }
1091
1092
        return $this->questionList;
1093
    }
1094
1095
    /**
1096
     * returns the number of questions in this exercise.
1097
     *
1098
     * @author Olivier Brouckaert
1099
     *
1100
     * @return int - number of questions
1101
     */
1102
    public function selectNbrQuestions()
1103
    {
1104
        return count($this->questionList);
1105
    }
1106
1107
    /**
1108
     * @return int
1109
     */
1110
    public function selectPropagateNeg()
1111
    {
1112
        return $this->propagate_neg;
1113
    }
1114
1115
    /**
1116
     * @return int
1117
     */
1118
    public function getSaveCorrectAnswers()
1119
    {
1120
        return $this->saveCorrectAnswers;
1121
    }
1122
1123
    /**
1124
     * Selects questions randomly in the question list.
1125
     *
1126
     * @author Olivier Brouckaert
1127
     * @author Hubert Borderiou 15 nov 2011
1128
     *
1129
     * @param bool $adminView Whether we should return all
1130
     *                        questions (admin view) or just a list limited by the max number of random questions
1131
     *
1132
     * @return array - if the exercise is not set to take questions randomly, returns the question list
1133
     *               without randomizing, otherwise, returns the list with questions selected randomly
1134
     */
1135
    public function getRandomList($adminView = false)
1136
    {
1137
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1138
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
1139
        $random = isset($this->random) && !empty($this->random) ? $this->random : 0;
1140
1141
        // Random with limit
1142
        $randomLimit = " ORDER BY RAND() LIMIT $random";
1143
1144
        // Random with no limit
1145
        if (-1 == $random) {
1146
            $randomLimit = ' ORDER BY RAND() ';
1147
        }
1148
1149
        // Admin see the list in default order
1150
        if (true === $adminView) {
1151
            // If viewing it as admin for edition, don't show it randomly, use title + id
1152
            $randomLimit = 'ORDER BY e.question_order';
1153
        }
1154
1155
        $sql = "SELECT e.question_id
1156
                FROM $quizRelQuestion e
1157
                INNER JOIN $question q
1158
                ON (e.question_id= q.id AND e.c_id = q.c_id)
1159
                WHERE
1160
                    e.c_id = {$this->course_id} AND
1161
                    e.exercice_id = '".Database::escape_string($this->id)."'
1162
                    $randomLimit ";
1163
        $result = Database::query($sql);
1164
        $questionList = [];
1165
        while ($row = Database::fetch_object($result)) {
1166
            $questionList[] = $row->question_id;
1167
        }
1168
1169
        return $questionList;
1170
    }
1171
1172
    /**
1173
     * returns 'true' if the question ID is in the question list.
1174
     *
1175
     * @author Olivier Brouckaert
1176
     *
1177
     * @param int $questionId - question ID
1178
     *
1179
     * @return bool - true if in the list, otherwise false
1180
     */
1181
    public function isInList($questionId)
1182
    {
1183
        $inList = false;
1184
        if (is_array($this->questionList)) {
1185
            $inList = in_array($questionId, $this->questionList);
1186
        }
1187
1188
        return $inList;
1189
    }
1190
1191
    /**
1192
     * If current exercise has a question.
1193
     *
1194
     * @param int $questionId
1195
     *
1196
     * @return int
1197
     */
1198
    public function hasQuestion($questionId)
1199
    {
1200
        $questionId = (int) $questionId;
1201
1202
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1203
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1204
        $sql = "SELECT q.id
1205
                FROM $TBL_EXERCICE_QUESTION e
1206
                INNER JOIN $TBL_QUESTIONS q
1207
                ON (e.question_id = q.id AND e.c_id = q.c_id)
1208
                WHERE
1209
                    q.id = $questionId AND
1210
                    e.c_id = {$this->course_id} AND
1211
                    e.exercice_id = ".$this->id;
1212
1213
        $result = Database::query($sql);
1214
1215
        return Database::num_rows($result) > 0;
1216
    }
1217
1218
    public function hasQuestionWithType($type)
1219
    {
1220
        $type = (int) $type;
1221
1222
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1223
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
1224
        $sql = "SELECT q.id
1225
                FROM $table e
1226
                INNER JOIN $tableQuestion q
1227
                ON (e.question_id = q.id AND e.c_id = q.c_id)
1228
                WHERE
1229
                    q.type = $type AND
1230
                    e.c_id = {$this->course_id} AND
1231
                    e.exercice_id = ".$this->id;
1232
1233
        $result = Database::query($sql);
1234
1235
        return Database::num_rows($result) > 0;
1236
    }
1237
1238
    public function hasQuestionWithTypeNotInList(array $questionTypeList)
1239
    {
1240
        if (empty($questionTypeList)) {
1241
            return false;
1242
        }
1243
1244
        $questionTypeToString = implode("','", array_map('intval', $questionTypeList));
1245
1246
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1247
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
1248
        $sql = "SELECT q.id
1249
                FROM $table e
1250
                INNER JOIN $tableQuestion q
1251
                ON (e.question_id = q.id AND e.c_id = q.c_id)
1252
                WHERE
1253
                    q.type NOT IN ('$questionTypeToString')  AND
1254
                    e.c_id = {$this->course_id} AND
1255
                    e.exercice_id = ".$this->id;
1256
1257
        $result = Database::query($sql);
1258
1259
        return Database::num_rows($result) > 0;
1260
    }
1261
1262
    /**
1263
     * changes the exercise title.
1264
     *
1265
     * @author Olivier Brouckaert
1266
     *
1267
     * @param string $title - exercise title
1268
     */
1269
    public function updateTitle($title)
1270
    {
1271
        $this->title = $this->exercise = $title;
1272
    }
1273
1274
    /**
1275
     * changes the exercise max attempts.
1276
     *
1277
     * @param int $attempts - exercise max attempts
1278
     */
1279
    public function updateAttempts($attempts)
1280
    {
1281
        $this->attempts = $attempts;
1282
    }
1283
1284
    /**
1285
     * changes the exercise feedback type.
1286
     *
1287
     * @param int $feedback_type
1288
     */
1289
    public function updateFeedbackType($feedback_type)
1290
    {
1291
        $this->feedback_type = $feedback_type;
1292
    }
1293
1294
    /**
1295
     * changes the exercise description.
1296
     *
1297
     * @author Olivier Brouckaert
1298
     *
1299
     * @param string $description - exercise description
1300
     */
1301
    public function updateDescription($description)
1302
    {
1303
        $this->description = $description;
1304
    }
1305
1306
    /**
1307
     * changes the exercise expired_time.
1308
     *
1309
     * @author Isaac flores
1310
     *
1311
     * @param int $expired_time The expired time of the quiz
1312
     */
1313
    public function updateExpiredTime($expired_time)
1314
    {
1315
        $this->expired_time = $expired_time;
1316
    }
1317
1318
    /**
1319
     * @param $value
1320
     */
1321
    public function updatePropagateNegative($value)
1322
    {
1323
        $this->propagate_neg = $value;
1324
    }
1325
1326
    /**
1327
     * @param int $value
1328
     */
1329
    public function updateSaveCorrectAnswers($value)
1330
    {
1331
        $this->saveCorrectAnswers = (int) $value;
1332
    }
1333
1334
    /**
1335
     * @param $value
1336
     */
1337
    public function updateReviewAnswers($value)
1338
    {
1339
        $this->review_answers = isset($value) && $value ? true : false;
1340
    }
1341
1342
    /**
1343
     * @param $value
1344
     */
1345
    public function updatePassPercentage($value)
1346
    {
1347
        $this->pass_percentage = $value;
1348
    }
1349
1350
    /**
1351
     * @param string $text
1352
     */
1353
    public function updateEmailNotificationTemplate($text)
1354
    {
1355
        $this->emailNotificationTemplate = $text;
1356
    }
1357
1358
    /**
1359
     * @param string $text
1360
     */
1361
    public function setEmailNotificationTemplateToUser($text)
1362
    {
1363
        $this->emailNotificationTemplateToUser = $text;
1364
    }
1365
1366
    /**
1367
     * @param string $value
1368
     */
1369
    public function setNotifyUserByEmail($value)
1370
    {
1371
        $this->notifyUserByEmail = $value;
1372
    }
1373
1374
    /**
1375
     * @param int $value
1376
     */
1377
    public function updateEndButton($value)
1378
    {
1379
        $this->endButton = (int) $value;
1380
    }
1381
1382
    /**
1383
     * @param string $value
1384
     */
1385
    public function setOnSuccessMessage($value)
1386
    {
1387
        $this->onSuccessMessage = $value;
1388
    }
1389
1390
    /**
1391
     * @param string $value
1392
     */
1393
    public function setOnFailedMessage($value)
1394
    {
1395
        $this->onFailedMessage = $value;
1396
    }
1397
1398
    /**
1399
     * @param $value
1400
     */
1401
    public function setModelType($value)
1402
    {
1403
        $this->modelType = (int) $value;
1404
    }
1405
1406
    /**
1407
     * @param int $value
1408
     */
1409
    public function setQuestionSelectionType($value)
1410
    {
1411
        $this->questionSelectionType = (int) $value;
1412
    }
1413
1414
    /**
1415
     * @return int
1416
     */
1417
    public function getQuestionSelectionType()
1418
    {
1419
        return (int) $this->questionSelectionType;
1420
    }
1421
1422
    /**
1423
     * @param array $categories
1424
     */
1425
    public function updateCategories($categories)
1426
    {
1427
        if (!empty($categories)) {
1428
            $categories = array_map('intval', $categories);
1429
            $this->categories = $categories;
1430
        }
1431
    }
1432
1433
    /**
1434
     * changes the exercise sound file.
1435
     *
1436
     * @author Olivier Brouckaert
1437
     *
1438
     * @param string $sound  - exercise sound file
1439
     * @param string $delete - ask to delete the file
1440
     */
1441
    public function updateSound($sound, $delete)
1442
    {
1443
        global $audioPath, $documentPath;
1444
        $TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
1445
1446
        if ($sound['size'] && (strstr($sound['type'], 'audio') || strstr($sound['type'], 'video'))) {
1447
            $this->sound = $sound['name'];
1448
1449
            if (@move_uploaded_file($sound['tmp_name'], $audioPath.'/'.$this->sound)) {
1450
                $sql = "SELECT 1 FROM $TBL_DOCUMENT
1451
                        WHERE
1452
                            c_id = ".$this->course_id." AND
1453
                            path = '".str_replace($documentPath, '', $audioPath).'/'.$this->sound."'";
1454
                $result = Database::query($sql);
1455
1456
                if (!Database::num_rows($result)) {
1457
                    $id = add_document(
1458
                        $this->course,
1459
                        str_replace($documentPath, '', $audioPath).'/'.$this->sound,
1460
                        'file',
1461
                        $sound['size'],
1462
                        $sound['name']
1463
                    );
1464
                    api_item_property_update(
1465
                        $this->course,
1466
                        TOOL_DOCUMENT,
1467
                        $id,
1468
                        'DocumentAdded',
1469
                        api_get_user_id()
1470
                    );
1471
                    item_property_update_on_folder(
1472
                        $this->course,
1473
                        str_replace($documentPath, '', $audioPath),
1474
                        api_get_user_id()
1475
                    );
1476
                }
1477
            }
1478
        } elseif ($delete && is_file($audioPath.'/'.$this->sound)) {
1479
            $this->sound = '';
1480
        }
1481
    }
1482
1483
    /**
1484
     * changes the exercise type.
1485
     *
1486
     * @author Olivier Brouckaert
1487
     *
1488
     * @param int $type - exercise type
1489
     */
1490
    public function updateType($type)
1491
    {
1492
        $this->type = $type;
1493
    }
1494
1495
    /**
1496
     * sets to 0 if questions are not selected randomly
1497
     * if questions are selected randomly, sets the draws.
1498
     *
1499
     * @author Olivier Brouckaert
1500
     *
1501
     * @param int $random - 0 if not random, otherwise the draws
1502
     */
1503
    public function setRandom($random)
1504
    {
1505
        $this->random = $random;
1506
    }
1507
1508
    /**
1509
     * sets to 0 if answers are not selected randomly
1510
     * if answers are selected randomly.
1511
     *
1512
     * @author Juan Carlos Rana
1513
     *
1514
     * @param int $random_answers - random answers
1515
     */
1516
    public function updateRandomAnswers($random_answers)
1517
    {
1518
        $this->random_answers = $random_answers;
1519
    }
1520
1521
    /**
1522
     * enables the exercise.
1523
     *
1524
     * @author Olivier Brouckaert
1525
     */
1526
    public function enable()
1527
    {
1528
        $this->active = 1;
1529
    }
1530
1531
    /**
1532
     * disables the exercise.
1533
     *
1534
     * @author Olivier Brouckaert
1535
     */
1536
    public function disable()
1537
    {
1538
        $this->active = 0;
1539
    }
1540
1541
    /**
1542
     * Set disable results.
1543
     */
1544
    public function disable_results()
1545
    {
1546
        $this->results_disabled = true;
1547
    }
1548
1549
    /**
1550
     * Enable results.
1551
     */
1552
    public function enable_results()
1553
    {
1554
        $this->results_disabled = false;
1555
    }
1556
1557
    /**
1558
     * @param int $results_disabled
1559
     */
1560
    public function updateResultsDisabled($results_disabled)
1561
    {
1562
        $this->results_disabled = (int) $results_disabled;
1563
    }
1564
1565
    /**
1566
     * updates the exercise in the data base.
1567
     *
1568
     * @param string $type_e
1569
     *
1570
     * @author Olivier Brouckaert
1571
     */
1572
    public function save($type_e = '')
1573
    {
1574
        $_course = $this->course;
1575
        $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
1576
1577
        $id = $this->id;
1578
        $exercise = $this->exercise;
1579
        $description = $this->description;
1580
        $sound = $this->sound;
1581
        $type = $this->type;
1582
        $attempts = isset($this->attempts) ? $this->attempts : 0;
1583
        $feedback_type = isset($this->feedback_type) ? $this->feedback_type : 0;
1584
        $random = $this->random;
1585
        $random_answers = $this->random_answers;
1586
        $active = $this->active;
1587
        $propagate_neg = (int) $this->propagate_neg;
1588
        $saveCorrectAnswers = isset($this->saveCorrectAnswers) ? (int) $this->saveCorrectAnswers : 0;
1589
        $review_answers = isset($this->review_answers) && $this->review_answers ? 1 : 0;
1590
        $randomByCat = (int) $this->randomByCat;
1591
        $text_when_finished = $this->text_when_finished;
1592
        $display_category_name = (int) $this->display_category_name;
1593
        $pass_percentage = (int) $this->pass_percentage;
1594
        $session_id = $this->sessionId;
1595
1596
        // If direct we do not show results
1597
        $results_disabled = (int) $this->results_disabled;
1598
        if (in_array($feedback_type, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1599
            $results_disabled = 0;
1600
        }
1601
        $expired_time = (int) $this->expired_time;
1602
1603
        // Exercise already exists
1604
        if ($id) {
1605
            // we prepare date in the database using the api_get_utc_datetime() function
1606
            $start_time = null;
1607
            if (!empty($this->start_time)) {
1608
                $start_time = $this->start_time;
1609
            }
1610
1611
            $end_time = null;
1612
            if (!empty($this->end_time)) {
1613
                $end_time = $this->end_time;
1614
            }
1615
1616
            $params = [
1617
                'title' => $exercise,
1618
                'description' => $description,
1619
            ];
1620
1621
            $paramsExtra = [];
1622
            if ($type_e != 'simple') {
1623
                $paramsExtra = [
1624
                    'sound' => $sound,
1625
                    'type' => $type,
1626
                    'random' => $random,
1627
                    'random_answers' => $random_answers,
1628
                    'active' => $active,
1629
                    'feedback_type' => $feedback_type,
1630
                    'start_time' => $start_time,
1631
                    'end_time' => $end_time,
1632
                    'max_attempt' => $attempts,
1633
                    'expired_time' => $expired_time,
1634
                    'propagate_neg' => $propagate_neg,
1635
                    'save_correct_answers' => $saveCorrectAnswers,
1636
                    'review_answers' => $review_answers,
1637
                    'random_by_category' => $randomByCat,
1638
                    'text_when_finished' => $text_when_finished,
1639
                    'display_category_name' => $display_category_name,
1640
                    'pass_percentage' => $pass_percentage,
1641
                    'results_disabled' => $results_disabled,
1642
                    'question_selection_type' => $this->getQuestionSelectionType(),
1643
                    'hide_question_title' => $this->getHideQuestionTitle(),
1644
                ];
1645
1646
                $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
1647
                if ($allow === true) {
1648
                    $paramsExtra['show_previous_button'] = $this->showPreviousButton();
1649
                }
1650
1651
                if (api_get_configuration_value('quiz_prevent_backwards_move')) {
1652
                    $paramsExtra['prevent_backwards'] = $this->getPreventBackwards();
1653
                }
1654
1655
                $allow = api_get_configuration_value('allow_exercise_categories');
1656
                if ($allow === true) {
1657
                    if (!empty($this->getExerciseCategoryId())) {
1658
                        $paramsExtra['exercise_category_id'] = $this->getExerciseCategoryId();
1659
                    }
1660
                }
1661
1662
                $allow = api_get_configuration_value('allow_notification_setting_per_exercise');
1663
                if ($allow === true) {
1664
                    $notifications = $this->getNotifications();
1665
                    $notifications = implode(',', $notifications);
1666
                    $paramsExtra['notifications'] = $notifications;
1667
                }
1668
1669
                $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
1670
                if ($pageConfig && !empty($this->pageResultConfiguration)) {
1671
                    $paramsExtra['page_result_configuration'] = $this->pageResultConfiguration;
1672
                }
1673
            }
1674
1675
            $params = array_merge($params, $paramsExtra);
1676
1677
            Database::update(
1678
                $TBL_EXERCISES,
1679
                $params,
1680
                ['c_id = ? AND id = ?' => [$this->course_id, $id]]
1681
            );
1682
1683
            // update into the item_property table
1684
            api_item_property_update(
1685
                $_course,
1686
                TOOL_QUIZ,
1687
                $id,
1688
                'QuizUpdated',
1689
                api_get_user_id()
1690
            );
1691
1692
            if (api_get_setting('search_enabled') === 'true') {
1693
                $this->search_engine_edit();
1694
            }
1695
        } else {
1696
            // Creates a new exercise
1697
            // In this case of new exercise, we don't do the api_get_utc_datetime()
1698
            // for date because, bellow, we call function api_set_default_visibility()
1699
            // In this function, api_set_default_visibility,
1700
            // the Quiz is saved too, with an $id and api_get_utc_datetime() is done.
1701
            // If we do it now, it will be done twice (cf. https://support.chamilo.org/issues/6586)
1702
            $start_time = null;
1703
            if (!empty($this->start_time)) {
1704
                $start_time = $this->start_time;
1705
            }
1706
1707
            $end_time = null;
1708
            if (!empty($this->end_time)) {
1709
                $end_time = $this->end_time;
1710
            }
1711
1712
            $params = [
1713
                'c_id' => $this->course_id,
1714
                'start_time' => $start_time,
1715
                'end_time' => $end_time,
1716
                'title' => $exercise,
1717
                'description' => $description,
1718
                'sound' => $sound,
1719
                'type' => $type,
1720
                'random' => $random,
1721
                'random_answers' => $random_answers,
1722
                'active' => $active,
1723
                'results_disabled' => $results_disabled,
1724
                'max_attempt' => $attempts,
1725
                'feedback_type' => $feedback_type,
1726
                'expired_time' => $expired_time,
1727
                'session_id' => $session_id,
1728
                'review_answers' => $review_answers,
1729
                'random_by_category' => $randomByCat,
1730
                'text_when_finished' => $text_when_finished,
1731
                'display_category_name' => $display_category_name,
1732
                'pass_percentage' => $pass_percentage,
1733
                'save_correct_answers' => $saveCorrectAnswers,
1734
                'propagate_neg' => $propagate_neg,
1735
                'hide_question_title' => $this->getHideQuestionTitle(),
1736
            ];
1737
1738
            $allow = api_get_configuration_value('allow_exercise_categories');
1739
            if (true === $allow) {
1740
                if (!empty($this->getExerciseCategoryId())) {
1741
                    $params['exercise_category_id'] = $this->getExerciseCategoryId();
1742
                }
1743
            }
1744
1745
            if (api_get_configuration_value('quiz_prevent_backwards_move')) {
1746
                $params['prevent_backwards'] = $this->getPreventBackwards();
1747
            }
1748
1749
            $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
1750
            if (true === $allow) {
1751
                $params['show_previous_button'] = $this->showPreviousButton();
1752
            }
1753
1754
            $allow = api_get_configuration_value('allow_notification_setting_per_exercise');
1755
            if (true === $allow) {
1756
                $notifications = $this->getNotifications();
1757
                $params['notifications'] = '';
1758
                if (!empty($notifications)) {
1759
                    $notifications = implode(',', $notifications);
1760
                    $params['notifications'] = $notifications;
1761
                }
1762
            }
1763
1764
            $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
1765
            if ($pageConfig && !empty($this->pageResultConfiguration)) {
1766
                $params['page_result_configuration'] = $this->pageResultConfiguration;
1767
            }
1768
1769
            $this->id = $this->iId = Database::insert($TBL_EXERCISES, $params);
1770
1771
            if ($this->id) {
1772
                $sql = "UPDATE $TBL_EXERCISES SET id = iid WHERE iid = {$this->id} ";
1773
                Database::query($sql);
1774
1775
                $sql = "UPDATE $TBL_EXERCISES
1776
                        SET question_selection_type= ".$this->getQuestionSelectionType()."
1777
                        WHERE id = ".$this->id." AND c_id = ".$this->course_id;
1778
                Database::query($sql);
1779
1780
                // insert into the item_property table
1781
                api_item_property_update(
1782
                    $this->course,
1783
                    TOOL_QUIZ,
1784
                    $this->id,
1785
                    'QuizAdded',
1786
                    api_get_user_id()
1787
                );
1788
1789
                // This function save the quiz again, carefull about start_time
1790
                // and end_time if you remove this line (see above)
1791
                api_set_default_visibility(
1792
                    $this->id,
1793
                    TOOL_QUIZ,
1794
                    null,
1795
                    $this->course
1796
                );
1797
1798
                if (api_get_setting('search_enabled') === 'true' && extension_loaded('xapian')) {
1799
                    $this->search_engine_save();
1800
                }
1801
            }
1802
        }
1803
1804
        $this->save_categories_in_exercise($this->categories);
1805
1806
        return $this->iId;
1807
    }
1808
1809
    /**
1810
     * Updates question position.
1811
     *
1812
     * @return bool
1813
     */
1814
    public function update_question_positions()
1815
    {
1816
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1817
        // Fixes #3483 when updating order
1818
        $questionList = $this->selectQuestionList(true);
1819
1820
        $this->id = (int) $this->id;
1821
1822
        if (empty($this->id)) {
1823
            return false;
1824
        }
1825
1826
        if (!empty($questionList)) {
1827
            foreach ($questionList as $position => $questionId) {
1828
                $position = (int) $position;
1829
                $questionId = (int) $questionId;
1830
                $sql = "UPDATE $table SET
1831
                            question_order ='".$position."'
1832
                        WHERE
1833
                            c_id = ".$this->course_id." AND
1834
                            question_id = ".$questionId." AND
1835
                            exercice_id=".$this->id;
1836
                Database::query($sql);
1837
            }
1838
        }
1839
1840
        return true;
1841
    }
1842
1843
    /**
1844
     * Adds a question into the question list.
1845
     *
1846
     * @author Olivier Brouckaert
1847
     *
1848
     * @param int $questionId - question ID
1849
     *
1850
     * @return bool - true if the question has been added, otherwise false
1851
     */
1852
    public function addToList($questionId)
1853
    {
1854
        // checks if the question ID is not in the list
1855
        if (!$this->isInList($questionId)) {
1856
            // selects the max position
1857
            if (!$this->selectNbrQuestions()) {
1858
                $pos = 1;
1859
            } else {
1860
                if (is_array($this->questionList)) {
1861
                    $pos = max(array_keys($this->questionList)) + 1;
1862
                }
1863
            }
1864
            $this->questionList[$pos] = $questionId;
1865
1866
            return true;
1867
        }
1868
1869
        return false;
1870
    }
1871
1872
    /**
1873
     * removes a question from the question list.
1874
     *
1875
     * @author Olivier Brouckaert
1876
     *
1877
     * @param int $questionId - question ID
1878
     *
1879
     * @return bool - true if the question has been removed, otherwise false
1880
     */
1881
    public function removeFromList($questionId)
1882
    {
1883
        // searches the position of the question ID in the list
1884
        $pos = array_search($questionId, $this->questionList);
1885
        // question not found
1886
        if (false === $pos) {
1887
            return false;
1888
        } else {
1889
            // dont reduce the number of random question if we use random by category option, or if
1890
            // random all questions
1891
            if ($this->isRandom() && 0 == $this->isRandomByCat()) {
1892
                if (count($this->questionList) >= $this->random && $this->random > 0) {
1893
                    $this->random--;
1894
                    $this->save();
1895
                }
1896
            }
1897
            // deletes the position from the array containing the wanted question ID
1898
            unset($this->questionList[$pos]);
1899
1900
            return true;
1901
        }
1902
    }
1903
1904
    /**
1905
     * deletes the exercise from the database
1906
     * Notice : leaves the question in the data base.
1907
     *
1908
     * @author Olivier Brouckaert
1909
     */
1910
    public function delete()
1911
    {
1912
        $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
1913
1914
        if ($limitTeacherAccess && !api_is_platform_admin()) {
1915
            return false;
1916
        }
1917
1918
        $locked = api_resource_is_locked_by_gradebook(
1919
            $this->id,
1920
            LINK_EXERCISE
1921
        );
1922
1923
        if ($locked) {
1924
            return false;
1925
        }
1926
1927
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
1928
        $sql = "UPDATE $table SET active='-1'
1929
                WHERE c_id = ".$this->course_id." AND id = ".intval($this->id);
1930
        Database::query($sql);
1931
1932
        api_item_property_update(
1933
            $this->course,
1934
            TOOL_QUIZ,
1935
            $this->id,
1936
            'QuizDeleted',
1937
            api_get_user_id()
1938
        );
1939
        api_item_property_update(
1940
            $this->course,
1941
            TOOL_QUIZ,
1942
            $this->id,
1943
            'delete',
1944
            api_get_user_id()
1945
        );
1946
1947
        Skill::deleteSkillsFromItem($this->iId, ITEM_TYPE_EXERCISE);
1948
1949
        if (api_get_setting('search_enabled') === 'true' &&
1950
            extension_loaded('xapian')
1951
        ) {
1952
            $this->search_engine_delete();
1953
        }
1954
1955
        $linkInfo = GradebookUtils::isResourceInCourseGradebook(
1956
            $this->course['code'],
1957
            LINK_EXERCISE,
1958
            $this->id,
1959
            $this->sessionId
1960
        );
1961
        if ($linkInfo !== false) {
1962
            GradebookUtils::remove_resource_from_course_gradebook($linkInfo['id']);
1963
        }
1964
1965
        return true;
1966
    }
1967
1968
    /**
1969
     * Creates the form to create / edit an exercise.
1970
     *
1971
     * @param FormValidator $form
1972
     * @param string        $type
1973
     */
1974
    public function createForm($form, $type = 'full')
1975
    {
1976
        if (empty($type)) {
1977
            $type = 'full';
1978
        }
1979
1980
        // Form title
1981
        $form_title = get_lang('NewEx');
1982
        if (!empty($_GET['exerciseId'])) {
1983
            $form_title = get_lang('ModifyExercise');
1984
        }
1985
1986
        $form->addHeader($form_title);
1987
1988
        // Title.
1989
        if (api_get_configuration_value('save_titles_as_html')) {
1990
            $form->addHtmlEditor(
1991
                'exerciseTitle',
1992
                get_lang('ExerciseName'),
1993
                false,
1994
                false,
1995
                ['ToolbarSet' => 'TitleAsHtml']
1996
            );
1997
        } else {
1998
            $form->addElement(
1999
                'text',
2000
                'exerciseTitle',
2001
                get_lang('ExerciseName'),
2002
                ['id' => 'exercise_title']
2003
            );
2004
        }
2005
2006
        $form->addElement('advanced_settings', 'advanced_params', get_lang('AdvancedParameters'));
2007
        $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
2008
2009
        if (api_get_configuration_value('allow_exercise_categories')) {
2010
            $categoryManager = new ExerciseCategoryManager();
2011
            $categories = $categoryManager->getCategories(api_get_course_int_id());
2012
            $options = [];
2013
            if (!empty($categories)) {
2014
                /** @var CExerciseCategory $category */
2015
                foreach ($categories as $category) {
2016
                    $options[$category->getId()] = $category->getName();
2017
                }
2018
            }
2019
2020
            $form->addSelect(
2021
                'exercise_category_id',
2022
                get_lang('Category'),
2023
                $options,
2024
                ['placeholder' => get_lang('SelectAnOption')]
2025
            );
2026
        }
2027
2028
        $editor_config = [
2029
            'ToolbarSet' => 'TestQuestionDescription',
2030
            'Width' => '100%',
2031
            'Height' => '150',
2032
        ];
2033
2034
        if (is_array($type)) {
2035
            $editor_config = array_merge($editor_config, $type);
2036
        }
2037
2038
        $form->addHtmlEditor(
2039
            'exerciseDescription',
2040
            get_lang('ExerciseDescription'),
2041
            false,
2042
            false,
2043
            $editor_config
2044
        );
2045
2046
        $skillList = [];
2047
        if ('full' === $type) {
2048
            // Can't modify a DirectFeedback question.
2049
            if (!in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
2050
                $this->setResultFeedbackGroup($form);
2051
2052
                // Type of results display on the final page
2053
                $this->setResultDisabledGroup($form);
2054
2055
                // Type of questions disposition on page
2056
                $radios = [];
2057
                $radios[] = $form->createElement(
2058
                    'radio',
2059
                    'exerciseType',
2060
                    null,
2061
                    get_lang('SimpleExercise'),
2062
                    '1',
2063
                    [
2064
                        'onclick' => 'check_per_page_all()',
2065
                        'id' => 'option_page_all',
2066
                    ]
2067
                );
2068
                $radios[] = $form->createElement(
2069
                    'radio',
2070
                    'exerciseType',
2071
                    null,
2072
                    get_lang('SequentialExercise'),
2073
                    '2',
2074
                    [
2075
                        'onclick' => 'check_per_page_one()',
2076
                        'id' => 'option_page_one',
2077
                    ]
2078
                );
2079
2080
                $form->addGroup($radios, null, get_lang('QuestionsPerPage'));
2081
            } else {
2082
                // if is Direct feedback but has not questions we can allow to modify the question type
2083
                if (empty($this->iId) || 0 === $this->getQuestionCount()) {
2084
                    $this->setResultFeedbackGroup($form);
2085
                    $this->setResultDisabledGroup($form);
2086
2087
                    // Type of questions disposition on page
2088
                    $radios = [];
2089
                    $radios[] = $form->createElement('radio', 'exerciseType', null, get_lang('SimpleExercise'), '1');
2090
                    $radios[] = $form->createElement(
2091
                        'radio',
2092
                        'exerciseType',
2093
                        null,
2094
                        get_lang('SequentialExercise'),
2095
                        '2'
2096
                    );
2097
                    $form->addGroup($radios, null, get_lang('ExerciseType'));
2098
                } else {
2099
                    $this->setResultFeedbackGroup($form, true);
2100
                    $group = $this->setResultDisabledGroup($form);
2101
                    $group->freeze();
2102
2103
                    // we force the options to the DirectFeedback exercisetype
2104
                    //$form->addElement('hidden', 'exerciseFeedbackType', $this->getFeedbackType());
2105
                    //$form->addElement('hidden', 'exerciseType', ONE_PER_PAGE);
2106
2107
                    // Type of questions disposition on page
2108
                    $radios[] = $form->createElement(
2109
                        'radio',
2110
                        'exerciseType',
2111
                        null,
2112
                        get_lang('SimpleExercise'),
2113
                        '1',
2114
                        [
2115
                            'onclick' => 'check_per_page_all()',
2116
                            'id' => 'option_page_all',
2117
                        ]
2118
                    );
2119
                    $radios[] = $form->createElement(
2120
                        'radio',
2121
                        'exerciseType',
2122
                        null,
2123
                        get_lang('SequentialExercise'),
2124
                        '2',
2125
                        [
2126
                            'onclick' => 'check_per_page_one()',
2127
                            'id' => 'option_page_one',
2128
                        ]
2129
                    );
2130
2131
                    $type_group = $form->addGroup($radios, null, get_lang('QuestionsPerPage'));
2132
                    $type_group->freeze();
2133
                }
2134
            }
2135
2136
            $option = [
2137
                EX_Q_SELECTION_ORDERED => get_lang('OrderedByUser'),
2138
                //  Defined by user
2139
                EX_Q_SELECTION_RANDOM => get_lang('Random'),
2140
                // 1-10, All
2141
                'per_categories' => '--------'.get_lang('UsingCategories').'----------',
2142
                // Base (A 123 {3} B 456 {3} C 789{2} D 0{0}) --> Matrix {3, 3, 2, 0}
2143
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED => get_lang('OrderedCategoriesAlphabeticallyWithQuestionsOrdered'),
2144
                // A 123 B 456 C 78 (0, 1, all)
2145
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED => get_lang('RandomCategoriesWithQuestionsOrdered'),
2146
                // C 78 B 456 A 123
2147
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM => get_lang('OrderedCategoriesAlphabeticallyWithRandomQuestions'),
2148
                // A 321 B 654 C 87
2149
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM => get_lang('RandomCategoriesWithRandomQuestions'),
2150
                // C 87 B 654 A 321
2151
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED => get_lang('RandomCategoriesWithQuestionsOrderedNoQuestionGrouped'),
2152
                /*    B 456 C 78 A 123
2153
                        456 78 123
2154
                        123 456 78
2155
                */
2156
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED => get_lang('RandomCategoriesWithRandomQuestionsNoQuestionGrouped'),
2157
                /*
2158
                    A 123 B 456 C 78
2159
                    B 456 C 78 A 123
2160
                    B 654 C 87 A 321
2161
                    654 87 321
2162
                    165 842 73
2163
                */
2164
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED => get_lang('OrderedCategoriesByParentWithQuestionsOrdered'),
2165
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM => get_lang('OrderedCategoriesByParentWithQuestionsRandom'),
2166
            ];
2167
2168
            $form->addElement(
2169
                'select',
2170
                'question_selection_type',
2171
                [get_lang('QuestionSelection')],
2172
                $option,
2173
                [
2174
                    'id' => 'questionSelection',
2175
                    'onchange' => 'checkQuestionSelection()',
2176
                ]
2177
            );
2178
2179
            $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
2180
            if ($pageConfig) {
2181
                $group = [
2182
                    $form->createElement(
2183
                        'checkbox',
2184
                        'hide_expected_answer',
2185
                        null,
2186
                        get_lang('HideExpectedAnswer')
2187
                    ),
2188
                    $form->createElement(
2189
                        'checkbox',
2190
                        'hide_total_score',
2191
                        null,
2192
                        get_lang('HideTotalScore')
2193
                    ),
2194
                    $form->createElement(
2195
                        'checkbox',
2196
                        'hide_question_score',
2197
                        null,
2198
                        get_lang('HideQuestionScore')
2199
                    ),
2200
                ];
2201
                $form->addGroup($group, null, get_lang('ResultsConfigurationPage'));
2202
            }
2203
2204
            $displayMatrix = 'none';
2205
            $displayRandom = 'none';
2206
            $selectionType = $this->getQuestionSelectionType();
2207
            switch ($selectionType) {
2208
                case EX_Q_SELECTION_RANDOM:
2209
                    $displayRandom = 'block';
2210
                    break;
2211
                case $selectionType >= EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED:
2212
                    $displayMatrix = 'block';
2213
                    break;
2214
            }
2215
2216
            $form->addHtml('<div id="hidden_random" style="display:'.$displayRandom.'">');
2217
            // Number of random question.
2218
            $max = ($this->id > 0) ? $this->getQuestionCount() : 10;
2219
            $option = range(0, $max);
2220
            $option[0] = get_lang('No');
2221
            $option[-1] = get_lang('AllQuestionsShort');
2222
            $form->addElement(
2223
                'select',
2224
                'randomQuestions',
2225
                [
2226
                    get_lang('RandomQuestions'),
2227
                    get_lang('RandomQuestionsHelp'),
2228
                ],
2229
                $option,
2230
                ['id' => 'randomQuestions']
2231
            );
2232
            $form->addHtml('</div>');
2233
            $form->addHtml('<div id="hidden_matrix" style="display:'.$displayMatrix.'">');
2234
2235
            // Category selection.
2236
            $cat = new TestCategory();
2237
            $cat_form = $cat->returnCategoryForm($this);
2238
            if (empty($cat_form)) {
2239
                $cat_form = '<span class="label label-warning">'.get_lang('NoCategoriesDefined').'</span>';
2240
            }
2241
            $form->addElement('label', null, $cat_form);
2242
            $form->addHtml('</div>');
2243
2244
            // Random answers.
2245
            $radios_random_answers = [
2246
                $form->createElement('radio', 'randomAnswers', null, get_lang('Yes'), '1'),
2247
                $form->createElement('radio', 'randomAnswers', null, get_lang('No'), '0'),
2248
            ];
2249
            $form->addGroup($radios_random_answers, null, get_lang('RandomAnswers'));
2250
2251
            // Category name.
2252
            $radio_display_cat_name = [
2253
                $form->createElement('radio', 'display_category_name', null, get_lang('Yes'), '1'),
2254
                $form->createElement('radio', 'display_category_name', null, get_lang('No'), '0'),
2255
            ];
2256
            $form->addGroup($radio_display_cat_name, null, get_lang('QuestionDisplayCategoryName'));
2257
2258
            // Hide question title.
2259
            $group = [
2260
                $form->createElement('radio', 'hide_question_title', null, get_lang('Yes'), '1'),
2261
                $form->createElement('radio', 'hide_question_title', null, get_lang('No'), '0'),
2262
            ];
2263
            $form->addGroup($group, null, get_lang('HideQuestionTitle'));
2264
2265
            $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
2266
            if (true === $allow) {
2267
                // Hide question title.
2268
                $group = [
2269
                    $form->createElement(
2270
                        'radio',
2271
                        'show_previous_button',
2272
                        null,
2273
                        get_lang('Yes'),
2274
                        '1'
2275
                    ),
2276
                    $form->createElement(
2277
                        'radio',
2278
                        'show_previous_button',
2279
                        null,
2280
                        get_lang('No'),
2281
                        '0'
2282
                    ),
2283
                ];
2284
                $form->addGroup($group, null, get_lang('ShowPreviousButton'));
2285
            }
2286
2287
            $form->addElement(
2288
                'number',
2289
                'exerciseAttempts',
2290
                get_lang('ExerciseAttempts'),
2291
                null,
2292
                ['id' => 'exerciseAttempts']
2293
            );
2294
2295
            // Exercise time limit
2296
            $form->addElement(
2297
                'checkbox',
2298
                'activate_start_date_check',
2299
                null,
2300
                get_lang('EnableStartTime'),
2301
                ['onclick' => 'activate_start_date()']
2302
            );
2303
2304
            if (!empty($this->start_time)) {
2305
                $form->addElement('html', '<div id="start_date_div" style="display:block;">');
2306
            } else {
2307
                $form->addElement('html', '<div id="start_date_div" style="display:none;">');
2308
            }
2309
2310
            $form->addElement('date_time_picker', 'start_time');
2311
            $form->addElement('html', '</div>');
2312
            $form->addElement(
2313
                'checkbox',
2314
                'activate_end_date_check',
2315
                null,
2316
                get_lang('EnableEndTime'),
2317
                ['onclick' => 'activate_end_date()']
2318
            );
2319
2320
            if (!empty($this->end_time)) {
2321
                $form->addHtml('<div id="end_date_div" style="display:block;">');
2322
            } else {
2323
                $form->addHtml('<div id="end_date_div" style="display:none;">');
2324
            }
2325
2326
            $form->addElement('date_time_picker', 'end_time');
2327
            $form->addElement('html', '</div>');
2328
2329
            $display = 'block';
2330
            $form->addElement(
2331
                'checkbox',
2332
                'propagate_neg',
2333
                null,
2334
                get_lang('PropagateNegativeResults')
2335
            );
2336
2337
            if (api_get_configuration_value('allow_quiz_save_correct_options')) {
2338
                $options = [
2339
                    '' => get_lang('SelectAnOption'),
2340
                    1 => get_lang('SaveTheCorrectAnswersForTheNextAttempt'),
2341
                    2 => get_lang('SaveAllAnswers'),
2342
                ];
2343
                $form->addSelect(
2344
                    'save_correct_answers',
2345
                    get_lang('SaveAnswers'),
2346
                    $options
2347
                );
2348
            } else {
2349
                $form->addCheckBox(
2350
                    'save_correct_answers',
2351
                    null,
2352
                    get_lang('SaveTheCorrectAnswersForTheNextAttempt')
2353
                );
2354
            }
2355
2356
            $form->addElement('html', '<div class="clear">&nbsp;</div>');
2357
            $form->addElement('checkbox', 'review_answers', null, get_lang('ReviewAnswers'));
2358
            $form->addElement('html', '<div id="divtimecontrol"  style="display:'.$display.';">');
2359
2360
            // Timer control
2361
            $form->addElement(
2362
                'checkbox',
2363
                'enabletimercontrol',
2364
                null,
2365
                get_lang('EnableTimerControl'),
2366
                [
2367
                    'onclick' => 'option_time_expired()',
2368
                    'id' => 'enabletimercontrol',
2369
                    'onload' => 'check_load_time()',
2370
                ]
2371
            );
2372
2373
            $expired_date = (int) $this->selectExpiredTime();
2374
2375
            if (($expired_date != '0')) {
2376
                $form->addElement('html', '<div id="timercontrol" style="display:block;">');
2377
            } else {
2378
                $form->addElement('html', '<div id="timercontrol" style="display:none;">');
2379
            }
2380
            $form->addText(
2381
                'enabletimercontroltotalminutes',
2382
                get_lang('ExerciseTotalDurationInMinutes'),
2383
                false,
2384
                [
2385
                    'id' => 'enabletimercontroltotalminutes',
2386
                    'cols-size' => [2, 2, 8],
2387
                ]
2388
            );
2389
            $form->addElement('html', '</div>');
2390
2391
            if (api_get_configuration_value('quiz_prevent_backwards_move')) {
2392
                $form->addCheckBox(
2393
                    'prevent_backwards',
2394
                    null,
2395
                    get_lang('QuizPreventBackwards')
2396
                );
2397
            }
2398
2399
            $form->addElement(
2400
                'text',
2401
                'pass_percentage',
2402
                [get_lang('PassPercentage'), null, '%'],
2403
                ['id' => 'pass_percentage']
2404
            );
2405
2406
            $form->addRule('pass_percentage', get_lang('Numeric'), 'numeric');
2407
            $form->addRule('pass_percentage', get_lang('ValueTooSmall'), 'min_numeric_length', 0);
2408
            $form->addRule('pass_percentage', get_lang('ValueTooBig'), 'max_numeric_length', 100);
2409
2410
            // add the text_when_finished textbox
2411
            $form->addHtmlEditor(
2412
                'text_when_finished',
2413
                get_lang('TextWhenFinished'),
2414
                false,
2415
                false,
2416
                $editor_config
2417
            );
2418
2419
            $allow = api_get_configuration_value('allow_notification_setting_per_exercise');
2420
            if ($allow === true) {
2421
                $settings = ExerciseLib::getNotificationSettings();
2422
                $group = [];
2423
                foreach ($settings as $itemId => $label) {
2424
                    $group[] = $form->createElement(
2425
                        'checkbox',
2426
                        'notifications[]',
2427
                        null,
2428
                        $label,
2429
                        ['value' => $itemId]
2430
                    );
2431
                }
2432
                $form->addGroup($group, '', [get_lang('EmailNotifications')]);
2433
            }
2434
2435
            $form->addCheckBox(
2436
                'update_title_in_lps',
2437
                null,
2438
                get_lang('UpdateTitleInLps')
2439
            );
2440
2441
            $defaults = [];
2442
            if (api_get_setting('search_enabled') === 'true') {
2443
                require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
2444
                $form->addElement('checkbox', 'index_document', '', get_lang('SearchFeatureDoIndexDocument'));
2445
                $form->addSelectLanguage('language', get_lang('SearchFeatureDocumentLanguage'));
2446
                $specific_fields = get_specific_field_list();
2447
2448
                foreach ($specific_fields as $specific_field) {
2449
                    $form->addElement('text', $specific_field['code'], $specific_field['name']);
2450
                    $filter = [
2451
                        'c_id' => api_get_course_int_id(),
2452
                        'field_id' => $specific_field['id'],
2453
                        'ref_id' => $this->id,
2454
                        'tool_id' => "'".TOOL_QUIZ."'",
2455
                    ];
2456
                    $values = get_specific_field_values_list($filter, ['value']);
2457
                    if (!empty($values)) {
2458
                        $arr_str_values = [];
2459
                        foreach ($values as $value) {
2460
                            $arr_str_values[] = $value['value'];
2461
                        }
2462
                        $defaults[$specific_field['code']] = implode(', ', $arr_str_values);
2463
                    }
2464
                }
2465
            }
2466
2467
            $skillList = Skill::addSkillsToForm($form, ITEM_TYPE_EXERCISE, $this->iId);
2468
2469
            $extraField = new ExtraField('exercise');
2470
            $extraField->addElements(
2471
                $form,
2472
                $this->iId,
2473
                ['notifications'], //exclude
2474
                false, // filter
2475
                false, // tag as select
2476
                [], //show only fields
2477
                [], // order fields
2478
                [] // extra data
2479
            );
2480
2481
            $settings = api_get_configuration_value('exercise_finished_notification_settings');
2482
            if (!empty($settings)) {
2483
                $options = [];
2484
                foreach ($settings as $name => $data) {
2485
                    $options[$name] = $name;
2486
                }
2487
                $form->addSelect(
2488
                    'extra_notifications',
2489
                    get_lang('Notifications'),
2490
                    $options,
2491
                    ['placeholder' => get_lang('SelectAnOption')]
2492
                );
2493
            }
2494
            $form->addElement('html', '</div>'); //End advanced setting
2495
            $form->addElement('html', '</div>');
2496
        }
2497
2498
        // submit
2499
        if (isset($_GET['exerciseId'])) {
2500
            $form->addButtonSave(get_lang('ModifyExercise'), 'submitExercise');
2501
        } else {
2502
            $form->addButtonUpdate(get_lang('ProcedToQuestions'), 'submitExercise');
2503
        }
2504
2505
        $form->addRule('exerciseTitle', get_lang('GiveExerciseName'), 'required');
2506
2507
        // defaults
2508
        if ($type == 'full') {
2509
            // rules
2510
            $form->addRule('exerciseAttempts', get_lang('Numeric'), 'numeric');
2511
            $form->addRule('start_time', get_lang('InvalidDate'), 'datetime');
2512
            $form->addRule('end_time', get_lang('InvalidDate'), 'datetime');
2513
2514
            if ($this->id > 0) {
2515
                $defaults['randomQuestions'] = $this->random;
2516
                $defaults['randomAnswers'] = $this->getRandomAnswers();
2517
                $defaults['exerciseType'] = $this->selectType();
2518
                $defaults['exerciseTitle'] = $this->get_formated_title();
2519
                $defaults['exerciseDescription'] = $this->selectDescription();
2520
                $defaults['exerciseAttempts'] = $this->selectAttempts();
2521
                $defaults['exerciseFeedbackType'] = $this->getFeedbackType();
2522
                $defaults['results_disabled'] = $this->selectResultsDisabled();
2523
                $defaults['propagate_neg'] = $this->selectPropagateNeg();
2524
                $defaults['save_correct_answers'] = $this->getSaveCorrectAnswers();
2525
                $defaults['review_answers'] = $this->review_answers;
2526
                $defaults['randomByCat'] = $this->getRandomByCategory();
2527
                $defaults['text_when_finished'] = $this->getTextWhenFinished();
2528
                $defaults['display_category_name'] = $this->selectDisplayCategoryName();
2529
                $defaults['pass_percentage'] = $this->selectPassPercentage();
2530
                $defaults['question_selection_type'] = $this->getQuestionSelectionType();
2531
                $defaults['hide_question_title'] = $this->getHideQuestionTitle();
2532
                $defaults['show_previous_button'] = $this->showPreviousButton();
2533
                $defaults['exercise_category_id'] = $this->getExerciseCategoryId();
2534
                $defaults['prevent_backwards'] = $this->getPreventBackwards();
2535
2536
                if (!empty($this->start_time)) {
2537
                    $defaults['activate_start_date_check'] = 1;
2538
                }
2539
                if (!empty($this->end_time)) {
2540
                    $defaults['activate_end_date_check'] = 1;
2541
                }
2542
2543
                $defaults['start_time'] = !empty($this->start_time) ? api_get_local_time($this->start_time) : date('Y-m-d 12:00:00');
2544
                $defaults['end_time'] = !empty($this->end_time) ? api_get_local_time($this->end_time) : date('Y-m-d 12:00:00', time() + 84600);
2545
2546
                // Get expired time
2547
                if ($this->expired_time != '0') {
2548
                    $defaults['enabletimercontrol'] = 1;
2549
                    $defaults['enabletimercontroltotalminutes'] = $this->expired_time;
2550
                } else {
2551
                    $defaults['enabletimercontroltotalminutes'] = 0;
2552
                }
2553
                $defaults['skills'] = array_keys($skillList);
2554
                $defaults['notifications'] = $this->getNotifications();
2555
            } else {
2556
                $defaults['exerciseType'] = 2;
2557
                $defaults['exerciseAttempts'] = 0;
2558
                $defaults['randomQuestions'] = 0;
2559
                $defaults['randomAnswers'] = 0;
2560
                $defaults['exerciseDescription'] = '';
2561
                $defaults['exerciseFeedbackType'] = 0;
2562
                $defaults['results_disabled'] = 0;
2563
                $defaults['randomByCat'] = 0;
2564
                $defaults['text_when_finished'] = '';
2565
                $defaults['start_time'] = date('Y-m-d 12:00:00');
2566
                $defaults['display_category_name'] = 1;
2567
                $defaults['end_time'] = date('Y-m-d 12:00:00', time() + 84600);
2568
                $defaults['pass_percentage'] = '';
2569
                $defaults['end_button'] = $this->selectEndButton();
2570
                $defaults['question_selection_type'] = 1;
2571
                $defaults['hide_question_title'] = 0;
2572
                $defaults['show_previous_button'] = 1;
2573
                $defaults['on_success_message'] = null;
2574
                $defaults['on_failed_message'] = null;
2575
            }
2576
        } else {
2577
            $defaults['exerciseTitle'] = $this->selectTitle();
2578
            $defaults['exerciseDescription'] = $this->selectDescription();
2579
        }
2580
2581
        if (api_get_setting('search_enabled') === 'true') {
2582
            $defaults['index_document'] = 'checked="checked"';
2583
        }
2584
2585
        $this->setPageResultConfigurationDefaults($defaults);
2586
        $form->setDefaults($defaults);
2587
2588
        // Freeze some elements.
2589
        if ($this->id != 0 && $this->edit_exercise_in_lp == false) {
2590
            $elementsToFreeze = [
2591
                'randomQuestions',
2592
                //'randomByCat',
2593
                'exerciseAttempts',
2594
                'propagate_neg',
2595
                'enabletimercontrol',
2596
                'review_answers',
2597
            ];
2598
2599
            foreach ($elementsToFreeze as $elementName) {
2600
                /** @var HTML_QuickForm_element $element */
2601
                $element = $form->getElement($elementName);
2602
                $element->freeze();
2603
            }
2604
        }
2605
    }
2606
2607
    public function setResultFeedbackGroup(FormValidator $form, $checkFreeze = true)
2608
    {
2609
        // Feedback type.
2610
        $feedback = [];
2611
        $endTest = $form->createElement(
2612
            'radio',
2613
            'exerciseFeedbackType',
2614
            null,
2615
            get_lang('ExerciseAtTheEndOfTheTest'),
2616
            EXERCISE_FEEDBACK_TYPE_END,
2617
            [
2618
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_END,
2619
                'onclick' => 'check_feedback()',
2620
            ]
2621
        );
2622
2623
        $noFeedBack = $form->createElement(
2624
            'radio',
2625
            'exerciseFeedbackType',
2626
            null,
2627
            get_lang('NoFeedback'),
2628
            EXERCISE_FEEDBACK_TYPE_EXAM,
2629
            [
2630
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_EXAM,
2631
            ]
2632
        );
2633
2634
        $feedback[] = $endTest;
2635
        $feedback[] = $noFeedBack;
2636
2637
        $scenarioEnabled = 'true' === api_get_setting('enable_quiz_scenario');
2638
        $freeze = true;
2639
        if ($scenarioEnabled) {
2640
            if ($this->getQuestionCount() > 0) {
2641
                $hasDifferentQuestion = $this->hasQuestionWithTypeNotInList([UNIQUE_ANSWER, HOT_SPOT_DELINEATION]);
2642
2643
                if (false === $hasDifferentQuestion) {
2644
                    $freeze = false;
2645
                }
2646
            } else {
2647
                $freeze = false;
2648
            }
2649
2650
            $direct = $form->createElement(
2651
                'radio',
2652
                'exerciseFeedbackType',
2653
                null,
2654
                get_lang('DirectFeedback'),
2655
                EXERCISE_FEEDBACK_TYPE_DIRECT,
2656
                [
2657
                    'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_DIRECT,
2658
                    'onclick' => 'check_direct_feedback()',
2659
                ]
2660
            );
2661
2662
            $directPopUp = $form->createElement(
2663
                'radio',
2664
                'exerciseFeedbackType',
2665
                null,
2666
                get_lang('ExerciseDirectPopUp'),
2667
                EXERCISE_FEEDBACK_TYPE_POPUP,
2668
                ['id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_POPUP, 'onclick' => 'check_direct_feedback()']
2669
            );
2670
2671
            if ($freeze) {
2672
                $direct->freeze();
2673
                $directPopUp->freeze();
2674
            }
2675
2676
            // If has delineation freeze all.
2677
            $hasDelineation = $this->hasQuestionWithType(HOT_SPOT_DELINEATION);
2678
            if ($hasDelineation) {
2679
                $endTest->freeze();
2680
                $noFeedBack->freeze();
2681
                $direct->freeze();
2682
                $directPopUp->freeze();
2683
            }
2684
2685
            $feedback[] = $direct;
2686
            $feedback[] = $directPopUp;
2687
        }
2688
2689
        $form->addGroup(
2690
            $feedback,
2691
            null,
2692
            [
2693
                get_lang('FeedbackType'),
2694
                get_lang('FeedbackDisplayOptions'),
2695
            ]
2696
        );
2697
    }
2698
2699
    /**
2700
     * function which process the creation of exercises.
2701
     *
2702
     * @param FormValidator $form
2703
     * @param string
2704
     *
2705
     * @return int c_quiz.iid
2706
     */
2707
    public function processCreation($form, $type = '')
2708
    {
2709
        $this->updateTitle(self::format_title_variable($form->getSubmitValue('exerciseTitle')));
2710
        $this->updateDescription($form->getSubmitValue('exerciseDescription'));
2711
        $this->updateAttempts($form->getSubmitValue('exerciseAttempts'));
2712
        $this->updateFeedbackType($form->getSubmitValue('exerciseFeedbackType'));
2713
        $this->updateType($form->getSubmitValue('exerciseType'));
2714
2715
        // If direct feedback then force to One per page
2716
        if (EXERCISE_FEEDBACK_TYPE_DIRECT == $form->getSubmitValue('exerciseFeedbackType')) {
2717
            $this->updateType(ONE_PER_PAGE);
2718
        }
2719
2720
        $this->setRandom($form->getSubmitValue('randomQuestions'));
2721
        $this->updateRandomAnswers($form->getSubmitValue('randomAnswers'));
2722
        $this->updateResultsDisabled($form->getSubmitValue('results_disabled'));
2723
        $this->updateExpiredTime($form->getSubmitValue('enabletimercontroltotalminutes'));
2724
        $this->updatePropagateNegative($form->getSubmitValue('propagate_neg'));
2725
        $this->updateSaveCorrectAnswers($form->getSubmitValue('save_correct_answers'));
2726
        $this->updateRandomByCat($form->getSubmitValue('randomByCat'));
2727
        $this->updateTextWhenFinished($form->getSubmitValue('text_when_finished'));
2728
        $this->updateDisplayCategoryName($form->getSubmitValue('display_category_name'));
2729
        $this->updateReviewAnswers($form->getSubmitValue('review_answers'));
2730
        $this->updatePassPercentage($form->getSubmitValue('pass_percentage'));
2731
        $this->updateCategories($form->getSubmitValue('category'));
2732
        $this->updateEndButton($form->getSubmitValue('end_button'));
2733
        $this->setOnSuccessMessage($form->getSubmitValue('on_success_message'));
2734
        $this->setOnFailedMessage($form->getSubmitValue('on_failed_message'));
2735
        $this->updateEmailNotificationTemplate($form->getSubmitValue('email_notification_template'));
2736
        $this->setEmailNotificationTemplateToUser($form->getSubmitValue('email_notification_template_to_user'));
2737
        $this->setNotifyUserByEmail($form->getSubmitValue('notify_user_by_email'));
2738
        $this->setModelType($form->getSubmitValue('model_type'));
2739
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2740
        $this->setHideQuestionTitle($form->getSubmitValue('hide_question_title'));
2741
        $this->sessionId = api_get_session_id();
2742
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2743
        $this->setScoreTypeModel($form->getSubmitValue('score_type_model'));
2744
        $this->setGlobalCategoryId($form->getSubmitValue('global_category_id'));
2745
        $this->setShowPreviousButton($form->getSubmitValue('show_previous_button'));
2746
        $this->setNotifications($form->getSubmitValue('notifications'));
2747
        $this->setExerciseCategoryId($form->getSubmitValue('exercise_category_id'));
2748
        $this->setPageResultConfiguration($form->getSubmitValues());
2749
        $this->preventBackwards = (int) $form->getSubmitValue('prevent_backwards');
2750
2751
        $this->start_time = null;
2752
        if ($form->getSubmitValue('activate_start_date_check') == 1) {
2753
            $start_time = $form->getSubmitValue('start_time');
2754
            $this->start_time = api_get_utc_datetime($start_time);
2755
        }
2756
2757
        $this->end_time = null;
2758
        if ($form->getSubmitValue('activate_end_date_check') == 1) {
2759
            $end_time = $form->getSubmitValue('end_time');
2760
            $this->end_time = api_get_utc_datetime($end_time);
2761
        }
2762
2763
        $this->expired_time = 0;
2764
        if ($form->getSubmitValue('enabletimercontrol') == 1) {
2765
            $expired_total_time = $form->getSubmitValue('enabletimercontroltotalminutes');
2766
            if ($this->expired_time == 0) {
2767
                $this->expired_time = $expired_total_time;
2768
            }
2769
        }
2770
2771
        $this->random_answers = 0;
2772
        if ($form->getSubmitValue('randomAnswers') == 1) {
2773
            $this->random_answers = 1;
2774
        }
2775
2776
        // Update title in all LPs that have this quiz added
2777
        if ($form->getSubmitValue('update_title_in_lps') == 1) {
2778
            $courseId = api_get_course_int_id();
2779
            $table = Database::get_course_table(TABLE_LP_ITEM);
2780
            $sql = "SELECT * FROM $table
2781
                    WHERE
2782
                        c_id = $courseId AND
2783
                        item_type = 'quiz' AND
2784
                        path = '".$this->id."'
2785
                    ";
2786
            $result = Database::query($sql);
2787
            $items = Database::store_result($result);
2788
            if (!empty($items)) {
2789
                foreach ($items as $item) {
2790
                    $itemId = $item['iid'];
2791
                    $sql = "UPDATE $table SET title = '".$this->title."'
2792
                            WHERE iid = $itemId AND c_id = $courseId ";
2793
                    Database::query($sql);
2794
                }
2795
            }
2796
        }
2797
2798
        $iId = $this->save($type);
2799
        if (!empty($iId)) {
2800
            $values = $form->getSubmitValues();
2801
            $values['item_id'] = $iId;
2802
            $extraFieldValue = new ExtraFieldValue('exercise');
2803
            $extraFieldValue->saveFieldValues($values);
2804
2805
            Skill::saveSkills($form, ITEM_TYPE_EXERCISE, $iId);
2806
        }
2807
    }
2808
2809
    public function search_engine_save()
2810
    {
2811
        if ($_POST['index_document'] != 1) {
2812
            return;
2813
        }
2814
        $course_id = api_get_course_id();
2815
2816
        require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
2817
2818
        $specific_fields = get_specific_field_list();
2819
        $ic_slide = new IndexableChunk();
2820
2821
        $all_specific_terms = '';
2822
        foreach ($specific_fields as $specific_field) {
2823
            if (isset($_REQUEST[$specific_field['code']])) {
2824
                $sterms = trim($_REQUEST[$specific_field['code']]);
2825
                if (!empty($sterms)) {
2826
                    $all_specific_terms .= ' '.$sterms;
2827
                    $sterms = explode(',', $sterms);
2828
                    foreach ($sterms as $sterm) {
2829
                        $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2830
                        add_specific_field_value($specific_field['id'], $course_id, TOOL_QUIZ, $this->id, $sterm);
2831
                    }
2832
                }
2833
            }
2834
        }
2835
2836
        // build the chunk to index
2837
        $ic_slide->addValue("title", $this->exercise);
2838
        $ic_slide->addCourseId($course_id);
2839
        $ic_slide->addToolId(TOOL_QUIZ);
2840
        $xapian_data = [
2841
            SE_COURSE_ID => $course_id,
2842
            SE_TOOL_ID => TOOL_QUIZ,
2843
            SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->id],
2844
            SE_USER => (int) api_get_user_id(),
2845
        ];
2846
        $ic_slide->xapian_data = serialize($xapian_data);
2847
        $exercise_description = $all_specific_terms.' '.$this->description;
2848
        $ic_slide->addValue("content", $exercise_description);
2849
2850
        $di = new ChamiloIndexer();
2851
        isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
2852
        $di->connectDb(null, null, $lang);
2853
        $di->addChunk($ic_slide);
2854
2855
        //index and return search engine document id
2856
        $did = $di->index();
2857
        if ($did) {
2858
            // save it to db
2859
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2860
            $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2861
			    VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2862
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id, $did);
2863
            Database::query($sql);
2864
        }
2865
    }
2866
2867
    public function search_engine_edit()
2868
    {
2869
        // update search enchine and its values table if enabled
2870
        if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
2871
            $course_id = api_get_course_id();
2872
2873
            // actually, it consists on delete terms from db,
2874
            // insert new ones, create a new search engine document, and remove the old one
2875
            // get search_did
2876
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2877
            $sql = 'SELECT * FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s LIMIT 1';
2878
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
2879
            $res = Database::query($sql);
2880
2881
            if (Database::num_rows($res) > 0) {
2882
                require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
2883
2884
                $se_ref = Database::fetch_array($res);
2885
                $specific_fields = get_specific_field_list();
2886
                $ic_slide = new IndexableChunk();
2887
2888
                $all_specific_terms = '';
2889
                foreach ($specific_fields as $specific_field) {
2890
                    delete_all_specific_field_value($course_id, $specific_field['id'], TOOL_QUIZ, $this->id);
2891
                    if (isset($_REQUEST[$specific_field['code']])) {
2892
                        $sterms = trim($_REQUEST[$specific_field['code']]);
2893
                        $all_specific_terms .= ' '.$sterms;
2894
                        $sterms = explode(',', $sterms);
2895
                        foreach ($sterms as $sterm) {
2896
                            $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2897
                            add_specific_field_value($specific_field['id'], $course_id, TOOL_QUIZ, $this->id, $sterm);
2898
                        }
2899
                    }
2900
                }
2901
2902
                // build the chunk to index
2903
                $ic_slide->addValue('title', $this->exercise);
2904
                $ic_slide->addCourseId($course_id);
2905
                $ic_slide->addToolId(TOOL_QUIZ);
2906
                $xapian_data = [
2907
                    SE_COURSE_ID => $course_id,
2908
                    SE_TOOL_ID => TOOL_QUIZ,
2909
                    SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->id],
2910
                    SE_USER => (int) api_get_user_id(),
2911
                ];
2912
                $ic_slide->xapian_data = serialize($xapian_data);
2913
                $exercise_description = $all_specific_terms.' '.$this->description;
2914
                $ic_slide->addValue('content', $exercise_description);
2915
2916
                $di = new ChamiloIndexer();
2917
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
2918
                $di->connectDb(null, null, $lang);
2919
                $di->remove_document($se_ref['search_did']);
2920
                $di->addChunk($ic_slide);
2921
2922
                //index and return search engine document id
2923
                $did = $di->index();
2924
                if ($did) {
2925
                    // save it to db
2926
                    $sql = 'DELETE FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=\'%s\'';
2927
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
2928
                    Database::query($sql);
2929
                    $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2930
                        VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2931
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id, $did);
2932
                    Database::query($sql);
2933
                }
2934
            } else {
2935
                $this->search_engine_save();
2936
            }
2937
        }
2938
    }
2939
2940
    public function search_engine_delete()
2941
    {
2942
        // remove from search engine if enabled
2943
        if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
2944
            $course_id = api_get_course_id();
2945
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2946
            $sql = 'SELECT * FROM %s
2947
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
2948
                    LIMIT 1';
2949
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
2950
            $res = Database::query($sql);
2951
            if (Database::num_rows($res) > 0) {
2952
                $row = Database::fetch_array($res);
2953
                $di = new ChamiloIndexer();
2954
                $di->remove_document($row['search_did']);
2955
                unset($di);
2956
                $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2957
                foreach ($this->questionList as $question_i) {
2958
                    $sql = 'SELECT type FROM %s WHERE id=%s';
2959
                    $sql = sprintf($sql, $tbl_quiz_question, $question_i);
2960
                    $qres = Database::query($sql);
2961
                    if (Database::num_rows($qres) > 0) {
2962
                        $qrow = Database::fetch_array($qres);
2963
                        $objQuestion = Question::getInstance($qrow['type']);
2964
                        $objQuestion = Question::read((int) $question_i);
2965
                        $objQuestion->search_engine_edit($this->id, false, true);
2966
                        unset($objQuestion);
2967
                    }
2968
                }
2969
            }
2970
            $sql = 'DELETE FROM %s
2971
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
2972
                    LIMIT 1';
2973
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
2974
            Database::query($sql);
2975
2976
            // remove terms from db
2977
            require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
2978
            delete_all_values_for_item($course_id, TOOL_QUIZ, $this->id);
2979
        }
2980
    }
2981
2982
    public function selectExpiredTime()
2983
    {
2984
        return $this->expired_time;
2985
    }
2986
2987
    /**
2988
     * Cleans the student's results only for the Exercise tool (Not from the LP)
2989
     * The LP results are NOT deleted by default, otherwise put $cleanLpTests = true
2990
     * Works with exercises in sessions.
2991
     *
2992
     * @param bool   $cleanLpTests
2993
     * @param string $cleanResultBeforeDate
2994
     *
2995
     * @return int quantity of user's exercises deleted
2996
     */
2997
    public function cleanResults($cleanLpTests = false, $cleanResultBeforeDate = null)
2998
    {
2999
        $sessionId = api_get_session_id();
3000
        $table_track_e_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3001
        $table_track_e_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3002
3003
        $sql_where = '  AND
3004
                        orig_lp_id = 0 AND
3005
                        orig_lp_item_id = 0';
3006
3007
        // if we want to delete results from LP too
3008
        if ($cleanLpTests) {
3009
            $sql_where = '';
3010
        }
3011
3012
        // if we want to delete attempts before date $cleanResultBeforeDate
3013
        // $cleanResultBeforeDate must be a valid UTC-0 date yyyy-mm-dd
3014
3015
        if (!empty($cleanResultBeforeDate)) {
3016
            $cleanResultBeforeDate = Database::escape_string($cleanResultBeforeDate);
3017
            if (api_is_valid_date($cleanResultBeforeDate)) {
3018
                $sql_where .= "  AND exe_date <= '$cleanResultBeforeDate' ";
3019
            } else {
3020
                return 0;
3021
            }
3022
        }
3023
3024
        $sql = "SELECT exe_id
3025
            FROM $table_track_e_exercises
3026
            WHERE
3027
                c_id = ".api_get_course_int_id()." AND
3028
                exe_exo_id = ".$this->id." AND
3029
                session_id = ".$sessionId." ".
3030
                $sql_where;
3031
3032
        $result = Database::query($sql);
3033
        $exe_list = Database::store_result($result);
3034
3035
        // deleting TRACK_E_ATTEMPT table
3036
        // check if exe in learning path or not
3037
        $i = 0;
3038
        if (is_array($exe_list) && count($exe_list) > 0) {
3039
            foreach ($exe_list as $item) {
3040
                $sql = "DELETE FROM $table_track_e_attempt
3041
                        WHERE exe_id = '".$item['exe_id']."'";
3042
                Database::query($sql);
3043
                $i++;
3044
            }
3045
        }
3046
3047
        // delete TRACK_E_EXERCISES table
3048
        $sql = "DELETE FROM $table_track_e_exercises
3049
                WHERE
3050
                  c_id = ".api_get_course_int_id()." AND
3051
                  exe_exo_id = ".$this->id." $sql_where AND
3052
                  session_id = ".$sessionId;
3053
        Database::query($sql);
3054
3055
        $this->generateStats($this->id, api_get_course_info(), $sessionId);
3056
3057
        Event::addEvent(
3058
            LOG_EXERCISE_RESULT_DELETE,
3059
            LOG_EXERCISE_ID,
3060
            $this->id,
3061
            null,
3062
            null,
3063
            api_get_course_int_id(),
3064
            $sessionId
3065
        );
3066
3067
        return $i;
3068
    }
3069
3070
    /**
3071
     * Copies an exercise (duplicate all questions and answers).
3072
     */
3073
    public function copyExercise()
3074
    {
3075
        $exerciseObject = $this;
3076
        $categories = $exerciseObject->getCategoriesInExercise(true);
3077
        // Get all questions no matter the order/category settings
3078
        $questionList = $exerciseObject->getQuestionOrderedList();
3079
        // Force the creation of a new exercise
3080
        $exerciseObject->updateTitle($exerciseObject->selectTitle().' - '.get_lang('Copy'));
3081
        // Hides the new exercise
3082
        $exerciseObject->updateStatus(false);
3083
        $exerciseObject->updateId(0);
3084
        $exerciseObject->sessionId = api_get_session_id();
3085
        $courseId = api_get_course_int_id();
3086
        $exerciseObject->save();
3087
        $newId = $exerciseObject->selectId();
3088
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
3089
3090
        $count = 1;
3091
        $batchSize = 20;
3092
        $em = Database::getManager();
3093
3094
        if ($newId && !empty($questionList)) {
3095
            // Question creation
3096
            foreach ($questionList as $oldQuestionId) {
3097
                $oldQuestionObj = Question::read($oldQuestionId, null, false);
3098
                $newQuestionId = $oldQuestionObj->duplicate();
3099
                if ($newQuestionId) {
3100
                    $newQuestionObj = Question::read($newQuestionId, null, false);
3101
                    if (isset($newQuestionObj) && $newQuestionObj) {
3102
                        //$newQuestionObj->addToList($newId);
3103
                        $sql = "INSERT INTO $exerciseRelQuestionTable (c_id, question_id, exercice_id, question_order)
3104
                                VALUES ($courseId, ".$newQuestionId.", ".$newId.", '$count')";
3105
                        Database::query($sql);
3106
                        $count++;
3107
3108
                        if (!empty($oldQuestionObj->category)) {
3109
                            $newQuestionObj->saveCategory($oldQuestionObj->category);
3110
                        }
3111
3112
                        // This should be moved to the duplicate function
3113
                        $newAnswerObj = new Answer($oldQuestionId, $courseId, $exerciseObject);
3114
                        $newAnswerObj->read();
3115
                        $newAnswerObj->duplicate($newQuestionObj);
3116
3117
                        if (($count % $batchSize) === 0) {
3118
                            $em->clear(); // Detaches all objects from Doctrine!
3119
                        }
3120
                    }
3121
                }
3122
            }
3123
            if (!empty($categories)) {
3124
                $newCategoryList = [];
3125
                foreach ($categories as $category) {
3126
                    $newCategoryList[$category['category_id']] = $category['count_questions'];
3127
                }
3128
                $exerciseObject->save_categories_in_exercise($newCategoryList);
3129
            }
3130
        }
3131
    }
3132
3133
    /**
3134
     * Changes the exercise status.
3135
     *
3136
     * @param string $status - exercise status
3137
     */
3138
    public function updateStatus($status)
3139
    {
3140
        $this->active = $status;
3141
    }
3142
3143
    /**
3144
     * @param int    $lp_id
3145
     * @param int    $lp_item_id
3146
     * @param int    $lp_item_view_id
3147
     * @param string $status
3148
     *
3149
     * @return array
3150
     */
3151
    public function get_stat_track_exercise_info(
3152
        $lp_id = 0,
3153
        $lp_item_id = 0,
3154
        $lp_item_view_id = 0,
3155
        $status = 'incomplete'
3156
    ) {
3157
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3158
        if (empty($lp_id)) {
3159
            $lp_id = 0;
3160
        }
3161
        if (empty($lp_item_id)) {
3162
            $lp_item_id = 0;
3163
        }
3164
        if (empty($lp_item_view_id)) {
3165
            $lp_item_view_id = 0;
3166
        }
3167
        $condition = ' WHERE exe_exo_id 	= '."'".$this->id."'".' AND
3168
					   exe_user_id 			= '."'".api_get_user_id()."'".' AND
3169
					   c_id                 = '.api_get_course_int_id().' AND
3170
					   status 				= '."'".Database::escape_string($status)."'".' AND
3171
					   orig_lp_id 			= '."'".$lp_id."'".' AND
3172
					   orig_lp_item_id 		= '."'".$lp_item_id."'".' AND
3173
                       orig_lp_item_view_id = '."'".$lp_item_view_id."'".' AND
3174
					   session_id 			= '."'".api_get_session_id()."' LIMIT 1"; //Adding limit 1 just in case
3175
3176
        $sql_track = 'SELECT * FROM '.$track_exercises.$condition;
3177
3178
        $result = Database::query($sql_track);
3179
        $new_array = [];
3180
        if (Database::num_rows($result) > 0) {
3181
            $new_array = Database::fetch_array($result, 'ASSOC');
3182
            $new_array['num_exe'] = Database::num_rows($result);
3183
        }
3184
3185
        return $new_array;
3186
    }
3187
3188
    /**
3189
     * Saves a test attempt.
3190
     *
3191
     * @param int $clock_expired_time clock_expired_time
3192
     * @param int  int lp id
3193
     * @param int  int lp item id
3194
     * @param int  int lp item_view id
3195
     * @param array $questionList
3196
     * @param float $weight
3197
     *
3198
     * @return int
3199
     */
3200
    public function save_stat_track_exercise_info(
3201
        $clock_expired_time = 0,
3202
        $safe_lp_id = 0,
3203
        $safe_lp_item_id = 0,
3204
        $safe_lp_item_view_id = 0,
3205
        $questionList = [],
3206
        $weight = 0
3207
    ) {
3208
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3209
        $safe_lp_id = (int) $safe_lp_id;
3210
        $safe_lp_item_id = (int) $safe_lp_item_id;
3211
        $safe_lp_item_view_id = (int) $safe_lp_item_view_id;
3212
3213
        if (empty($clock_expired_time)) {
3214
            $clock_expired_time = null;
3215
        }
3216
3217
        $questionList = array_map('intval', $questionList);
3218
3219
        $params = [
3220
            'exe_exo_id' => $this->id,
3221
            'exe_user_id' => api_get_user_id(),
3222
            'c_id' => api_get_course_int_id(),
3223
            'status' => 'incomplete',
3224
            'session_id' => api_get_session_id(),
3225
            'data_tracking' => implode(',', $questionList),
3226
            'start_date' => api_get_utc_datetime(),
3227
            'orig_lp_id' => $safe_lp_id,
3228
            'orig_lp_item_id' => $safe_lp_item_id,
3229
            'orig_lp_item_view_id' => $safe_lp_item_view_id,
3230
            'exe_weighting' => $weight,
3231
            'user_ip' => Database::escape_string(api_get_real_ip()),
3232
            'exe_date' => api_get_utc_datetime(),
3233
            'exe_result' => 0,
3234
            'steps_counter' => 0,
3235
            'exe_duration' => 0,
3236
            'expired_time_control' => $clock_expired_time,
3237
            'questions_to_check' => '',
3238
        ];
3239
3240
        return Database::insert($track_exercises, $params);
3241
    }
3242
3243
    /**
3244
     * @param int    $question_id
3245
     * @param int    $questionNum
3246
     * @param array  $questions_in_media
3247
     * @param string $currentAnswer
3248
     * @param array  $myRemindList
3249
     * @param bool   $showPreviousButton
3250
     *
3251
     * @return string
3252
     */
3253
    public function show_button(
3254
        $question_id,
3255
        $questionNum,
3256
        $questions_in_media = [],
3257
        $currentAnswer = '',
3258
        $myRemindList = [],
3259
        $showPreviousButton = true
3260
    ) {
3261
        global $safe_lp_id, $safe_lp_item_id, $safe_lp_item_view_id;
3262
        $nbrQuestions = $this->countQuestionsInExercise();
3263
        $buttonList = [];
3264
        $html = $label = '';
3265
        $hotspotGet = isset($_POST['hotspot']) ? Security::remove_XSS($_POST['hotspot']) : null;
3266
3267
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]) &&
3268
            $this->type == ONE_PER_PAGE
3269
        ) {
3270
            $urlTitle = get_lang('ContinueTest');
3271
            if ($questionNum == count($this->questionList)) {
3272
                $urlTitle = get_lang('EndTest');
3273
            }
3274
3275
            $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit_modal.php?'.api_get_cidreq();
3276
            $url .= '&'.http_build_query([
3277
                'learnpath_id' => $safe_lp_id,
3278
                'learnpath_item_id' => $safe_lp_item_id,
3279
                'learnpath_item_view_id' => $safe_lp_item_view_id,
3280
                'hotspot' => $hotspotGet,
3281
                'nbrQuestions' => $nbrQuestions,
3282
                'num' => $questionNum,
3283
                'exerciseType' => $this->type,
3284
                'exerciseId' => $this->id,
3285
                'reminder' => empty($myRemindList) ? null : 2,
3286
                'tryagain' => isset($_REQUEST['tryagain']) && 1 === (int) $_REQUEST['tryagain'] ? 1 : 0,
3287
            ]);
3288
3289
            $params = [
3290
                'class' => 'ajax btn btn-default no-close-button',
3291
                'data-title' => Security::remove_XSS(get_lang('Comment')),
3292
                'data-size' => 'md',
3293
                'id' => "button_$question_id",
3294
            ];
3295
3296
            if ($this->getFeedbackType() === EXERCISE_FEEDBACK_TYPE_POPUP) {
3297
                $params['data-block-closing'] = 'true';
3298
                $params['class'] .= ' no-header ';
3299
            }
3300
3301
            $html .= Display::url($urlTitle, $url, $params);
3302
            $html .= '<br />';
3303
3304
            return $html;
3305
        }
3306
3307
        if (!api_is_allowed_to_session_edit()) {
3308
            return '';
3309
        }
3310
3311
        $isReviewingAnswers = isset($_REQUEST['reminder']) && 2 === (int) $_REQUEST['reminder'];
3312
3313
        // User
3314
        $endReminderValue = false;
3315
        if (!empty($myRemindList) && $isReviewingAnswers) {
3316
            $endValue = end($myRemindList);
3317
            if ($endValue == $question_id) {
3318
                $endReminderValue = true;
3319
            }
3320
        }
3321
        if ($this->type == ALL_ON_ONE_PAGE || $nbrQuestions == $questionNum || $endReminderValue) {
3322
            if ($this->review_answers) {
3323
                $label = get_lang('ReviewQuestions');
3324
                $class = 'btn btn-success';
3325
            } else {
3326
                $label = get_lang('EndTest');
3327
                $class = 'btn btn-warning';
3328
            }
3329
        } else {
3330
            $label = get_lang('NextQuestion');
3331
            $class = 'btn btn-primary';
3332
        }
3333
        // used to select it with jquery
3334
        $class .= ' question-validate-btn';
3335
        if ($this->type == ONE_PER_PAGE) {
3336
            if ($questionNum != 1 && $this->showPreviousButton()) {
3337
                $prev_question = $questionNum - 2;
3338
                $showPreview = true;
3339
                if (!empty($myRemindList) && $isReviewingAnswers) {
3340
                    $beforeId = null;
3341
                    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...
3342
                        if (isset($myRemindList[$i]) && $myRemindList[$i] == $question_id) {
3343
                            $beforeId = isset($myRemindList[$i - 1]) ? $myRemindList[$i - 1] : null;
3344
                            break;
3345
                        }
3346
                    }
3347
3348
                    if (empty($beforeId)) {
3349
                        $showPreview = false;
3350
                    } else {
3351
                        $num = 0;
3352
                        foreach ($this->questionList as $originalQuestionId) {
3353
                            if ($originalQuestionId == $beforeId) {
3354
                                break;
3355
                            }
3356
                            $num++;
3357
                        }
3358
                        $prev_question = $num;
3359
                    }
3360
                }
3361
3362
                if ($showPreviousButton && $showPreview && 0 === $this->getPreventBackwards()) {
3363
                    $buttonList[] = Display::button(
3364
                        'previous_question_and_save',
3365
                        get_lang('PreviousQuestion'),
3366
                        [
3367
                            'type' => 'button',
3368
                            'class' => 'btn btn-default',
3369
                            'data-prev' => $prev_question,
3370
                            'data-question' => $question_id,
3371
                        ]
3372
                    );
3373
                }
3374
            }
3375
3376
            // Next question
3377
            if (!empty($questions_in_media)) {
3378
                $buttonList[] = Display::button(
3379
                    'save_question_list',
3380
                    $label,
3381
                    [
3382
                        'type' => 'button',
3383
                        'class' => $class,
3384
                        'data-list' => implode(",", $questions_in_media),
3385
                    ]
3386
                );
3387
            } else {
3388
                $buttonList[] = Display::button(
3389
                    'save_now',
3390
                    $label,
3391
                    ['type' => 'button', 'class' => $class, 'data-question' => $question_id]
3392
                );
3393
            }
3394
            $buttonList[] = '<span id="save_for_now_'.$question_id.'" class="exercise_save_mini_message"></span>';
3395
3396
            $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3397
3398
            return $html;
3399
        }
3400
3401
        if ($this->review_answers) {
3402
            $all_label = get_lang('ReviewQuestions');
3403
            $class = 'btn btn-success';
3404
        } else {
3405
            $all_label = get_lang('EndTest');
3406
            $class = 'btn btn-warning';
3407
        }
3408
        // used to select it with jquery
3409
        $class .= ' question-validate-btn';
3410
        $buttonList[] = Display::button(
3411
            'validate_all',
3412
            $all_label,
3413
            ['type' => 'button', 'class' => $class]
3414
        );
3415
        $buttonList[] = Display::span(null, ['id' => 'save_all_response']);
3416
        $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3417
3418
        return $html;
3419
    }
3420
3421
    /**
3422
     * @param int    $timeLeft in seconds
3423
     * @param string $url
3424
     *
3425
     * @return string
3426
     */
3427
    public function showSimpleTimeControl($timeLeft, $url = '')
3428
    {
3429
        $timeLeft = (int) $timeLeft;
3430
3431
        return "<script>
3432
            function openClockWarning() {
3433
                $('#clock_warning').dialog({
3434
                    modal:true,
3435
                    height:320,
3436
                    width:550,
3437
                    closeOnEscape: false,
3438
                    resizable: false,
3439
                    buttons: {
3440
                        '".addslashes(get_lang('Close'))."': function() {
3441
                            $('#clock_warning').dialog('close');
3442
                        }
3443
                    },
3444
                    close: function() {
3445
                        window.location.href = '$url';
3446
                    }
3447
                });
3448
                $('#clock_warning').dialog('open');
3449
                $('#counter_to_redirect').epiclock({
3450
                    mode: $.epiclock.modes.countdown,
3451
                    offset: {seconds: 5},
3452
                    format: 's'
3453
                }).bind('timer', function () {
3454
                    window.location.href = '$url';
3455
                });
3456
            }
3457
3458
            function onExpiredTimeExercise() {
3459
                $('#wrapper-clock').hide();
3460
                $('#expired-message-id').show();
3461
                // Fixes bug #5263
3462
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3463
                openClockWarning();
3464
            }
3465
3466
			$(function() {
3467
				// time in seconds when using minutes there are some seconds lost
3468
                var time_left = parseInt(".$timeLeft.");
3469
                $('#exercise_clock_warning').epiclock({
3470
                    mode: $.epiclock.modes.countdown,
3471
                    offset: {seconds: time_left},
3472
                    format: 'x:i:s',
3473
                    renderer: 'minute'
3474
                }).bind('timer', function () {
3475
                    onExpiredTimeExercise();
3476
                });
3477
	       		$('#submit_save').click(function () {});
3478
	        });
3479
	    </script>";
3480
    }
3481
3482
    /**
3483
     * So the time control will work.
3484
     *
3485
     * @param int $timeLeft
3486
     *
3487
     * @return string
3488
     */
3489
    public function showTimeControlJS($timeLeft)
3490
    {
3491
        $timeLeft = (int) $timeLeft;
3492
        $script = 'redirectExerciseToResult();';
3493
        if (ALL_ON_ONE_PAGE == $this->type) {
3494
            $script = "save_now_all('validate');";
3495
        } elseif (ONE_PER_PAGE == $this->type) {
3496
            $script = 'window.quizTimeEnding = true;
3497
                $(\'[name="save_now"]\').trigger(\'click\');';
3498
        }
3499
3500
        return "<script>
3501
            function openClockWarning() {
3502
                $('#clock_warning').dialog({
3503
                    modal:true,
3504
                    height:320,
3505
                    width:550,
3506
                    closeOnEscape: false,
3507
                    resizable: false,
3508
                    buttons: {
3509
                        '".addslashes(get_lang('EndTest'))."': function() {
3510
                            $('#clock_warning').dialog('close');
3511
                        }
3512
                    },
3513
                    close: function() {
3514
                        send_form();
3515
                    }
3516
                });
3517
3518
                $('#clock_warning').dialog('open');
3519
                $('#counter_to_redirect').epiclock({
3520
                    mode: $.epiclock.modes.countdown,
3521
                    offset: {seconds: 5},
3522
                    format: 's'
3523
                }).bind('timer', function () {
3524
                    send_form();
3525
                });
3526
            }
3527
3528
            function send_form() {
3529
                if ($('#exercise_form').length) {
3530
                    $script
3531
                } else {
3532
                    // In exercise_reminder.php
3533
                    final_submit();
3534
                }
3535
            }
3536
3537
            function onExpiredTimeExercise() {
3538
                $('#wrapper-clock').hide();
3539
                $('#expired-message-id').show();
3540
                // Fixes bug #5263
3541
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3542
                openClockWarning();
3543
            }
3544
3545
			$(function() {
3546
				// time in seconds when using minutes there are some seconds lost
3547
                var time_left = parseInt(".$timeLeft.");
3548
                $('#exercise_clock_warning').epiclock({
3549
                    mode: $.epiclock.modes.countdown,
3550
                    offset: {seconds: time_left},
3551
                    format: 'x:C:s',
3552
                    renderer: 'minute'
3553
                }).bind('timer', function () {
3554
                    onExpiredTimeExercise();
3555
                });
3556
	       		$('#submit_save').click(function () {});
3557
	        });
3558
	    </script>";
3559
    }
3560
3561
    /**
3562
     * This function was originally found in the exercise_show.php.
3563
     *
3564
     * @param int    $exeId
3565
     * @param int    $questionId
3566
     * @param mixed  $choice                                    the user-selected option
3567
     * @param string $from                                      function is called from 'exercise_show' or
3568
     *                                                          'exercise_result'
3569
     * @param array  $exerciseResultCoordinates                 the hotspot coordinates $hotspot[$question_id] =
3570
     *                                                          coordinates
3571
     * @param bool   $save_results                              save results in the DB or just show the response
3572
     * @param bool   $from_database                             gets information from DB or from the current selection
3573
     * @param bool   $show_result                               show results or not
3574
     * @param int    $propagate_neg
3575
     * @param array  $hotspot_delineation_result
3576
     * @param bool   $showTotalScoreAndUserChoicesInLastAttempt
3577
     * @param bool   $updateResults
3578
     * @param bool   $showHotSpotDelineationTable
3579
     * @param int    $questionDuration                          seconds
3580
     *
3581
     * @todo    reduce parameters of this function
3582
     *
3583
     * @return string html code
3584
     */
3585
    public function manage_answer(
3586
        $exeId,
3587
        $questionId,
3588
        $choice,
3589
        $from = 'exercise_show',
3590
        $exerciseResultCoordinates = [],
3591
        $save_results = true,
3592
        $from_database = false,
3593
        $show_result = true,
3594
        $propagate_neg = 0,
3595
        $hotspot_delineation_result = [],
3596
        $showTotalScoreAndUserChoicesInLastAttempt = true,
3597
        $updateResults = false,
3598
        $showHotSpotDelineationTable = false,
3599
        $questionDuration = 0
3600
    ) {
3601
        $debug = false;
3602
        //needed in order to use in the exercise_attempt() for the time
3603
        global $learnpath_id, $learnpath_item_id;
3604
        require_once api_get_path(LIBRARY_PATH).'geometry.lib.php';
3605
        $em = Database::getManager();
3606
        $feedback_type = $this->getFeedbackType();
3607
        $results_disabled = $this->selectResultsDisabled();
3608
        $questionDuration = (int) $questionDuration;
3609
3610
        if ($debug) {
3611
            error_log("<------ manage_answer ------> ");
3612
            error_log('exe_id: '.$exeId);
3613
            error_log('$from:  '.$from);
3614
            error_log('$save_results: '.intval($save_results));
3615
            error_log('$from_database: '.intval($from_database));
3616
            error_log('$show_result: '.intval($show_result));
3617
            error_log('$propagate_neg: '.$propagate_neg);
3618
            error_log('$exerciseResultCoordinates: '.print_r($exerciseResultCoordinates, 1));
3619
            error_log('$hotspot_delineation_result: '.print_r($hotspot_delineation_result, 1));
3620
            error_log('$learnpath_id: '.$learnpath_id);
3621
            error_log('$learnpath_item_id: '.$learnpath_item_id);
3622
            error_log('$choice: '.print_r($choice, 1));
3623
        }
3624
3625
        $final_overlap = 0;
3626
        $final_missing = 0;
3627
        $final_excess = 0;
3628
        $overlap_color = 0;
3629
        $missing_color = 0;
3630
        $excess_color = 0;
3631
        $threadhold1 = 0;
3632
        $threadhold2 = 0;
3633
        $threadhold3 = 0;
3634
        $arrques = null;
3635
        $arrans = null;
3636
        $studentChoice = null;
3637
        $expectedAnswer = '';
3638
        $calculatedChoice = '';
3639
        $calculatedStatus = '';
3640
        $questionId = (int) $questionId;
3641
        $exeId = (int) $exeId;
3642
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3643
        $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
3644
        $studentChoiceDegree = null;
3645
3646
        // Creates a temporary Question object
3647
        $course_id = $this->course_id;
3648
        $objQuestionTmp = Question::read($questionId, $this->course);
3649
3650
        if (false === $objQuestionTmp) {
3651
            return false;
3652
        }
3653
3654
        $questionName = $objQuestionTmp->selectTitle();
3655
        $questionWeighting = $objQuestionTmp->selectWeighting();
3656
        $answerType = $objQuestionTmp->selectType();
3657
        $quesId = $objQuestionTmp->selectId();
3658
        $extra = $objQuestionTmp->extra;
3659
        $next = 1; //not for now
3660
        $totalWeighting = 0;
3661
        $totalScore = 0;
3662
3663
        // Extra information of the question
3664
        if ((
3665
            $answerType == MULTIPLE_ANSWER_TRUE_FALSE ||
3666
            $answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY
3667
            )
3668
            && !empty($extra)
3669
        ) {
3670
            $extra = explode(':', $extra);
3671
            // Fixes problems with negatives values using intval
3672
            $true_score = (float) trim($extra[0]);
3673
            $false_score = (float) trim($extra[1]);
3674
            $doubt_score = (float) trim($extra[2]);
3675
        }
3676
3677
        // Construction of the Answer object
3678
        $objAnswerTmp = new Answer($questionId, $course_id);
3679
        $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
3680
3681
        if ($debug) {
3682
            error_log('Count of possible answers: '.$nbrAnswers);
3683
            error_log('$answerType: '.$answerType);
3684
        }
3685
3686
        if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
3687
            $choiceTmp = $choice;
3688
            $choice = isset($choiceTmp['choice']) ? $choiceTmp['choice'] : '';
3689
            $choiceDegreeCertainty = isset($choiceTmp['choiceDegreeCertainty']) ? $choiceTmp['choiceDegreeCertainty'] : '';
3690
        }
3691
3692
        if ($answerType == FREE_ANSWER ||
3693
            $answerType == ORAL_EXPRESSION ||
3694
            $answerType == CALCULATED_ANSWER ||
3695
            $answerType == ANNOTATION
3696
        ) {
3697
            $nbrAnswers = 1;
3698
        }
3699
3700
        $generatedFile = '';
3701
        if ($answerType == ORAL_EXPRESSION) {
3702
            $exe_info = Event::get_exercise_results_by_attempt($exeId);
3703
            $exe_info = isset($exe_info[$exeId]) ? $exe_info[$exeId] : null;
3704
3705
            $objQuestionTmp->initFile(
3706
                api_get_session_id(),
3707
                isset($exe_info['exe_user_id']) ? $exe_info['exe_user_id'] : api_get_user_id(),
3708
                isset($exe_info['exe_exo_id']) ? $exe_info['exe_exo_id'] : $this->id,
3709
                isset($exe_info['exe_id']) ? $exe_info['exe_id'] : $exeId
3710
            );
3711
3712
            // Probably this attempt came in an exercise all question by page
3713
            if ($feedback_type == 0) {
3714
                $objQuestionTmp->replaceWithRealExe($exeId);
3715
            }
3716
            $generatedFile = $objQuestionTmp->getFileUrl();
3717
        }
3718
3719
        $user_answer = '';
3720
        // Get answer list for matching
3721
        $sql = "SELECT id_auto, id, answer
3722
                FROM $table_ans
3723
                WHERE c_id = $course_id AND question_id = $questionId";
3724
        $res_answer = Database::query($sql);
3725
3726
        $answerMatching = [];
3727
        while ($real_answer = Database::fetch_array($res_answer)) {
3728
            $answerMatching[$real_answer['id_auto']] = $real_answer['answer'];
3729
        }
3730
3731
        $real_answers = [];
3732
        $quiz_question_options = Question::readQuestionOption(
3733
            $questionId,
3734
            $course_id
3735
        );
3736
3737
        $organs_at_risk_hit = 0;
3738
        $questionScore = 0;
3739
        $orderedHotSpots = [];
3740
        if ($answerType == HOT_SPOT || $answerType == ANNOTATION) {
3741
            $orderedHotSpots = $em->getRepository('ChamiloCoreBundle:TrackEHotspot')->findBy(
3742
                [
3743
                    'hotspotQuestionId' => $questionId,
3744
                    'cId' => $course_id,
3745
                    'hotspotExeId' => $exeId,
3746
                ],
3747
                ['hotspotAnswerId' => 'ASC']
3748
            );
3749
        }
3750
3751
        if ($debug) {
3752
            error_log('-- Start answer loop --');
3753
        }
3754
3755
        $answerDestination = null;
3756
        $userAnsweredQuestion = false;
3757
        $correctAnswerId = [];
3758
        for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
3759
            $answer = $objAnswerTmp->selectAnswer($answerId);
3760
            $answerComment = $objAnswerTmp->selectComment($answerId);
3761
            $answerCorrect = $objAnswerTmp->isCorrect($answerId);
3762
            $answerWeighting = (float) $objAnswerTmp->selectWeighting($answerId);
3763
            $answerAutoId = $objAnswerTmp->selectAutoId($answerId);
3764
            $answerIid = isset($objAnswerTmp->iid[$answerId]) ? (int) $objAnswerTmp->iid[$answerId] : 0;
3765
3766
            if ($debug) {
3767
                error_log("c_quiz_answer.id_auto: $answerAutoId ");
3768
                error_log("Answer marked as correct in db (0/1)?: $answerCorrect ");
3769
            }
3770
3771
            // Delineation
3772
            $delineation_cord = $objAnswerTmp->selectHotspotCoordinates(1);
3773
            $answer_delineation_destination = $objAnswerTmp->selectDestination(1);
3774
3775
            switch ($answerType) {
3776
                case UNIQUE_ANSWER:
3777
                case UNIQUE_ANSWER_IMAGE:
3778
                case UNIQUE_ANSWER_NO_OPTION:
3779
                case READING_COMPREHENSION:
3780
                    if ($from_database) {
3781
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3782
                                WHERE
3783
                                    exe_id = $exeId AND
3784
                                    question_id = $questionId";
3785
                        $result = Database::query($sql);
3786
                        $choice = Database::result($result, 0, 'answer');
3787
3788
                        if (false === $userAnsweredQuestion) {
3789
                            $userAnsweredQuestion = !empty($choice);
3790
                        }
3791
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3792
                        if ($studentChoice) {
3793
                            $questionScore += $answerWeighting;
3794
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3795
                            $correctAnswerId[] = $answerId;
3796
                        }
3797
                    } else {
3798
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3799
                        if ($studentChoice) {
3800
                            $questionScore += $answerWeighting;
3801
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3802
                            $correctAnswerId[] = $answerId;
3803
                        }
3804
                    }
3805
                    break;
3806
                case MULTIPLE_ANSWER_TRUE_FALSE:
3807
                    if ($from_database) {
3808
                        $choice = [];
3809
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3810
                                WHERE
3811
                                    exe_id = $exeId AND
3812
                                    question_id = ".$questionId;
3813
3814
                        $result = Database::query($sql);
3815
                        while ($row = Database::fetch_array($result)) {
3816
                            $values = explode(':', $row['answer']);
3817
                            $my_answer_id = isset($values[0]) ? $values[0] : '';
3818
                            $option = isset($values[1]) ? $values[1] : '';
3819
                            $choice[$my_answer_id] = $option;
3820
                        }
3821
3822
                        $userAnsweredQuestion = !empty($choice);
3823
                    }
3824
3825
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3826
                    if (!empty($studentChoice)) {
3827
                        $correctAnswerId[] = $answerAutoId;
3828
                        if ($studentChoice == $answerCorrect) {
3829
                            $questionScore += $true_score;
3830
                        } else {
3831
                            if ($quiz_question_options[$studentChoice]['name'] === "Don't know" ||
3832
                                $quiz_question_options[$studentChoice]['name'] === "DoubtScore"
3833
                            ) {
3834
                                $questionScore += $doubt_score;
3835
                            } else {
3836
                                $questionScore += $false_score;
3837
                            }
3838
                        }
3839
                    } else {
3840
                        // If no result then the user just hit don't know
3841
                        $studentChoice = 3;
3842
                        $questionScore += $doubt_score;
3843
                    }
3844
                    $totalScore = $questionScore;
3845
                    break;
3846
                case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
3847
                    if ($from_database) {
3848
                        $choice = [];
3849
                        $choiceDegreeCertainty = [];
3850
                        $sql = "SELECT answer
3851
                            FROM $TBL_TRACK_ATTEMPT
3852
                            WHERE
3853
                            exe_id = $exeId AND question_id = $questionId";
3854
3855
                        $result = Database::query($sql);
3856
                        while ($row = Database::fetch_array($result)) {
3857
                            $ind = $row['answer'];
3858
                            $values = explode(':', $ind);
3859
                            $myAnswerId = $values[0];
3860
                            $option = $values[1];
3861
                            $percent = $values[2];
3862
                            $choice[$myAnswerId] = $option;
3863
                            $choiceDegreeCertainty[$myAnswerId] = $percent;
3864
                        }
3865
                    }
3866
3867
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3868
                    $studentChoiceDegree = isset($choiceDegreeCertainty[$answerAutoId]) ? $choiceDegreeCertainty[$answerAutoId] : null;
3869
3870
                    // student score update
3871
                    if (!empty($studentChoice)) {
3872
                        if ($studentChoice == $answerCorrect) {
3873
                            // correct answer and student is Unsure or PrettySur
3874
                            if (isset($quiz_question_options[$studentChoiceDegree]) &&
3875
                                $quiz_question_options[$studentChoiceDegree]['position'] >= 3 &&
3876
                                $quiz_question_options[$studentChoiceDegree]['position'] < 9
3877
                            ) {
3878
                                $questionScore += $true_score;
3879
                            } else {
3880
                                // student ignore correct answer
3881
                                $questionScore += $doubt_score;
3882
                            }
3883
                        } else {
3884
                            // false answer and student is Unsure or PrettySur
3885
                            if ($quiz_question_options[$studentChoiceDegree]['position'] >= 3
3886
                                && $quiz_question_options[$studentChoiceDegree]['position'] < 9) {
3887
                                $questionScore += $false_score;
3888
                            } else {
3889
                                // student ignore correct answer
3890
                                $questionScore += $doubt_score;
3891
                            }
3892
                        }
3893
                    }
3894
                    $totalScore = $questionScore;
3895
                    break;
3896
                case MULTIPLE_ANSWER:
3897
                    if ($from_database) {
3898
                        $choice = [];
3899
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3900
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3901
                        $resultans = Database::query($sql);
3902
                        while ($row = Database::fetch_array($resultans)) {
3903
                            $choice[$row['answer']] = 1;
3904
                        }
3905
3906
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3907
                        $real_answers[$answerId] = (bool) $studentChoice;
3908
3909
                        if ($studentChoice) {
3910
                            $questionScore += $answerWeighting;
3911
                        }
3912
                    } else {
3913
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3914
                        $real_answers[$answerId] = (bool) $studentChoice;
3915
3916
                        if (isset($studentChoice)) {
3917
                            $correctAnswerId[] = $answerAutoId;
3918
                            $questionScore += $answerWeighting;
3919
                        }
3920
                    }
3921
                    $totalScore += $answerWeighting;
3922
                    break;
3923
                case GLOBAL_MULTIPLE_ANSWER:
3924
                    if ($from_database) {
3925
                        $choice = [];
3926
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3927
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3928
                        $resultans = Database::query($sql);
3929
                        while ($row = Database::fetch_array($resultans)) {
3930
                            $choice[$row['answer']] = 1;
3931
                        }
3932
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3933
                        $real_answers[$answerId] = (bool) $studentChoice;
3934
                        if ($studentChoice) {
3935
                            $questionScore += $answerWeighting;
3936
                        }
3937
                    } else {
3938
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3939
                        if (isset($studentChoice)) {
3940
                            $questionScore += $answerWeighting;
3941
                        }
3942
                        $real_answers[$answerId] = (bool) $studentChoice;
3943
                    }
3944
                    $totalScore += $answerWeighting;
3945
                    if ($debug) {
3946
                        error_log("studentChoice: $studentChoice");
3947
                    }
3948
                    break;
3949
                case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
3950
                    if ($from_database) {
3951
                        $choice = [];
3952
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3953
                                WHERE exe_id = $exeId AND question_id = $questionId";
3954
                        $resultans = Database::query($sql);
3955
                        while ($row = Database::fetch_array($resultans)) {
3956
                            $result = explode(':', $row['answer']);
3957
                            if (isset($result[0])) {
3958
                                $my_answer_id = isset($result[0]) ? $result[0] : '';
3959
                                $option = isset($result[1]) ? $result[1] : '';
3960
                                $choice[$my_answer_id] = $option;
3961
                            }
3962
                        }
3963
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3964
                        $real_answers[$answerId] = false;
3965
                        if ($answerCorrect == $studentChoice) {
3966
                            $real_answers[$answerId] = true;
3967
                        }
3968
                    } else {
3969
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3970
                        $real_answers[$answerId] = false;
3971
                        if ($answerCorrect == $studentChoice) {
3972
                            $real_answers[$answerId] = true;
3973
                        }
3974
                    }
3975
                    break;
3976
                case MULTIPLE_ANSWER_COMBINATION:
3977
                    if ($from_database) {
3978
                        $choice = [];
3979
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3980
                                WHERE exe_id = $exeId AND question_id = $questionId";
3981
                        $resultans = Database::query($sql);
3982
                        while ($row = Database::fetch_array($resultans)) {
3983
                            $choice[$row['answer']] = 1;
3984
                        }
3985
3986
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3987
                        if (1 == $answerCorrect) {
3988
                            $real_answers[$answerId] = false;
3989
                            if ($studentChoice) {
3990
                                $real_answers[$answerId] = true;
3991
                            }
3992
                        } else {
3993
                            $real_answers[$answerId] = true;
3994
                            if ($studentChoice) {
3995
                                $real_answers[$answerId] = false;
3996
                            }
3997
                        }
3998
                    } else {
3999
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
4000
                        if (1 == $answerCorrect) {
4001
                            $real_answers[$answerId] = false;
4002
                            if ($studentChoice) {
4003
                                $real_answers[$answerId] = true;
4004
                            }
4005
                        } else {
4006
                            $real_answers[$answerId] = true;
4007
                            if ($studentChoice) {
4008
                                $real_answers[$answerId] = false;
4009
                            }
4010
                        }
4011
                    }
4012
                    break;
4013
                case FILL_IN_BLANKS:
4014
                    $str = '';
4015
                    $answerFromDatabase = '';
4016
                    if ($from_database) {
4017
                        $sql = "SELECT answer
4018
                                FROM $TBL_TRACK_ATTEMPT
4019
                                WHERE
4020
                                    exe_id = $exeId AND
4021
                                    question_id= $questionId ";
4022
                        $result = Database::query($sql);
4023
                        $str = $answerFromDatabase = Database::result($result, 0, 'answer');
4024
                    }
4025
4026
                    // if ($save_results == false && strpos($answerFromDatabase, 'font color') !== false) {
4027
                    if (false) {
4028
                        // the question is encoded like this
4029
                        // [A] B [C] D [E] F::10,10,10@1
4030
                        // number 1 before the "@" means that is a switchable fill in blank question
4031
                        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4032
                        // means that is a normal fill blank question
4033
                        // first we explode the "::"
4034
                        $pre_array = explode('::', $answer);
4035
4036
                        // is switchable fill blank or not
4037
                        $last = count($pre_array) - 1;
4038
                        $is_set_switchable = explode('@', $pre_array[$last]);
4039
                        $switchable_answer_set = false;
4040
                        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
4041
                            $switchable_answer_set = true;
4042
                        }
4043
                        $answer = '';
4044
                        for ($k = 0; $k < $last; $k++) {
4045
                            $answer .= $pre_array[$k];
4046
                        }
4047
                        // splits weightings that are joined with a comma
4048
                        $answerWeighting = explode(',', $is_set_switchable[0]);
4049
                        // we save the answer because it will be modified
4050
                        $temp = $answer;
4051
                        $answer = '';
4052
                        $j = 0;
4053
                        //initialise answer tags
4054
                        $user_tags = $correct_tags = $real_text = [];
4055
                        // the loop will stop at the end of the text
4056
                        while (1) {
4057
                            // quits the loop if there are no more blanks (detect '[')
4058
                            if ($temp == false || ($pos = api_strpos($temp, '[')) === false) {
4059
                                // adds the end of the text
4060
                                $answer = $temp;
4061
                                $real_text[] = $answer;
4062
                                break; //no more "blanks", quit the loop
4063
                            }
4064
                            // adds the piece of text that is before the blank
4065
                            //and ends with '[' into a general storage array
4066
                            $real_text[] = api_substr($temp, 0, $pos + 1);
4067
                            $answer .= api_substr($temp, 0, $pos + 1);
4068
                            //take the string remaining (after the last "[" we found)
4069
                            $temp = api_substr($temp, $pos + 1);
4070
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4071
                            if (($pos = api_strpos($temp, ']')) === false) {
4072
                                // adds the end of the text
4073
                                $answer .= $temp;
4074
                                break;
4075
                            }
4076
                            if ($from_database) {
4077
                                $str = $answerFromDatabase;
4078
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4079
                                $str = str_replace('\r\n', '', $str);
4080
4081
                                $choice = $arr[1];
4082
                                if (isset($choice[$j])) {
4083
                                    $tmp = api_strrpos($choice[$j], ' / ');
4084
                                    $choice[$j] = api_substr($choice[$j], 0, $tmp);
4085
                                    $choice[$j] = trim($choice[$j]);
4086
                                    // Needed to let characters ' and " to work as part of an answer
4087
                                    $choice[$j] = stripslashes($choice[$j]);
4088
                                } else {
4089
                                    $choice[$j] = null;
4090
                                }
4091
                            } else {
4092
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4093
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4094
                            }
4095
4096
                            $user_tags[] = $choice[$j];
4097
                            // Put the contents of the [] answer tag into correct_tags[]
4098
                            $correct_tags[] = api_substr($temp, 0, $pos);
4099
                            $j++;
4100
                            $temp = api_substr($temp, $pos + 1);
4101
                        }
4102
                        $answer = '';
4103
                        $real_correct_tags = $correct_tags;
4104
                        $chosen_list = [];
4105
4106
                        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...
4107
                            if (0 == $i) {
4108
                                $answer .= $real_text[0];
4109
                            }
4110
                            if (!$switchable_answer_set) {
4111
                                // Needed to parse ' and " characters
4112
                                $user_tags[$i] = stripslashes($user_tags[$i]);
4113
                                if ($correct_tags[$i] == $user_tags[$i]) {
4114
                                    // gives the related weighting to the student
4115
                                    $questionScore += $answerWeighting[$i];
4116
                                    // increments total score
4117
                                    $totalScore += $answerWeighting[$i];
4118
                                    // adds the word in green at the end of the string
4119
                                    $answer .= $correct_tags[$i];
4120
                                } elseif (!empty($user_tags[$i])) {
4121
                                    // else if the word entered by the student IS NOT the same as
4122
                                    // the one defined by the professor
4123
                                    // adds the word in red at the end of the string, and strikes it
4124
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4125
                                } else {
4126
                                    // adds a tabulation if no word has been typed by the student
4127
                                    $answer .= ''; // remove &nbsp; that causes issue
4128
                                }
4129
                            } else {
4130
                                // switchable fill in the blanks
4131
                                if (in_array($user_tags[$i], $correct_tags)) {
4132
                                    $chosen_list[] = $user_tags[$i];
4133
                                    $correct_tags = array_diff($correct_tags, $chosen_list);
4134
                                    // gives the related weighting to the student
4135
                                    $questionScore += $answerWeighting[$i];
4136
                                    // increments total score
4137
                                    $totalScore += $answerWeighting[$i];
4138
                                    // adds the word in green at the end of the string
4139
                                    $answer .= $user_tags[$i];
4140
                                } elseif (!empty($user_tags[$i])) {
4141
                                    // else if the word entered by the student IS NOT the same
4142
                                    // as the one defined by the professor
4143
                                    // adds the word in red at the end of the string, and strikes it
4144
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4145
                                } else {
4146
                                    // adds a tabulation if no word has been typed by the student
4147
                                    $answer .= ''; // remove &nbsp; that causes issue
4148
                                }
4149
                            }
4150
4151
                            // adds the correct word, followed by ] to close the blank
4152
                            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4153
                            if (isset($real_text[$i + 1])) {
4154
                                $answer .= $real_text[$i + 1];
4155
                            }
4156
                        }
4157
                    } else {
4158
                        // insert the student result in the track_e_attempt table, field answer
4159
                        // $answer is the answer like in the c_quiz_answer table for the question
4160
                        // student data are choice[]
4161
                        $listCorrectAnswers = FillBlanks::getAnswerInfo($answer);
4162
                        $switchableAnswerSet = $listCorrectAnswers['switchable'];
4163
                        $answerWeighting = $listCorrectAnswers['weighting'];
4164
                        // user choices is an array $choice
4165
4166
                        // get existing user data in n the BDD
4167
                        if ($from_database) {
4168
                            $listStudentResults = FillBlanks::getAnswerInfo(
4169
                                $answerFromDatabase,
4170
                                true
4171
                            );
4172
                            $choice = $listStudentResults['student_answer'];
4173
                        }
4174
4175
                        // loop other all blanks words
4176
                        if (!$switchableAnswerSet) {
4177
                            // not switchable answer, must be in the same place than teacher order
4178
                            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...
4179
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
4180
                                $correctAnswer = $listCorrectAnswers['words'][$i];
4181
4182
                                if ($debug) {
4183
                                    error_log("Student answer: $i");
4184
                                    error_log($studentAnswer);
4185
                                }
4186
4187
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4188
                                // Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
4189
                                // ENT_QUOTES is used in order to transform ' to &#039;
4190
                                if (!$from_database) {
4191
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4192
                                    if ($debug) {
4193
                                        error_log('Student answer cleaned:');
4194
                                        error_log($studentAnswer);
4195
                                    }
4196
                                }
4197
4198
                                $isAnswerCorrect = 0;
4199
                                if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
4200
                                    // gives the related weighting to the student
4201
                                    $questionScore += $answerWeighting[$i];
4202
                                    // increments total score
4203
                                    $totalScore += $answerWeighting[$i];
4204
                                    $isAnswerCorrect = 1;
4205
                                }
4206
                                if ($debug) {
4207
                                    error_log("isAnswerCorrect $i: $isAnswerCorrect");
4208
                                }
4209
4210
                                $studentAnswerToShow = $studentAnswer;
4211
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4212
                                if ($debug) {
4213
                                    error_log("Fill in blank type: $type");
4214
                                }
4215
                                if ($type == FillBlanks::FILL_THE_BLANK_MENU) {
4216
                                    $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4217
                                    if ($studentAnswer != '') {
4218
                                        foreach ($listMenu as $item) {
4219
                                            if (sha1($item) == $studentAnswer) {
4220
                                                $studentAnswerToShow = $item;
4221
                                            }
4222
                                        }
4223
                                    }
4224
                                }
4225
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4226
                                $listCorrectAnswers['student_score'][$i] = $isAnswerCorrect;
4227
                            }
4228
                        } else {
4229
                            // switchable answer
4230
                            $listStudentAnswerTemp = $choice;
4231
                            $listTeacherAnswerTemp = $listCorrectAnswers['words'];
4232
4233
                            // for every teacher answer, check if there is a student answer
4234
                            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...
4235
                                $studentAnswer = trim($listStudentAnswerTemp[$i]);
4236
                                $studentAnswerToShow = $studentAnswer;
4237
4238
                                if (empty($studentAnswer)) {
4239
                                    continue;
4240
                                }
4241
4242
                                if ($debug) {
4243
                                    error_log("Student answer: $i");
4244
                                    error_log($studentAnswer);
4245
                                }
4246
4247
                                if (!$from_database) {
4248
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4249
                                    if ($debug) {
4250
                                        error_log("Student answer cleaned:");
4251
                                        error_log($studentAnswer);
4252
                                    }
4253
                                }
4254
4255
                                $found = false;
4256
                                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...
4257
                                    $correctAnswer = $listTeacherAnswerTemp[$j];
4258
4259
                                    if (!$found) {
4260
                                        if (FillBlanks::isStudentAnswerGood(
4261
                                            $studentAnswer,
4262
                                            $correctAnswer,
4263
                                            $from_database
4264
                                        )) {
4265
                                            $questionScore += $answerWeighting[$i];
4266
                                            $totalScore += $answerWeighting[$i];
4267
                                            $listTeacherAnswerTemp[$j] = '';
4268
                                            $found = true;
4269
                                        }
4270
                                    }
4271
4272
                                    $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4273
                                    if ($type == FillBlanks::FILL_THE_BLANK_MENU) {
4274
                                        $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4275
                                        if (!empty($studentAnswer)) {
4276
                                            foreach ($listMenu as $key => $item) {
4277
                                                if ($key == $correctAnswer) {
4278
                                                    $studentAnswerToShow = $item;
4279
                                                    break;
4280
                                                }
4281
                                            }
4282
                                        }
4283
                                    }
4284
                                }
4285
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4286
                                $listCorrectAnswers['student_score'][$i] = $found ? 1 : 0;
4287
                            }
4288
                        }
4289
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
4290
                    }
4291
                    break;
4292
                case CALCULATED_ANSWER:
4293
                    $calculatedAnswerList = Session::read('calculatedAnswerId');
4294
                    if (!empty($calculatedAnswerList)) {
4295
                        $answer = $objAnswerTmp->selectAnswer($calculatedAnswerList[$questionId]);
4296
                        $preArray = explode('@@', $answer);
4297
                        $last = count($preArray) - 1;
4298
                        $answer = '';
4299
                        for ($k = 0; $k < $last; $k++) {
4300
                            $answer .= $preArray[$k];
4301
                        }
4302
                        $answerWeighting = [$answerWeighting];
4303
                        // we save the answer because it will be modified
4304
                        $temp = $answer;
4305
                        $answer = '';
4306
                        $j = 0;
4307
                        // initialise answer tags
4308
                        $userTags = $correctTags = $realText = [];
4309
                        // the loop will stop at the end of the text
4310
                        while (1) {
4311
                            // quits the loop if there are no more blanks (detect '[')
4312
                            if ($temp == false || ($pos = api_strpos($temp, '[')) === false) {
4313
                                // adds the end of the text
4314
                                $answer = $temp;
4315
                                $realText[] = $answer;
4316
                                break; //no more "blanks", quit the loop
4317
                            }
4318
                            // adds the piece of text that is before the blank
4319
                            // and ends with '[' into a general storage array
4320
                            $realText[] = api_substr($temp, 0, $pos + 1);
4321
                            $answer .= api_substr($temp, 0, $pos + 1);
4322
                            // take the string remaining (after the last "[" we found)
4323
                            $temp = api_substr($temp, $pos + 1);
4324
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4325
                            if (false === ($pos = api_strpos($temp, ']'))) {
4326
                                // adds the end of the text
4327
                                $answer .= $temp;
4328
                                break;
4329
                            }
4330
4331
                            if ($from_database) {
4332
                                $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4333
                                        WHERE
4334
                                            exe_id = $exeId AND
4335
                                            question_id = $questionId ";
4336
                                $result = Database::query($sql);
4337
                                $str = Database::result($result, 0, 'answer');
4338
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4339
                                $str = str_replace('\r\n', '', $str);
4340
                                $choice = $arr[1];
4341
                                if (isset($choice[$j])) {
4342
                                    $tmp = api_strrpos($choice[$j], ' / ');
4343
                                    if ($tmp) {
4344
                                        $choice[$j] = api_substr($choice[$j], 0, $tmp);
4345
                                    } else {
4346
                                        $tmp = ltrim($tmp, '[');
4347
                                        $tmp = rtrim($tmp, ']');
4348
                                    }
4349
                                    $choice[$j] = trim($choice[$j]);
4350
                                    // Needed to let characters ' and " to work as part of an answer
4351
                                    $choice[$j] = stripslashes($choice[$j]);
4352
                                } else {
4353
                                    $choice[$j] = null;
4354
                                }
4355
                            } else {
4356
                                // This value is the user input not escaped while correct answer is escaped by ckeditor
4357
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4358
                            }
4359
                            $userTags[] = $choice[$j];
4360
                            // put the contents of the [] answer tag into correct_tags[]
4361
                            $correctTags[] = api_substr($temp, 0, $pos);
4362
                            $j++;
4363
                            $temp = api_substr($temp, $pos + 1);
4364
                        }
4365
                        $answer = '';
4366
                        $realCorrectTags = $correctTags;
4367
                        $calculatedStatus = Display::label(get_lang('Incorrect'), 'danger');
4368
                        $expectedAnswer = '';
4369
                        $calculatedChoice = '';
4370
4371
                        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...
4372
                            if (0 == $i) {
4373
                                $answer .= $realText[0];
4374
                            }
4375
                            // Needed to parse ' and " characters
4376
                            $userTags[$i] = stripslashes($userTags[$i]);
4377
                            if ($correctTags[$i] == $userTags[$i]) {
4378
                                // gives the related weighting to the student
4379
                                $questionScore += $answerWeighting[$i];
4380
                                // increments total score
4381
                                $totalScore += $answerWeighting[$i];
4382
                                // adds the word in green at the end of the string
4383
                                $answer .= $correctTags[$i];
4384
                                $calculatedChoice = $correctTags[$i];
4385
                            } elseif (!empty($userTags[$i])) {
4386
                                // else if the word entered by the student IS NOT the same as
4387
                                // the one defined by the professor
4388
                                // adds the word in red at the end of the string, and strikes it
4389
                                $answer .= '<font color="red"><s>'.$userTags[$i].'</s></font>';
4390
                                $calculatedChoice = $userTags[$i];
4391
                            } else {
4392
                                // adds a tabulation if no word has been typed by the student
4393
                                $answer .= ''; // remove &nbsp; that causes issue
4394
                            }
4395
                            // adds the correct word, followed by ] to close the blank
4396
                            if (EXERCISE_FEEDBACK_TYPE_EXAM != $this->results_disabled) {
4397
                                $answer .= ' / <font color="green"><b>'.$realCorrectTags[$i].'</b></font>';
4398
                                $calculatedStatus = Display::label(get_lang('Correct'), 'success');
4399
                                $expectedAnswer = $realCorrectTags[$i];
4400
                            }
4401
                            $answer .= ']';
4402
                            if (isset($realText[$i + 1])) {
4403
                                $answer .= $realText[$i + 1];
4404
                            }
4405
                        }
4406
                    } else {
4407
                        if ($from_database) {
4408
                            $sql = "SELECT *
4409
                                    FROM $TBL_TRACK_ATTEMPT
4410
                                    WHERE
4411
                                        exe_id = $exeId AND
4412
                                        question_id = $questionId ";
4413
                            $result = Database::query($sql);
4414
                            $resultData = Database::fetch_array($result, 'ASSOC');
4415
                            $answer = $resultData['answer'];
4416
                            $questionScore = $resultData['marks'];
4417
                        }
4418
                    }
4419
                    break;
4420
                case FREE_ANSWER:
4421
                    if ($from_database) {
4422
                        $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
4423
                                 WHERE
4424
                                    exe_id = $exeId AND
4425
                                    question_id= ".$questionId;
4426
                        $result = Database::query($sql);
4427
                        $data = Database::fetch_array($result);
4428
                        $choice = '';
4429
                        if ($data) {
4430
                            $choice = $data['answer'];
4431
                        }
4432
4433
                        $choice = str_replace('\r\n', '', $choice);
4434
                        $choice = stripslashes($choice);
4435
                        $questionScore = $data['marks'];
4436
4437
                        if (-1 == $questionScore) {
4438
                            $totalScore += 0;
4439
                        } else {
4440
                            $totalScore += $questionScore;
4441
                        }
4442
                        if ('' == $questionScore) {
4443
                            $questionScore = 0;
4444
                        }
4445
                        $arrques = $questionName;
4446
                        $arrans = $choice;
4447
                    } else {
4448
                        $studentChoice = $choice;
4449
                        if ($studentChoice) {
4450
                            //Fixing negative puntation see #2193
4451
                            $questionScore = 0;
4452
                            $totalScore += 0;
4453
                        }
4454
                    }
4455
                    break;
4456
                case ORAL_EXPRESSION:
4457
                    if ($from_database) {
4458
                        $query = "SELECT answer, marks
4459
                                  FROM $TBL_TRACK_ATTEMPT
4460
                                  WHERE
4461
                                        exe_id = $exeId AND
4462
                                        question_id = $questionId
4463
                                 ";
4464
                        $resq = Database::query($query);
4465
                        $row = Database::fetch_assoc($resq);
4466
                        $choice = $row['answer'];
4467
                        $choice = str_replace('\r\n', '', $choice);
4468
                        $choice = stripslashes($choice);
4469
                        $questionScore = $row['marks'];
4470
                        if ($questionScore == -1) {
4471
                            $totalScore += 0;
4472
                        } else {
4473
                            $totalScore += $questionScore;
4474
                        }
4475
                        $arrques = $questionName;
4476
                        $arrans = $choice;
4477
                    } else {
4478
                        $studentChoice = $choice;
4479
                        if ($studentChoice) {
4480
                            //Fixing negative puntation see #2193
4481
                            $questionScore = 0;
4482
                            $totalScore += 0;
4483
                        }
4484
                    }
4485
                    break;
4486
                case DRAGGABLE:
4487
                case MATCHING_DRAGGABLE:
4488
                case MATCHING:
4489
                    if ($from_database) {
4490
                        $sql = "SELECT id, answer, id_auto
4491
                                FROM $table_ans
4492
                                WHERE
4493
                                    c_id = $course_id AND
4494
                                    question_id = $questionId AND
4495
                                    correct = 0
4496
                                ";
4497
                        $result = Database::query($sql);
4498
                        // Getting the real answer
4499
                        $real_list = [];
4500
                        while ($realAnswer = Database::fetch_array($result)) {
4501
                            $real_list[$realAnswer['id_auto']] = $realAnswer['answer'];
4502
                        }
4503
4504
                        $sql = "SELECT id, answer, correct, id_auto, ponderation
4505
                                FROM $table_ans
4506
                                WHERE
4507
                                    c_id = $course_id AND
4508
                                    question_id = $questionId AND
4509
                                    correct <> 0
4510
                                ORDER BY id_auto";
4511
                        $result = Database::query($sql);
4512
                        $options = [];
4513
                        while ($row = Database::fetch_array($result, 'ASSOC')) {
4514
                            $options[] = $row;
4515
                        }
4516
4517
                        $questionScore = 0;
4518
                        $counterAnswer = 1;
4519
                        foreach ($options as $a_answers) {
4520
                            $i_answer_id = $a_answers['id']; //3
4521
                            $s_answer_label = $a_answers['answer']; // your daddy - your mother
4522
                            $i_answer_correct_answer = $a_answers['correct']; //1 - 2
4523
                            $i_answer_id_auto = $a_answers['id_auto']; // 3 - 4
4524
4525
                            $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4526
                                    WHERE
4527
                                        exe_id = '$exeId' AND
4528
                                        question_id = '$questionId' AND
4529
                                        position = '$i_answer_id_auto'";
4530
                            $result = Database::query($sql);
4531
                            $s_user_answer = 0;
4532
                            if (Database::num_rows($result) > 0) {
4533
                                //  rich - good looking
4534
                                $s_user_answer = Database::result($result, 0, 0);
4535
                            }
4536
                            $i_answerWeighting = $a_answers['ponderation'];
4537
                            $user_answer = '';
4538
                            $status = Display::label(get_lang('Incorrect'), 'danger');
4539
4540
                            if (!empty($s_user_answer)) {
4541
                                if (DRAGGABLE == $answerType) {
4542
                                    if ($s_user_answer == $i_answer_correct_answer) {
4543
                                        $questionScore += $i_answerWeighting;
4544
                                        $totalScore += $i_answerWeighting;
4545
                                        $user_answer = Display::label(get_lang('Correct'), 'success');
4546
                                        if ($this->showExpectedChoice()) {
4547
                                            $user_answer = $answerMatching[$i_answer_id_auto];
4548
                                        }
4549
                                        $status = Display::label(get_lang('Correct'), 'success');
4550
                                    } else {
4551
                                        $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4552
                                        if ($this->showExpectedChoice()) {
4553
                                            $data = $options[$real_list[$s_user_answer] - 1];
4554
                                            $user_answer = $data['answer'];
4555
                                        }
4556
                                    }
4557
                                } else {
4558
                                    if ($s_user_answer == $i_answer_correct_answer) {
4559
                                        $questionScore += $i_answerWeighting;
4560
                                        $totalScore += $i_answerWeighting;
4561
                                        $status = Display::label(get_lang('Correct'), 'success');
4562
4563
                                        // Try with id
4564
                                        if (isset($real_list[$i_answer_id])) {
4565
                                            $user_answer = Display::span(
4566
                                                $real_list[$i_answer_id],
4567
                                                ['style' => 'color: #008000; font-weight: bold;']
4568
                                            );
4569
                                        }
4570
4571
                                        // Try with $i_answer_id_auto
4572
                                        if (empty($user_answer)) {
4573
                                            if (isset($real_list[$i_answer_id_auto])) {
4574
                                                $user_answer = Display::span(
4575
                                                    $real_list[$i_answer_id_auto],
4576
                                                    ['style' => 'color: #008000; font-weight: bold;']
4577
                                                );
4578
                                            }
4579
                                        }
4580
4581
                                        if (isset($real_list[$i_answer_correct_answer])) {
4582
                                            $user_answer = Display::span(
4583
                                                $real_list[$i_answer_correct_answer],
4584
                                                ['style' => 'color: #008000; font-weight: bold;']
4585
                                            );
4586
                                        }
4587
                                    } else {
4588
                                        $user_answer = Display::span(
4589
                                            $real_list[$s_user_answer],
4590
                                            ['style' => 'color: #FF0000; text-decoration: line-through;']
4591
                                        );
4592
                                        if ($this->showExpectedChoice()) {
4593
                                            if (isset($real_list[$s_user_answer])) {
4594
                                                $user_answer = Display::span($real_list[$s_user_answer]);
4595
                                            }
4596
                                        }
4597
                                    }
4598
                                }
4599
                            } elseif (DRAGGABLE == $answerType) {
4600
                                $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4601
                                if ($this->showExpectedChoice()) {
4602
                                    $user_answer = '';
4603
                                }
4604
                            } else {
4605
                                $user_answer = Display::span(
4606
                                    get_lang('Incorrect').' &nbsp;',
4607
                                    ['style' => 'color: #FF0000; text-decoration: line-through;']
4608
                                );
4609
                                if ($this->showExpectedChoice()) {
4610
                                    $user_answer = '';
4611
                                }
4612
                            }
4613
4614
                            if ($show_result) {
4615
                                if (false === $this->showExpectedChoice() &&
4616
                                    false === $showTotalScoreAndUserChoicesInLastAttempt
4617
                                ) {
4618
                                    $user_answer = '';
4619
                                }
4620
                                switch ($answerType) {
4621
                                    case MATCHING:
4622
                                    case MATCHING_DRAGGABLE:
4623
                                        echo '<tr>';
4624
                                        if (!in_array(
4625
                                            $this->results_disabled,
4626
                                            [
4627
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4628
                                            ]
4629
                                        )
4630
                                        ) {
4631
                                            echo '<td>'.$s_answer_label.'</td>';
4632
                                            echo '<td>'.$user_answer.'</td>';
4633
                                        } else {
4634
                                            echo '<td>'.$s_answer_label.'</td>';
4635
                                            $status = Display::label(get_lang('Correct'), 'success');
4636
                                        }
4637
4638
                                        if ($this->showExpectedChoice()) {
4639
                                            if ($this->showExpectedChoiceColumn()) {
4640
                                                echo '<td>';
4641
                                                if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4642
                                                    if (isset($real_list[$i_answer_correct_answer]) &&
4643
                                                        $showTotalScoreAndUserChoicesInLastAttempt == true
4644
                                                    ) {
4645
                                                        echo Display::span(
4646
                                                            $real_list[$i_answer_correct_answer]
4647
                                                        );
4648
                                                    }
4649
                                                }
4650
                                                echo '</td>';
4651
                                            }
4652
                                            echo '<td>'.$status.'</td>';
4653
                                        } else {
4654
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4655
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4656
                                                    $showTotalScoreAndUserChoicesInLastAttempt === true
4657
                                                ) {
4658
                                                    if ($this->showExpectedChoiceColumn()) {
4659
                                                        echo '<td>';
4660
                                                        echo Display::span(
4661
                                                            $real_list[$i_answer_correct_answer],
4662
                                                            ['style' => 'color: #008000; font-weight: bold;']
4663
                                                        );
4664
                                                        echo '</td>';
4665
                                                    }
4666
                                                }
4667
                                            }
4668
                                        }
4669
                                        echo '</tr>';
4670
                                        break;
4671
                                    case DRAGGABLE:
4672
                                        if (false == $showTotalScoreAndUserChoicesInLastAttempt) {
4673
                                            $s_answer_label = '';
4674
                                        }
4675
                                        echo '<tr>';
4676
                                        if ($this->showExpectedChoice()) {
4677
                                            if (!in_array($this->results_disabled, [
4678
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4679
                                                //RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4680
                                            ])
4681
                                            ) {
4682
                                                echo '<td>'.$user_answer.'</td>';
4683
                                            } else {
4684
                                                $status = Display::label(get_lang('Correct'), 'success');
4685
                                            }
4686
                                            echo '<td>'.$s_answer_label.'</td>';
4687
                                            echo '<td>'.$status.'</td>';
4688
                                        } else {
4689
                                            echo '<td>'.$s_answer_label.'</td>';
4690
                                            echo '<td>'.$user_answer.'</td>';
4691
                                            echo '<td>';
4692
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4693
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4694
                                                    $showTotalScoreAndUserChoicesInLastAttempt === true
4695
                                                ) {
4696
                                                    echo Display::span(
4697
                                                        $real_list[$i_answer_correct_answer],
4698
                                                        ['style' => 'color: #008000; font-weight: bold;']
4699
                                                    );
4700
                                                }
4701
                                            }
4702
                                            echo '</td>';
4703
                                        }
4704
                                        echo '</tr>';
4705
                                        break;
4706
                                }
4707
                            }
4708
                            $counterAnswer++;
4709
                        }
4710
                        break 2; // break the switch and the "for" condition
4711
                    } else {
4712
                        if ($answerCorrect) {
4713
                            if (isset($choice[$answerAutoId]) &&
4714
                                $answerCorrect == $choice[$answerAutoId]
4715
                            ) {
4716
                                $correctAnswerId[] = $answerAutoId;
4717
                                $questionScore += $answerWeighting;
4718
                                $totalScore += $answerWeighting;
4719
                                $user_answer = Display::span($answerMatching[$choice[$answerAutoId]]);
4720
                            } else {
4721
                                if (isset($answerMatching[$choice[$answerAutoId]])) {
4722
                                    $user_answer = Display::span(
4723
                                        $answerMatching[$choice[$answerAutoId]],
4724
                                        ['style' => 'color: #FF0000; text-decoration: line-through;']
4725
                                    );
4726
                                }
4727
                            }
4728
                            $matching[$answerAutoId] = $choice[$answerAutoId];
4729
                        }
4730
                    }
4731
                    break;
4732
                case HOT_SPOT:
4733
                    if ($from_database) {
4734
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4735
                        // Check auto id
4736
                        $foundAnswerId = $answerAutoId;
4737
                        $sql = "SELECT hotspot_correct
4738
                                FROM $TBL_TRACK_HOTSPOT
4739
                                WHERE
4740
                                    hotspot_exe_id = $exeId AND
4741
                                    hotspot_question_id= $questionId AND
4742
                                    hotspot_answer_id = $answerAutoId
4743
                                ORDER BY hotspot_id ASC";
4744
                        $result = Database::query($sql);
4745
                        if (Database::num_rows($result)) {
4746
                            $studentChoice = Database::result(
4747
                                $result,
4748
                                0,
4749
                                'hotspot_correct'
4750
                            );
4751
4752
                            if ($studentChoice) {
4753
                                $questionScore += $answerWeighting;
4754
                                $totalScore += $answerWeighting;
4755
                            }
4756
                        } else {
4757
                            // If answer.id is different:
4758
                            $sql = "SELECT hotspot_correct
4759
                                FROM $TBL_TRACK_HOTSPOT
4760
                                WHERE
4761
                                    hotspot_exe_id = $exeId AND
4762
                                    hotspot_question_id= $questionId AND
4763
                                    hotspot_answer_id = ".intval($answerId)."
4764
                                ORDER BY hotspot_id ASC";
4765
                            $result = Database::query($sql);
4766
                            $foundAnswerId = $answerId;
4767
                            if (Database::num_rows($result)) {
4768
                                $studentChoice = Database::result(
4769
                                    $result,
4770
                                    0,
4771
                                    'hotspot_correct'
4772
                                );
4773
4774
                                if ($studentChoice) {
4775
                                    $questionScore += $answerWeighting;
4776
                                    $totalScore += $answerWeighting;
4777
                                }
4778
                            } else {
4779
                                // check answer.iid
4780
                                if (!empty($answerIid)) {
4781
                                    $sql = "SELECT hotspot_correct
4782
                                            FROM $TBL_TRACK_HOTSPOT
4783
                                            WHERE
4784
                                                hotspot_exe_id = $exeId AND
4785
                                                hotspot_question_id= $questionId AND
4786
                                                hotspot_answer_id = $answerIid
4787
                                            ORDER BY hotspot_id ASC";
4788
                                    $result = Database::query($sql);
4789
                                    $foundAnswerId = $answerIid;
4790
                                    $studentChoice = Database::result(
4791
                                        $result,
4792
                                        0,
4793
                                        'hotspot_correct'
4794
                                    );
4795
4796
                                    if ($studentChoice) {
4797
                                        $questionScore += $answerWeighting;
4798
                                        $totalScore += $answerWeighting;
4799
                                    }
4800
                                }
4801
                            }
4802
                        }
4803
                    } else {
4804
                        if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
4805
                            $choice[$answerAutoId] = 0;
4806
                            $choice[$answerIid] = 0;
4807
                        } else {
4808
                            $studentChoice = $choice[$answerAutoId];
4809
                            if (empty($studentChoice)) {
4810
                                $studentChoice = $choice[$answerIid];
4811
                            }
4812
                            $choiceIsValid = false;
4813
                            if (!empty($studentChoice)) {
4814
                                $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
4815
                                $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
4816
                                $choicePoint = Geometry::decodePoint($studentChoice);
4817
4818
                                switch ($hotspotType) {
4819
                                    case 'square':
4820
                                        $hotspotProperties = Geometry::decodeSquare($hotspotCoordinates);
4821
                                        $choiceIsValid = Geometry::pointIsInSquare($hotspotProperties, $choicePoint);
4822
                                        break;
4823
                                    case 'circle':
4824
                                        $hotspotProperties = Geometry::decodeEllipse($hotspotCoordinates);
4825
                                        $choiceIsValid = Geometry::pointIsInEllipse($hotspotProperties, $choicePoint);
4826
                                        break;
4827
                                    case 'poly':
4828
                                        $hotspotProperties = Geometry::decodePolygon($hotspotCoordinates);
4829
                                        $choiceIsValid = Geometry::pointIsInPolygon($hotspotProperties, $choicePoint);
4830
                                        break;
4831
                                }
4832
                            }
4833
4834
                            $choice[$answerAutoId] = 0;
4835
                            if ($choiceIsValid) {
4836
                                $questionScore += $answerWeighting;
4837
                                $totalScore += $answerWeighting;
4838
                                $choice[$answerAutoId] = 1;
4839
                                $choice[$answerIid] = 1;
4840
                            }
4841
                        }
4842
                    }
4843
                    break;
4844
                case HOT_SPOT_ORDER:
4845
                    // @todo never added to chamilo
4846
                    // for hotspot with fixed order
4847
                    $studentChoice = $choice['order'][$answerId];
4848
                    if ($studentChoice == $answerId) {
4849
                        $questionScore += $answerWeighting;
4850
                        $totalScore += $answerWeighting;
4851
                        $studentChoice = true;
4852
                    } else {
4853
                        $studentChoice = false;
4854
                    }
4855
                    break;
4856
                case HOT_SPOT_DELINEATION:
4857
                    // for hotspot with delineation
4858
                    if ($from_database) {
4859
                        // getting the user answer
4860
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4861
                        $query = "SELECT hotspot_correct, hotspot_coordinate
4862
                                    FROM $TBL_TRACK_HOTSPOT
4863
                                    WHERE
4864
                                        hotspot_exe_id = $exeId AND
4865
                                        hotspot_question_id= $questionId AND
4866
                                        hotspot_answer_id = '1'";
4867
                        // By default we take 1 because it's a delineation
4868
                        $resq = Database::query($query);
4869
                        $row = Database::fetch_array($resq, 'ASSOC');
4870
4871
                        $choice = $row['hotspot_correct'];
4872
                        $user_answer = $row['hotspot_coordinate'];
4873
4874
                        // THIS is very important otherwise the poly_compile will throw an error!!
4875
                        // round-up the coordinates
4876
                        $coords = explode('/', $user_answer);
4877
                        $coords = array_filter($coords);
4878
                        $user_array = '';
4879
                        foreach ($coords as $coord) {
4880
                            list($x, $y) = explode(';', $coord);
4881
                            $user_array .= round($x).';'.round($y).'/';
4882
                        }
4883
                        $user_array = substr($user_array, 0, -1) ?: '';
4884
                    } else {
4885
                        if (!empty($studentChoice)) {
4886
                            $correctAnswerId[] = $answerAutoId;
4887
                            $newquestionList[] = $questionId;
4888
                        }
4889
4890
                        if (1 === $answerId) {
4891
                            $studentChoice = $choice[$answerId];
4892
                            $questionScore += $answerWeighting;
4893
                        }
4894
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
4895
                            $user_array = $_SESSION['exerciseResultCoordinates'][$questionId];
4896
                        }
4897
                    }
4898
                    $_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
4899
                    $_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
4900
                    break;
4901
                case ANNOTATION:
4902
                    if ($from_database) {
4903
                        $sql = "SELECT answer, marks
4904
                                FROM $TBL_TRACK_ATTEMPT
4905
                                WHERE
4906
                                  exe_id = $exeId AND
4907
                                  question_id = $questionId ";
4908
                        $resq = Database::query($sql);
4909
                        $data = Database::fetch_array($resq);
4910
4911
                        $questionScore = empty($data['marks']) ? 0 : $data['marks'];
4912
                        $arrques = $questionName;
4913
                        break;
4914
                    }
4915
                    $studentChoice = $choice;
4916
                    if ($studentChoice) {
4917
                        $questionScore = 0;
4918
                    }
4919
                    break;
4920
            }
4921
4922
            if ($show_result) {
4923
                if ('exercise_result' === $from) {
4924
                    // Display answers (if not matching type, or if the answer is correct)
4925
                    if (!in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE]) ||
4926
                        $answerCorrect
4927
                    ) {
4928
                        if (in_array(
4929
                            $answerType,
4930
                            [
4931
                                UNIQUE_ANSWER,
4932
                                UNIQUE_ANSWER_IMAGE,
4933
                                UNIQUE_ANSWER_NO_OPTION,
4934
                                MULTIPLE_ANSWER,
4935
                                MULTIPLE_ANSWER_COMBINATION,
4936
                                GLOBAL_MULTIPLE_ANSWER,
4937
                                READING_COMPREHENSION,
4938
                            ]
4939
                        )) {
4940
                            ExerciseShowFunctions::display_unique_or_multiple_answer(
4941
                                $this,
4942
                                $feedback_type,
4943
                                $answerType,
4944
                                $studentChoice,
4945
                                $answer,
4946
                                $answerComment,
4947
                                $answerCorrect,
4948
                                0,
4949
                                0,
4950
                                0,
4951
                                $results_disabled,
4952
                                $showTotalScoreAndUserChoicesInLastAttempt,
4953
                                $this->export
4954
                            );
4955
                        } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE) {
4956
                            ExerciseShowFunctions::display_multiple_answer_true_false(
4957
                                $this,
4958
                                $feedback_type,
4959
                                $answerType,
4960
                                $studentChoice,
4961
                                $answer,
4962
                                $answerComment,
4963
                                $answerCorrect,
4964
                                0,
4965
                                $questionId,
4966
                                0,
4967
                                $results_disabled,
4968
                                $showTotalScoreAndUserChoicesInLastAttempt
4969
                            );
4970
                        } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
4971
                            ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
4972
                                $this,
4973
                                $feedback_type,
4974
                                $studentChoice,
4975
                                $studentChoiceDegree,
4976
                                $answer,
4977
                                $answerComment,
4978
                                $answerCorrect,
4979
                                $questionId,
4980
                                $results_disabled
4981
                            );
4982
                        } elseif ($answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE) {
4983
                            ExerciseShowFunctions::display_multiple_answer_combination_true_false(
4984
                                $this,
4985
                                $feedback_type,
4986
                                $answerType,
4987
                                $studentChoice,
4988
                                $answer,
4989
                                $answerComment,
4990
                                $answerCorrect,
4991
                                0,
4992
                                0,
4993
                                0,
4994
                                $results_disabled,
4995
                                $showTotalScoreAndUserChoicesInLastAttempt
4996
                            );
4997
                        } elseif ($answerType == FILL_IN_BLANKS) {
4998
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
4999
                                $this,
5000
                                $feedback_type,
5001
                                $answer,
5002
                                0,
5003
                                0,
5004
                                $results_disabled,
5005
                                '',
5006
                                $showTotalScoreAndUserChoicesInLastAttempt
5007
                            );
5008
                        } elseif ($answerType == CALCULATED_ANSWER) {
5009
                            ExerciseShowFunctions::display_calculated_answer(
5010
                                $this,
5011
                                $feedback_type,
5012
                                $answer,
5013
                                0,
5014
                                0,
5015
                                $results_disabled,
5016
                                $showTotalScoreAndUserChoicesInLastAttempt,
5017
                                $expectedAnswer,
5018
                                $calculatedChoice,
5019
                                $calculatedStatus
5020
                            );
5021
                        } elseif ($answerType == FREE_ANSWER) {
5022
                            ExerciseShowFunctions::display_free_answer(
5023
                                $feedback_type,
5024
                                $choice,
5025
                                $exeId,
5026
                                $questionId,
5027
                                $questionScore,
5028
                                $results_disabled
5029
                            );
5030
                        } elseif ($answerType == ORAL_EXPRESSION) {
5031
                            // to store the details of open questions in an array to be used in mail
5032
                            /** @var OralExpression $objQuestionTmp */
5033
                            ExerciseShowFunctions::display_oral_expression_answer(
5034
                                $feedback_type,
5035
                                $choice,
5036
                                0,
5037
                                0,
5038
                                $objQuestionTmp->getFileUrl(true),
5039
                                $results_disabled,
5040
                                $questionScore
5041
                            );
5042
                        } elseif ($answerType == HOT_SPOT) {
5043
                            $correctAnswerId = 0;
5044
                            /** @var TrackEHotspot $hotspot */
5045
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5046
                                if ($hotspot->getHotspotAnswerId() == $answerIid) {
5047
                                    break;
5048
                                }
5049
                            }
5050
5051
                            // force to show whether the choice is correct or not
5052
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
5053
                            ExerciseShowFunctions::display_hotspot_answer(
5054
                                $this,
5055
                                $feedback_type,
5056
                                ++$correctAnswerId,
5057
                                $answer,
5058
                                $studentChoice,
5059
                                $answerComment,
5060
                                $results_disabled,
5061
                                $correctAnswerId,
5062
                                $showTotalScoreAndUserChoicesInLastAttempt
5063
                            );
5064
                        } elseif ($answerType == HOT_SPOT_ORDER) {
5065
                            ExerciseShowFunctions::display_hotspot_order_answer(
5066
                                $feedback_type,
5067
                                $answerId,
5068
                                $answer,
5069
                                $studentChoice,
5070
                                $answerComment
5071
                            );
5072
                        } elseif ($answerType == HOT_SPOT_DELINEATION) {
5073
                            $user_answer = $_SESSION['exerciseResultCoordinates'][$questionId];
5074
5075
                            // Round-up the coordinates
5076
                            $coords = explode('/', $user_answer);
5077
                            $coords = array_filter($coords);
5078
                            $user_array = '';
5079
                            foreach ($coords as $coord) {
5080
                                if (!empty($coord)) {
5081
                                    $parts = explode(';', $coord);
5082
                                    if (!empty($parts)) {
5083
                                        $user_array .= round($parts[0]).';'.round($parts[1]).'/';
5084
                                    }
5085
                                }
5086
                            }
5087
                            $user_array = substr($user_array, 0, -1) ?: '';
5088
                            if ($next) {
5089
                                $user_answer = $user_array;
5090
                                // We compare only the delineation not the other points
5091
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5092
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5093
5094
                                // Calculating the area
5095
                                $poly_user = convert_coordinates($user_answer, '/');
5096
                                $poly_answer = convert_coordinates($answer_question, '|');
5097
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5098
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5099
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5100
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5101
5102
                                $overlap = $poly_results['both'];
5103
                                $poly_answer_area = $poly_results['s1'];
5104
                                $poly_user_area = $poly_results['s2'];
5105
                                $missing = $poly_results['s1Only'];
5106
                                $excess = $poly_results['s2Only'];
5107
5108
                                // //this is an area in pixels
5109
                                if ($debug > 0) {
5110
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5111
                                }
5112
5113
                                if ($overlap < 1) {
5114
                                    // Shortcut to avoid complicated calculations
5115
                                    $final_overlap = 0;
5116
                                    $final_missing = 100;
5117
                                    $final_excess = 100;
5118
                                } else {
5119
                                    // the final overlap is the percentage of the initial polygon
5120
                                    // that is overlapped by the user's polygon
5121
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5122
                                    if ($debug > 1) {
5123
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap, 0);
5124
                                    }
5125
                                    // the final missing area is the percentage of the initial polygon
5126
                                    // that is not overlapped by the user's polygon
5127
                                    $final_missing = 100 - $final_overlap;
5128
                                    if ($debug > 1) {
5129
                                        error_log(__LINE__.' - Final missing is '.$final_missing, 0);
5130
                                    }
5131
                                    // the final excess area is the percentage of the initial polygon's size
5132
                                    // that is covered by the user's polygon outside of the initial polygon
5133
                                    $final_excess = round((((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100);
5134
                                    if ($debug > 1) {
5135
                                        error_log(__LINE__.' - Final excess is '.$final_excess, 0);
5136
                                    }
5137
                                }
5138
5139
                                // Checking the destination parameters parsing the "@@"
5140
                                $destination_items = explode('@@', $answerDestination);
5141
                                $threadhold_total = $destination_items[0];
5142
                                $threadhold_items = explode(';', $threadhold_total);
5143
                                $threadhold1 = $threadhold_items[0]; // overlap
5144
                                $threadhold2 = $threadhold_items[1]; // excess
5145
                                $threadhold3 = $threadhold_items[2]; // missing
5146
5147
                                // if is delineation
5148
                                if ($answerId === 1) {
5149
                                    //setting colors
5150
                                    if ($final_overlap >= $threadhold1) {
5151
                                        $overlap_color = true;
5152
                                    }
5153
                                    if ($final_excess <= $threadhold2) {
5154
                                        $excess_color = true;
5155
                                    }
5156
                                    if ($final_missing <= $threadhold3) {
5157
                                        $missing_color = true;
5158
                                    }
5159
5160
                                    // if pass
5161
                                    if ($final_overlap >= $threadhold1 &&
5162
                                        $final_missing <= $threadhold3 &&
5163
                                        $final_excess <= $threadhold2
5164
                                    ) {
5165
                                        $next = 1; //go to the oars
5166
                                        $result_comment = get_lang('Acceptable');
5167
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5168
                                    } else {
5169
                                        $next = 0;
5170
                                        $result_comment = get_lang('Unacceptable');
5171
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5172
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5173
                                        // checking the destination parameters parsing the "@@"
5174
                                        $destination_items = explode('@@', $answerDestination);
5175
                                    }
5176
                                } elseif ($answerId > 1) {
5177
                                    if ($objAnswerTmp->selectHotspotType($answerId) == 'noerror') {
5178
                                        if ($debug > 0) {
5179
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5180
                                        }
5181
                                        //type no error shouldn't be treated
5182
                                        $next = 1;
5183
                                        continue;
5184
                                    }
5185
                                    if ($debug > 0) {
5186
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5187
                                    }
5188
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5189
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5190
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5191
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5192
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5193
5194
                                    if ($overlap == false) {
5195
                                        //all good, no overlap
5196
                                        $next = 1;
5197
                                        continue;
5198
                                    } else {
5199
                                        if ($debug > 0) {
5200
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5201
                                        }
5202
                                        $organs_at_risk_hit++;
5203
                                        //show the feedback
5204
                                        $next = 0;
5205
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5206
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5207
5208
                                        $destination_items = explode('@@', $answerDestination);
5209
                                        $try_hotspot = $destination_items[1];
5210
                                        $lp_hotspot = $destination_items[2];
5211
                                        $select_question_hotspot = $destination_items[3];
5212
                                        $url_hotspot = $destination_items[4];
5213
                                    }
5214
                                }
5215
                            } else {
5216
                                // the first delineation feedback
5217
                                if ($debug > 0) {
5218
                                    error_log(__LINE__.' first', 0);
5219
                                }
5220
                            }
5221
                        } elseif (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
5222
                            echo '<tr>';
5223
                            echo Display::tag('td', $answerMatching[$answerId]);
5224
                            echo Display::tag(
5225
                                'td',
5226
                                "$user_answer / ".Display::tag(
5227
                                    'strong',
5228
                                    $answerMatching[$answerCorrect],
5229
                                    ['style' => 'color: #008000; font-weight: bold;']
5230
                                )
5231
                            );
5232
                            echo '</tr>';
5233
                        } elseif ($answerType == ANNOTATION) {
5234
                            ExerciseShowFunctions::displayAnnotationAnswer(
5235
                                $feedback_type,
5236
                                $exeId,
5237
                                $questionId,
5238
                                $questionScore,
5239
                                $results_disabled
5240
                            );
5241
                        }
5242
                    }
5243
                } else {
5244
                    if ($debug) {
5245
                        error_log('Showing questions $from '.$from);
5246
                    }
5247
5248
                    switch ($answerType) {
5249
                        case UNIQUE_ANSWER:
5250
                        case UNIQUE_ANSWER_IMAGE:
5251
                        case UNIQUE_ANSWER_NO_OPTION:
5252
                        case MULTIPLE_ANSWER:
5253
                        case GLOBAL_MULTIPLE_ANSWER:
5254
                        case MULTIPLE_ANSWER_COMBINATION:
5255
                        case READING_COMPREHENSION:
5256
                            if ($answerId == 1) {
5257
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5258
                                    $this,
5259
                                    $feedback_type,
5260
                                    $answerType,
5261
                                    $studentChoice,
5262
                                    $answer,
5263
                                    $answerComment,
5264
                                    $answerCorrect,
5265
                                    $exeId,
5266
                                    $questionId,
5267
                                    $answerId,
5268
                                    $results_disabled,
5269
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5270
                                    $this->export
5271
                                );
5272
                            } else {
5273
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5274
                                    $this,
5275
                                    $feedback_type,
5276
                                    $answerType,
5277
                                    $studentChoice,
5278
                                    $answer,
5279
                                    $answerComment,
5280
                                    $answerCorrect,
5281
                                    $exeId,
5282
                                    $questionId,
5283
                                    '',
5284
                                    $results_disabled,
5285
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5286
                                    $this->export
5287
                                );
5288
                            }
5289
                            break;
5290
                        case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
5291
                            if ($answerId == 1) {
5292
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5293
                                    $this,
5294
                                    $feedback_type,
5295
                                    $answerType,
5296
                                    $studentChoice,
5297
                                    $answer,
5298
                                    $answerComment,
5299
                                    $answerCorrect,
5300
                                    $exeId,
5301
                                    $questionId,
5302
                                    $answerId,
5303
                                    $results_disabled,
5304
                                    $showTotalScoreAndUserChoicesInLastAttempt
5305
                                );
5306
                            } else {
5307
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5308
                                    $this,
5309
                                    $feedback_type,
5310
                                    $answerType,
5311
                                    $studentChoice,
5312
                                    $answer,
5313
                                    $answerComment,
5314
                                    $answerCorrect,
5315
                                    $exeId,
5316
                                    $questionId,
5317
                                    '',
5318
                                    $results_disabled,
5319
                                    $showTotalScoreAndUserChoicesInLastAttempt
5320
                                );
5321
                            }
5322
                            break;
5323
                        case MULTIPLE_ANSWER_TRUE_FALSE:
5324
                            if ($answerId == 1) {
5325
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5326
                                    $this,
5327
                                    $feedback_type,
5328
                                    $answerType,
5329
                                    $studentChoice,
5330
                                    $answer,
5331
                                    $answerComment,
5332
                                    $answerCorrect,
5333
                                    $exeId,
5334
                                    $questionId,
5335
                                    $answerId,
5336
                                    $results_disabled,
5337
                                    $showTotalScoreAndUserChoicesInLastAttempt
5338
                                );
5339
                            } else {
5340
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5341
                                    $this,
5342
                                    $feedback_type,
5343
                                    $answerType,
5344
                                    $studentChoice,
5345
                                    $answer,
5346
                                    $answerComment,
5347
                                    $answerCorrect,
5348
                                    $exeId,
5349
                                    $questionId,
5350
                                    '',
5351
                                    $results_disabled,
5352
                                    $showTotalScoreAndUserChoicesInLastAttempt
5353
                                );
5354
                            }
5355
                            break;
5356
                        case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
5357
                            if ($answerId == 1) {
5358
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5359
                                    $this,
5360
                                    $feedback_type,
5361
                                    $studentChoice,
5362
                                    $studentChoiceDegree,
5363
                                    $answer,
5364
                                    $answerComment,
5365
                                    $answerCorrect,
5366
                                    $questionId,
5367
                                    $results_disabled
5368
                                );
5369
                            } else {
5370
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5371
                                    $this,
5372
                                    $feedback_type,
5373
                                    $studentChoice,
5374
                                    $studentChoiceDegree,
5375
                                    $answer,
5376
                                    $answerComment,
5377
                                    $answerCorrect,
5378
                                    $questionId,
5379
                                    $results_disabled
5380
                                );
5381
                            }
5382
                            break;
5383
                        case FILL_IN_BLANKS:
5384
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5385
                                $this,
5386
                                $feedback_type,
5387
                                $answer,
5388
                                $exeId,
5389
                                $questionId,
5390
                                $results_disabled,
5391
                                $str,
5392
                                $showTotalScoreAndUserChoicesInLastAttempt
5393
                            );
5394
                            break;
5395
                        case CALCULATED_ANSWER:
5396
                            ExerciseShowFunctions::display_calculated_answer(
5397
                                $this,
5398
                                $feedback_type,
5399
                                $answer,
5400
                                $exeId,
5401
                                $questionId,
5402
                                $results_disabled,
5403
                                '',
5404
                                $showTotalScoreAndUserChoicesInLastAttempt
5405
                            );
5406
                            break;
5407
                        case FREE_ANSWER:
5408
                            echo ExerciseShowFunctions::display_free_answer(
5409
                                $feedback_type,
5410
                                $choice,
5411
                                $exeId,
5412
                                $questionId,
5413
                                $questionScore,
5414
                                $results_disabled
5415
                            );
5416
                            break;
5417
                        case ORAL_EXPRESSION:
5418
                            echo '<tr>
5419
                                <td valign="top">'.
5420
                                ExerciseShowFunctions::display_oral_expression_answer(
5421
                                    $feedback_type,
5422
                                    $choice,
5423
                                    $exeId,
5424
                                    $questionId,
5425
                                    $objQuestionTmp->getFileUrl(),
5426
                                    $results_disabled,
5427
                                    $questionScore
5428
                                ).'</td>
5429
                                </tr>
5430
                                </table>';
5431
                            break;
5432
                        case HOT_SPOT:
5433
                            $correctAnswerId = 0;
5434
                            /** @var TrackEHotspot $hotspot */
5435
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5436
                                if ($hotspot->getHotspotAnswerId() == $foundAnswerId) {
5437
                                    break;
5438
                                }
5439
                            }
5440
5441
                            ExerciseShowFunctions::display_hotspot_answer(
5442
                                $this,
5443
                                $feedback_type,
5444
                                ++$correctAnswerId,
5445
                                $answer,
5446
                                $studentChoice,
5447
                                $answerComment,
5448
                                $results_disabled,
5449
                                $correctAnswerId,
5450
                                $showTotalScoreAndUserChoicesInLastAttempt
5451
                            );
5452
                            break;
5453
                        case HOT_SPOT_DELINEATION:
5454
                            $user_answer = $user_array;
5455
                            if ($next) {
5456
                                $user_answer = $user_array;
5457
                                // we compare only the delineation not the other points
5458
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5459
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5460
5461
                                // calculating the area
5462
                                $poly_user = convert_coordinates($user_answer, '/');
5463
                                $poly_answer = convert_coordinates($answer_question, '|');
5464
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5465
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5466
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5467
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5468
5469
                                $overlap = $poly_results['both'];
5470
                                $poly_answer_area = $poly_results['s1'];
5471
                                $poly_user_area = $poly_results['s2'];
5472
                                $missing = $poly_results['s1Only'];
5473
                                $excess = $poly_results['s2Only'];
5474
                                if ($debug > 0) {
5475
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5476
                                }
5477
                                if ($overlap < 1) {
5478
                                    //shortcut to avoid complicated calculations
5479
                                    $final_overlap = 0;
5480
                                    $final_missing = 100;
5481
                                    $final_excess = 100;
5482
                                } else {
5483
                                    // the final overlap is the percentage of the initial polygon
5484
                                    // that is overlapped by the user's polygon
5485
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5486
5487
                                    // the final missing area is the percentage of the initial polygon that
5488
                                    // is not overlapped by the user's polygon
5489
                                    $final_missing = 100 - $final_overlap;
5490
                                    // the final excess area is the percentage of the initial polygon's size that is
5491
                                    // covered by the user's polygon outside of the initial polygon
5492
                                    $final_excess = round((((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100);
5493
5494
                                    if ($debug > 1) {
5495
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap);
5496
                                        error_log(__LINE__.' - Final excess is '.$final_excess);
5497
                                        error_log(__LINE__.' - Final missing is '.$final_missing);
5498
                                    }
5499
                                }
5500
5501
                                // Checking the destination parameters parsing the "@@"
5502
                                $destination_items = explode('@@', $answerDestination);
5503
                                $threadhold_total = $destination_items[0];
5504
                                $threadhold_items = explode(';', $threadhold_total);
5505
                                $threadhold1 = $threadhold_items[0]; // overlap
5506
                                $threadhold2 = $threadhold_items[1]; // excess
5507
                                $threadhold3 = $threadhold_items[2]; //missing
5508
                                // if is delineation
5509
                                if ($answerId === 1) {
5510
                                    //setting colors
5511
                                    if ($final_overlap >= $threadhold1) {
5512
                                        $overlap_color = true;
5513
                                    }
5514
                                    if ($final_excess <= $threadhold2) {
5515
                                        $excess_color = true;
5516
                                    }
5517
                                    if ($final_missing <= $threadhold3) {
5518
                                        $missing_color = true;
5519
                                    }
5520
5521
                                    // if pass
5522
                                    if ($final_overlap >= $threadhold1 &&
5523
                                        $final_missing <= $threadhold3 &&
5524
                                        $final_excess <= $threadhold2
5525
                                    ) {
5526
                                        $next = 1; //go to the oars
5527
                                        $result_comment = get_lang('Acceptable');
5528
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5529
                                    } else {
5530
                                        $next = 0;
5531
                                        $result_comment = get_lang('Unacceptable');
5532
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5533
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5534
                                        //checking the destination parameters parsing the "@@"
5535
                                        $destination_items = explode('@@', $answerDestination);
5536
                                    }
5537
                                } elseif ($answerId > 1) {
5538
                                    if ($objAnswerTmp->selectHotspotType($answerId) === 'noerror') {
5539
                                        if ($debug > 0) {
5540
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5541
                                        }
5542
                                        //type no error shouldn't be treated
5543
                                        $next = 1;
5544
                                        break;
5545
                                    }
5546
                                    if ($debug > 0) {
5547
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5548
                                    }
5549
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5550
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5551
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5552
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5553
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5554
5555
                                    if ($overlap == false) {
5556
                                        //all good, no overlap
5557
                                        $next = 1;
5558
                                        break;
5559
                                    } else {
5560
                                        if ($debug > 0) {
5561
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5562
                                        }
5563
                                        $organs_at_risk_hit++;
5564
                                        //show the feedback
5565
                                        $next = 0;
5566
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5567
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5568
5569
                                        $destination_items = explode('@@', $answerDestination);
5570
                                        $try_hotspot = $destination_items[1];
5571
                                        $lp_hotspot = $destination_items[2];
5572
                                        $select_question_hotspot = $destination_items[3];
5573
                                        $url_hotspot = $destination_items[4];
5574
                                    }
5575
                                }
5576
                            }
5577
                            break;
5578
                        case HOT_SPOT_ORDER:
5579
                            ExerciseShowFunctions::display_hotspot_order_answer(
5580
                                $feedback_type,
5581
                                $answerId,
5582
                                $answer,
5583
                                $studentChoice,
5584
                                $answerComment
5585
                            );
5586
                            break;
5587
                        case DRAGGABLE:
5588
                        case MATCHING_DRAGGABLE:
5589
                        case MATCHING:
5590
                            echo '<tr>';
5591
                            echo Display::tag('td', $answerMatching[$answerId]);
5592
                            echo Display::tag(
5593
                                'td',
5594
                                "$user_answer / ".Display::tag(
5595
                                    'strong',
5596
                                    $answerMatching[$answerCorrect],
5597
                                    ['style' => 'color: #008000; font-weight: bold;']
5598
                                )
5599
                            );
5600
                            echo '</tr>';
5601
                            break;
5602
                        case ANNOTATION:
5603
                            ExerciseShowFunctions::displayAnnotationAnswer(
5604
                                $feedback_type,
5605
                                $exeId,
5606
                                $questionId,
5607
                                $questionScore,
5608
                                $results_disabled
5609
                            );
5610
                            break;
5611
                    }
5612
                }
5613
            }
5614
        } // end for that loops over all answers of the current question
5615
5616
        if ($debug) {
5617
            error_log('-- End answer loop --');
5618
        }
5619
5620
        $final_answer = true;
5621
5622
        foreach ($real_answers as $my_answer) {
5623
            if (!$my_answer) {
5624
                $final_answer = false;
5625
            }
5626
        }
5627
5628
        //we add the total score after dealing with the answers
5629
        if ($answerType == MULTIPLE_ANSWER_COMBINATION ||
5630
            $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE
5631
        ) {
5632
            if ($final_answer) {
5633
                //getting only the first score where we save the weight of all the question
5634
                $answerWeighting = $objAnswerTmp->selectWeighting(1);
5635
                $questionScore += $answerWeighting;
5636
            }
5637
        }
5638
5639
        $extra_data = [
5640
            'final_overlap' => $final_overlap,
5641
            'final_missing' => $final_missing,
5642
            'final_excess' => $final_excess,
5643
            'overlap_color' => $overlap_color,
5644
            'missing_color' => $missing_color,
5645
            'excess_color' => $excess_color,
5646
            'threadhold1' => $threadhold1,
5647
            'threadhold2' => $threadhold2,
5648
            'threadhold3' => $threadhold3,
5649
        ];
5650
5651
        if ($from === 'exercise_result') {
5652
            // if answer is hotspot. To the difference of exercise_show.php,
5653
            //  we use the results from the session (from_db=0)
5654
            // TODO Change this, because it is wrong to show the user
5655
            //  some results that haven't been stored in the database yet
5656
            if ($answerType == HOT_SPOT || $answerType == HOT_SPOT_ORDER || $answerType == HOT_SPOT_DELINEATION) {
5657
                if ($debug) {
5658
                    error_log('$from AND this is a hotspot kind of question ');
5659
                }
5660
                if ($answerType === HOT_SPOT_DELINEATION) {
5661
                    if ($showHotSpotDelineationTable) {
5662
                        if (!is_numeric($final_overlap)) {
5663
                            $final_overlap = 0;
5664
                        }
5665
                        if (!is_numeric($final_missing)) {
5666
                            $final_missing = 0;
5667
                        }
5668
                        if (!is_numeric($final_excess)) {
5669
                            $final_excess = 0;
5670
                        }
5671
5672
                        if ($final_overlap > 100) {
5673
                            $final_overlap = 100;
5674
                        }
5675
5676
                        $table_resume = '<table class="table table-hover table-striped data_table">
5677
                                <tr class="row_odd" >
5678
                                    <td></td>
5679
                                    <td ><b>'.get_lang('Requirements').'</b></td>
5680
                                    <td><b>'.get_lang('YourAnswer').'</b></td>
5681
                                </tr>
5682
                                <tr class="row_even">
5683
                                    <td><b>'.get_lang('Overlap').'</b></td>
5684
                                    <td>'.get_lang('Min').' '.$threadhold1.'</td>
5685
                                    <td class="text-right '.($overlap_color ? 'text-success' : 'text-danger').'">'
5686
                            .(($final_overlap < 0) ? 0 : intval($final_overlap)).'</td>
5687
                                </tr>
5688
                                <tr>
5689
                                    <td><b>'.get_lang('Excess').'</b></td>
5690
                                    <td>'.get_lang('Max').' '.$threadhold2.'</td>
5691
                                    <td class="text-right '.($excess_color ? 'text-success' : 'text-danger').'">'
5692
                            .(($final_excess < 0) ? 0 : intval($final_excess)).'</td>
5693
                                </tr>
5694
                                <tr class="row_even">
5695
                                    <td><b>'.get_lang('Missing').'</b></td>
5696
                                    <td>'.get_lang('Max').' '.$threadhold3.'</td>
5697
                                    <td class="text-right '.($missing_color ? 'text-success' : 'text-danger').'">'
5698
                            .(($final_missing < 0) ? 0 : intval($final_missing)).'</td>
5699
                                </tr>
5700
                            </table>';
5701
                        if ($next == 0) {
5702
                        } else {
5703
                            $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
5704
                            $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
5705
                        }
5706
5707
                        $message = '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>
5708
                                    <p style="text-align:center">';
5709
                        $message .= '<p>'.get_lang('YourDelineation').'</p>';
5710
                        $message .= $table_resume;
5711
                        $message .= '<br />'.get_lang('ResultIs').' '.$result_comment.'<br />';
5712
                        if ($organs_at_risk_hit > 0) {
5713
                            $message .= '<p><b>'.get_lang('OARHit').'</b></p>';
5714
                        }
5715
                        $message .= '<p>'.$comment.'</p>';
5716
                        echo $message;
5717
5718
                        $_SESSION['hotspot_delineation_result'][$this->selectId()][$questionId][0] = $message;
5719
                        $_SESSION['hotspot_delineation_result'][$this->selectId()][$questionId][1] = $_SESSION['exerciseResultCoordinates'][$questionId];
5720
                    } else {
5721
                        echo $hotspot_delineation_result[0];
5722
                    }
5723
5724
                    // Save the score attempts
5725
                    if (1) {
5726
                        //getting the answer 1 or 0 comes from exercise_submit_modal.php
5727
                        $final_answer = isset($hotspot_delineation_result[1]) ? $hotspot_delineation_result[1] : '';
5728
                        if ($final_answer == 0) {
5729
                            $questionScore = 0;
5730
                        }
5731
                        // we always insert the answer_id 1 = delineation
5732
                        Event::saveQuestionAttempt($questionScore, 1, $quesId, $exeId, 0);
5733
                        //in delineation mode, get the answer from $hotspot_delineation_result[1]
5734
                        $hotspotValue = isset($hotspot_delineation_result[1]) ? (int) $hotspot_delineation_result[1] === 1 ? 1 : 0 : 0;
5735
                        Event::saveExerciseAttemptHotspot(
5736
                            $exeId,
5737
                            $quesId,
5738
                            1,
5739
                            $hotspotValue,
5740
                            $exerciseResultCoordinates[$quesId]
5741
                        );
5742
                    } else {
5743
                        if ($final_answer == 0) {
5744
                            $questionScore = 0;
5745
                            $answer = 0;
5746
                            Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0);
5747
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5748
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5749
                                    Event::saveExerciseAttemptHotspot(
5750
                                        $exeId,
5751
                                        $quesId,
5752
                                        $idx,
5753
                                        0,
5754
                                        $val
5755
                                    );
5756
                                }
5757
                            }
5758
                        } else {
5759
                            Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0);
5760
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5761
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5762
                                    $hotspotValue = (int) $choice[$idx] === 1 ? 1 : 0;
5763
                                    Event::saveExerciseAttemptHotspot(
5764
                                        $exeId,
5765
                                        $quesId,
5766
                                        $idx,
5767
                                        $hotspotValue,
5768
                                        $val
5769
                                    );
5770
                                }
5771
                            }
5772
                        }
5773
                    }
5774
                }
5775
            }
5776
5777
            $relPath = api_get_path(WEB_CODE_PATH);
5778
5779
            if ($answerType == HOT_SPOT || $answerType == HOT_SPOT_ORDER) {
5780
                // We made an extra table for the answers
5781
                if ($show_result) {
5782
                    echo '</table></td></tr>';
5783
                    echo "
5784
                        <tr>
5785
                            <td>
5786
                                <p><em>".get_lang('HotSpot')."</em></p>
5787
                                <div id=\"hotspot-solution-$questionId\"></div>
5788
                                <script>
5789
                                    $(function() {
5790
                                        new HotspotQuestion({
5791
                                            questionId: $questionId,
5792
                                            exerciseId: {$this->id},
5793
                                            exeId: $exeId,
5794
                                            selector: '#hotspot-solution-$questionId',
5795
                                            for: 'solution',
5796
                                            relPath: '$relPath'
5797
                                        });
5798
                                    });
5799
                                </script>
5800
                            </td>
5801
                        </tr>
5802
                    ";
5803
                }
5804
            } elseif ($answerType == ANNOTATION) {
5805
                if ($show_result) {
5806
                    echo '
5807
                        <p><em>'.get_lang('Annotation').'</em></p>
5808
                        <div id="annotation-canvas-'.$questionId.'"></div>
5809
                        <script>
5810
                            AnnotationQuestion({
5811
                                questionId: parseInt('.$questionId.'),
5812
                                exerciseId: parseInt('.$exeId.'),
5813
                                relPath: \''.$relPath.'\',
5814
                                courseId: parseInt('.$course_id.')
5815
                            });
5816
                        </script>
5817
                    ';
5818
                }
5819
            }
5820
5821
            if ($show_result && $answerType != ANNOTATION) {
5822
                echo '</table>';
5823
            }
5824
        }
5825
        unset($objAnswerTmp);
5826
5827
        $totalWeighting += $questionWeighting;
5828
        // Store results directly in the database
5829
        // For all in one page exercises, the results will be
5830
        // stored by exercise_results.php (using the session)
5831
        if ($save_results) {
5832
            if ($debug) {
5833
                error_log("Save question results $save_results");
5834
                error_log('choice: ');
5835
                error_log(print_r($choice, 1));
5836
            }
5837
5838
            if (empty($choice)) {
5839
                $choice = 0;
5840
            }
5841
            // with certainty degree
5842
            if (empty($choiceDegreeCertainty)) {
5843
                $choiceDegreeCertainty = 0;
5844
            }
5845
            if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE ||
5846
                $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE ||
5847
                $answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY
5848
            ) {
5849
                if ($choice != 0) {
5850
                    $reply = array_keys($choice);
5851
                    $countReply = count($reply);
5852
                    for ($i = 0; $i < $countReply; $i++) {
5853
                        $chosenAnswer = $reply[$i];
5854
                        if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
5855
                            if ($choiceDegreeCertainty != 0) {
5856
                                $replyDegreeCertainty = array_keys($choiceDegreeCertainty);
5857
                                $answerDegreeCertainty = isset($replyDegreeCertainty[$i]) ? $replyDegreeCertainty[$i] : '';
5858
                                $answerValue = isset($choiceDegreeCertainty[$answerDegreeCertainty]) ? $choiceDegreeCertainty[$answerDegreeCertainty] : '';
5859
                                Event::saveQuestionAttempt(
5860
                                    $questionScore,
5861
                                    $chosenAnswer.':'.$choice[$chosenAnswer].':'.$answerValue,
5862
                                    $quesId,
5863
                                    $exeId,
5864
                                    $i,
5865
                                    $this->id,
5866
                                    $updateResults,
5867
                                    $questionDuration
5868
                                );
5869
                            }
5870
                        } else {
5871
                            Event::saveQuestionAttempt(
5872
                                $questionScore,
5873
                                $chosenAnswer.':'.$choice[$chosenAnswer],
5874
                                $quesId,
5875
                                $exeId,
5876
                                $i,
5877
                                $this->id,
5878
                                $updateResults,
5879
                                $questionDuration
5880
                            );
5881
                        }
5882
                        if ($debug) {
5883
                            error_log('result =>'.$questionScore.' '.$chosenAnswer.':'.$choice[$chosenAnswer]);
5884
                        }
5885
                    }
5886
                } else {
5887
                    Event::saveQuestionAttempt(
5888
                        $questionScore,
5889
                        0,
5890
                        $quesId,
5891
                        $exeId,
5892
                        0,
5893
                        $this->id,
5894
                        false,
5895
                        $questionDuration
5896
                    );
5897
                }
5898
            } elseif ($answerType == MULTIPLE_ANSWER || $answerType == GLOBAL_MULTIPLE_ANSWER) {
5899
                if ($choice != 0) {
5900
                    $reply = array_keys($choice);
5901
                    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...
5902
                        $ans = $reply[$i];
5903
                        Event::saveQuestionAttempt(
5904
                            $questionScore,
5905
                            $ans,
5906
                            $quesId,
5907
                            $exeId,
5908
                            $i,
5909
                            $this->id,
5910
                            false,
5911
                            $questionDuration
5912
                        );
5913
                    }
5914
                } else {
5915
                    Event::saveQuestionAttempt(
5916
                        $questionScore,
5917
                        0,
5918
                        $quesId,
5919
                        $exeId,
5920
                        0,
5921
                        $this->id,
5922
                        false,
5923
                        $questionDuration
5924
                    );
5925
                }
5926
            } elseif ($answerType == MULTIPLE_ANSWER_COMBINATION) {
5927
                if ($choice != 0) {
5928
                    $reply = array_keys($choice);
5929
                    for ($i = 0; $i < count($reply); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
5930
                        $ans = $reply[$i];
5931
                        Event::saveQuestionAttempt(
5932
                            $questionScore,
5933
                            $ans,
5934
                            $quesId,
5935
                            $exeId,
5936
                            $i,
5937
                            $this->id,
5938
                            false,
5939
                            $questionDuration
5940
                        );
5941
                    }
5942
                } else {
5943
                    Event::saveQuestionAttempt(
5944
                        $questionScore,
5945
                        0,
5946
                        $quesId,
5947
                        $exeId,
5948
                        0,
5949
                        $this->id,
5950
                        false,
5951
                        $questionDuration
5952
                    );
5953
                }
5954
            } elseif (in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE])) {
5955
                if (isset($matching)) {
5956
                    foreach ($matching as $j => $val) {
5957
                        Event::saveQuestionAttempt(
5958
                            $questionScore,
5959
                            $val,
5960
                            $quesId,
5961
                            $exeId,
5962
                            $j,
5963
                            $this->id,
5964
                            false,
5965
                            $questionDuration
5966
                        );
5967
                    }
5968
                }
5969
            } elseif ($answerType == FREE_ANSWER) {
5970
                $answer = $choice;
5971
                Event::saveQuestionAttempt(
5972
                    $questionScore,
5973
                    $answer,
5974
                    $quesId,
5975
                    $exeId,
5976
                    0,
5977
                    $this->id,
5978
                    false,
5979
                    $questionDuration
5980
                );
5981
            } elseif ($answerType == ORAL_EXPRESSION) {
5982
                $answer = $choice;
5983
                Event::saveQuestionAttempt(
5984
                    $questionScore,
5985
                    $answer,
5986
                    $quesId,
5987
                    $exeId,
5988
                    0,
5989
                    $this->id,
5990
                    false,
5991
                    $questionDuration,
5992
                    $objQuestionTmp->getAbsoluteFilePath()
5993
                );
5994
            } elseif (
5995
                in_array(
5996
                    $answerType,
5997
                    [UNIQUE_ANSWER, UNIQUE_ANSWER_IMAGE, UNIQUE_ANSWER_NO_OPTION, READING_COMPREHENSION]
5998
                )
5999
            ) {
6000
                $answer = $choice;
6001
                Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0, $this->id, false, $questionDuration);
6002
            } elseif ($answerType == HOT_SPOT || $answerType == ANNOTATION) {
6003
                $answer = [];
6004
                if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) {
6005
                    if ($debug) {
6006
                        error_log('Checking result coordinates');
6007
                    }
6008
                    Database::delete(
6009
                        Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
6010
                        [
6011
                            'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
6012
                                $exeId,
6013
                                $questionId,
6014
                                api_get_course_int_id(),
6015
                            ],
6016
                        ]
6017
                    );
6018
6019
                    foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
6020
                        $answer[] = $val;
6021
                        $hotspotValue = (int) $choice[$idx] === 1 ? 1 : 0;
6022
                        if ($debug) {
6023
                            error_log('Hotspot value: '.$hotspotValue);
6024
                        }
6025
                        Event::saveExerciseAttemptHotspot(
6026
                            $exeId,
6027
                            $quesId,
6028
                            $idx,
6029
                            $hotspotValue,
6030
                            $val,
6031
                            false,
6032
                            $this->id
6033
                        );
6034
                    }
6035
                } else {
6036
                    if ($debug) {
6037
                        error_log('Empty: exerciseResultCoordinates');
6038
                    }
6039
                }
6040
                Event::saveQuestionAttempt(
6041
                    $questionScore,
6042
                    implode('|', $answer),
6043
                    $quesId,
6044
                    $exeId,
6045
                    0,
6046
                    $this->id,
6047
                    false,
6048
                    $questionDuration
6049
                );
6050
            } else {
6051
                Event::saveQuestionAttempt(
6052
                    $questionScore,
6053
                    $answer,
6054
                    $quesId,
6055
                    $exeId,
6056
                    0,
6057
                    $this->id,
6058
                    false,
6059
                    $questionDuration
6060
                );
6061
            }
6062
        }
6063
6064
        if ($propagate_neg == 0 && $questionScore < 0) {
6065
            $questionScore = 0;
6066
        }
6067
6068
        if ($save_results) {
6069
            $statsTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6070
            $sql = "UPDATE $statsTable SET
6071
                        exe_result = exe_result + ".floatval($questionScore)."
6072
                    WHERE exe_id = $exeId";
6073
            Database::query($sql);
6074
        }
6075
6076
        $return = [
6077
            'score' => $questionScore,
6078
            'weight' => $questionWeighting,
6079
            'extra' => $extra_data,
6080
            'open_question' => $arrques,
6081
            'open_answer' => $arrans,
6082
            'answer_type' => $answerType,
6083
            'generated_oral_file' => $generatedFile,
6084
            'user_answered' => $userAnsweredQuestion,
6085
            'correct_answer_id' => $correctAnswerId,
6086
            'answer_destination' => $answerDestination,
6087
        ];
6088
6089
        return $return;
6090
    }
6091
6092
    /**
6093
     * Sends a notification when a user ends an examn.
6094
     *
6095
     * @param string $type                  'start' or 'end' of an exercise
6096
     * @param array  $question_list_answers
6097
     * @param string $origin
6098
     * @param int    $exe_id
6099
     * @param float  $score
6100
     * @param float  $weight
6101
     *
6102
     * @return bool
6103
     */
6104
    public function send_mail_notification_for_exam(
6105
        $type = 'end',
6106
        $question_list_answers,
6107
        $origin,
6108
        $exe_id,
6109
        $score = null,
6110
        $weight = null
6111
    ) {
6112
        $setting = api_get_course_setting('email_alert_manager_on_new_quiz');
6113
6114
        if (empty($setting) && empty($this->getNotifications())) {
6115
            return false;
6116
        }
6117
6118
        $settingFromExercise = $this->getNotifications();
6119
        if (!empty($settingFromExercise)) {
6120
            $setting = $settingFromExercise;
6121
        }
6122
6123
        // Email configuration settings
6124
        $courseCode = api_get_course_id();
6125
        $courseInfo = api_get_course_info($courseCode);
6126
6127
        if (empty($courseInfo)) {
6128
            return false;
6129
        }
6130
6131
        $sessionId = api_get_session_id();
6132
6133
        $sessionData = '';
6134
        if (!empty($sessionId)) {
6135
            $sessionInfo = api_get_session_info($sessionId);
6136
            if (!empty($sessionInfo)) {
6137
                $sessionData = '<tr>'
6138
                    .'<td>'.get_lang('SessionName').'</td>'
6139
                    .'<td>'.$sessionInfo['name'].'</td>'
6140
                    .'</tr>';
6141
            }
6142
        }
6143
6144
        $sendStart = false;
6145
        $sendEnd = false;
6146
        $sendEndOpenQuestion = false;
6147
        $sendEndOralQuestion = false;
6148
6149
        foreach ($setting as $option) {
6150
            switch ($option) {
6151
                case 0:
6152
                    return false;
6153
                    break;
6154
                case 1: // End
6155
                    if ($type === 'end') {
6156
                        $sendEnd = true;
6157
                    }
6158
                    break;
6159
                case 2: // start
6160
                    if ($type === 'start') {
6161
                        $sendStart = true;
6162
                    }
6163
                    break;
6164
                case 3: // end + open
6165
                    if ($type === 'end') {
6166
                        $sendEndOpenQuestion = true;
6167
                    }
6168
                    break;
6169
                case 4: // end + oral
6170
                    if ($type === 'end') {
6171
                        $sendEndOralQuestion = true;
6172
                    }
6173
                    break;
6174
            }
6175
        }
6176
6177
        $user_info = api_get_user_info(api_get_user_id());
6178
        $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_show.php?'.
6179
            api_get_cidreq(true, true, 'qualify').'&id='.$exe_id.'&action=qualify';
6180
6181
        if (!empty($sessionId)) {
6182
            $addGeneralCoach = true;
6183
            $setting = api_get_configuration_value('block_quiz_mail_notification_general_coach');
6184
            if ($setting === true) {
6185
                $addGeneralCoach = false;
6186
            }
6187
            $teachers = CourseManager::get_coach_list_from_course_code(
6188
                $courseCode,
6189
                $sessionId,
6190
                $addGeneralCoach
6191
            );
6192
        } else {
6193
            $teachers = CourseManager::get_teacher_list_from_course_code($courseCode);
6194
        }
6195
6196
        if ($sendEndOpenQuestion) {
6197
            $this->sendNotificationForOpenQuestions(
6198
                $question_list_answers,
6199
                $origin,
6200
                $user_info,
6201
                $url,
6202
                $teachers
6203
            );
6204
        }
6205
6206
        if ($sendEndOralQuestion) {
6207
            $this->sendNotificationForOralQuestions(
6208
                $question_list_answers,
6209
                $origin,
6210
                $exe_id,
6211
                $user_info,
6212
                $url,
6213
                $teachers
6214
            );
6215
        }
6216
6217
        if (!$sendEnd && !$sendStart) {
6218
            return false;
6219
        }
6220
6221
        $scoreLabel = '';
6222
        if ($sendEnd &&
6223
            api_get_configuration_value('send_score_in_exam_notification_mail_to_manager') == true
6224
        ) {
6225
            $notificationPercentage = api_get_configuration_value('send_notification_score_in_percentage');
6226
            $scoreLabel = ExerciseLib::show_score($score, $weight, $notificationPercentage, true);
6227
            $scoreLabel = "<tr>
6228
                            <td>".get_lang('Score')."</td>
6229
                            <td>&nbsp;$scoreLabel</td>
6230
                        </tr>";
6231
        }
6232
6233
        if ($sendEnd) {
6234
            $msg = get_lang('ExerciseAttempted').'<br /><br />';
6235
        } else {
6236
            $msg = get_lang('StudentStartExercise').'<br /><br />';
6237
        }
6238
6239
        $msg .= get_lang('AttemptDetails').' : <br /><br />
6240
                    <table>
6241
                        <tr>
6242
                            <td>'.get_lang('CourseName').'</td>
6243
                            <td>#course#</td>
6244
                        </tr>
6245
                        '.$sessionData.'
6246
                        <tr>
6247
                            <td>'.get_lang('Exercise').'</td>
6248
                            <td>&nbsp;#exercise#</td>
6249
                        </tr>
6250
                        <tr>
6251
                            <td>'.get_lang('StudentName').'</td>
6252
                            <td>&nbsp;#student_complete_name#</td>
6253
                        </tr>
6254
                        <tr>
6255
                            <td>'.get_lang('StudentEmail').'</td>
6256
                            <td>&nbsp;#email#</td>
6257
                        </tr>
6258
                        '.$scoreLabel.'
6259
                    </table>';
6260
6261
        $variables = [
6262
            '#email#' => $user_info['email'],
6263
            '#exercise#' => $this->exercise,
6264
            '#student_complete_name#' => $user_info['complete_name'],
6265
            '#course#' => Display::url(
6266
                $courseInfo['title'],
6267
                $courseInfo['course_public_url'].'?id_session='.$sessionId
6268
            ),
6269
        ];
6270
6271
        if ($sendEnd) {
6272
            $msg .= '<br /><a href="#url#">'.get_lang('ClickToCommentAndGiveFeedback').'</a>';
6273
            $variables['#url#'] = $url;
6274
        }
6275
6276
        $content = str_replace(array_keys($variables), array_values($variables), $msg);
6277
6278
        if ($sendEnd) {
6279
            $subject = get_lang('ExerciseAttempted');
6280
        } else {
6281
            $subject = get_lang('StudentStartExercise');
6282
        }
6283
6284
        if (!empty($teachers)) {
6285
            foreach ($teachers as $user_id => $teacher_data) {
6286
                MessageManager::send_message_simple(
6287
                    $user_id,
6288
                    $subject,
6289
                    $content
6290
                );
6291
            }
6292
        }
6293
    }
6294
6295
    /**
6296
     * @param array $user_data         result of api_get_user_info()
6297
     * @param array $trackExerciseInfo result of get_stat_track_exercise_info
6298
     * @param bool  $saveUserResult
6299
     * @param bool  $allowSignature
6300
     * @param bool  $allowExportPdf
6301
     *
6302
     * @return string
6303
     */
6304
    public function showExerciseResultHeader(
6305
        $user_data,
6306
        $trackExerciseInfo,
6307
        $saveUserResult,
6308
        $allowSignature = false,
6309
        $allowExportPdf = false
6310
    ) {
6311
        if (api_get_configuration_value('hide_user_info_in_quiz_result')) {
6312
            return '';
6313
        }
6314
6315
        $start_date = null;
6316
        if (isset($trackExerciseInfo['start_date'])) {
6317
            $start_date = api_convert_and_format_date($trackExerciseInfo['start_date']);
6318
        }
6319
        $duration = isset($trackExerciseInfo['duration_formatted']) ? $trackExerciseInfo['duration_formatted'] : null;
6320
        $ip = isset($trackExerciseInfo['user_ip']) ? $trackExerciseInfo['user_ip'] : null;
6321
6322
        if (!empty($user_data)) {
6323
            $userFullName = $user_data['complete_name'];
6324
            if (api_is_teacher() || api_is_platform_admin(true, true)) {
6325
                $userFullName = '<a href="'.$user_data['profile_url'].'" title="'.get_lang('GoToStudentDetails').'">'.
6326
                    $user_data['complete_name'].'</a>';
6327
            }
6328
6329
            $data = [
6330
                'name_url' => $userFullName,
6331
                'complete_name' => $user_data['complete_name'],
6332
                'username' => $user_data['username'],
6333
                'avatar' => $user_data['avatar_medium'],
6334
                'url' => $user_data['profile_url'],
6335
            ];
6336
6337
            if (!empty($user_data['official_code'])) {
6338
                $data['code'] = $user_data['official_code'];
6339
            }
6340
        }
6341
        // Description can be very long and is generally meant to explain
6342
        //   rules *before* the exam. Leaving here to make display easier if
6343
        //   necessary
6344
        /*
6345
        if (!empty($this->description)) {
6346
            $array[] = array('title' => get_lang("Description"), 'content' => $this->description);
6347
        }
6348
        */
6349
        if (!empty($start_date)) {
6350
            $data['start_date'] = $start_date;
6351
        }
6352
6353
        if (!empty($duration)) {
6354
            $data['duration'] = $duration;
6355
        }
6356
6357
        if (!empty($ip)) {
6358
            $data['ip'] = $ip;
6359
        }
6360
6361
        if (api_get_configuration_value('save_titles_as_html')) {
6362
            $data['title'] = $this->get_formated_title().get_lang('Result');
6363
        } else {
6364
            $data['title'] = PHP_EOL.$this->exercise.' : '.get_lang('Result');
6365
        }
6366
6367
        $questionsCount = count(explode(',', $trackExerciseInfo['data_tracking']));
6368
        $savedAnswersCount = $this->countUserAnswersSavedInExercise($trackExerciseInfo['exe_id']);
6369
6370
        $data['number_of_answers'] = $questionsCount;
6371
        $data['number_of_answers_saved'] = $savedAnswersCount;
6372
        $exeId = $trackExerciseInfo['exe_id'];
6373
6374
        if (false !== api_get_configuration_value('quiz_confirm_saved_answers')) {
6375
            $em = Database::getManager();
6376
6377
            if ($saveUserResult) {
6378
                $trackConfirmation = new TrackEExerciseConfirmation();
6379
                $trackConfirmation
6380
                    ->setUserId($trackExerciseInfo['exe_user_id'])
6381
                    ->setQuizId($trackExerciseInfo['exe_exo_id'])
6382
                    ->setAttemptId($trackExerciseInfo['exe_id'])
6383
                    ->setQuestionsCount($questionsCount)
6384
                    ->setSavedAnswersCount($savedAnswersCount)
6385
                    ->setCourseId($trackExerciseInfo['c_id'])
6386
                    ->setSessionId($trackExerciseInfo['session_id'])
6387
                    ->setCreatedAt(api_get_utc_datetime(null, false, true));
6388
6389
                $em->persist($trackConfirmation);
6390
                $em->flush();
6391
            } else {
6392
                $trackConfirmation = $em
6393
                    ->getRepository('ChamiloCoreBundle:TrackEExerciseConfirmation')
6394
                    ->findOneBy(
6395
                        [
6396
                            'attemptId' => $trackExerciseInfo['exe_id'],
6397
                            'quizId' => $trackExerciseInfo['exe_exo_id'],
6398
                            'courseId' => $trackExerciseInfo['c_id'],
6399
                            'sessionId' => $trackExerciseInfo['session_id'],
6400
                        ]
6401
                    );
6402
            }
6403
6404
            $data['track_confirmation'] = $trackConfirmation;
6405
        }
6406
6407
        $signature = '';
6408
        if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($this)) {
6409
            $signature = ExerciseSignaturePlugin::getSignature($trackExerciseInfo['exe_user_id'], $trackExerciseInfo);
6410
        }
6411
6412
        $tpl = new Template(null, false, false, false, false, false, false);
6413
        $tpl->assign('data', $data);
6414
        $tpl->assign('allow_signature', $allowSignature);
6415
        $tpl->assign('signature', $signature);
6416
        $tpl->assign('allow_export_pdf', $allowExportPdf);
6417
        $tpl->assign('export_url', api_get_path(WEB_CODE_PATH).'exercise/result.php?action=export&id='.$exeId.'&'.api_get_cidreq());
6418
6419
        $layoutTemplate = $tpl->get_template('exercise/partials/result_exercise.tpl');
6420
6421
        return $tpl->fetch($layoutTemplate);
6422
    }
6423
6424
    /**
6425
     * Returns the exercise result.
6426
     *
6427
     * @param 	int		attempt id
6428
     *
6429
     * @return array
6430
     */
6431
    public function get_exercise_result($exe_id)
6432
    {
6433
        $result = [];
6434
        $track_exercise_info = ExerciseLib::get_exercise_track_exercise_info($exe_id);
6435
6436
        if (!empty($track_exercise_info)) {
6437
            $totalScore = 0;
6438
            $objExercise = new Exercise();
6439
            $objExercise->read($track_exercise_info['exe_exo_id']);
6440
            if (!empty($track_exercise_info['data_tracking'])) {
6441
                $question_list = explode(',', $track_exercise_info['data_tracking']);
6442
            }
6443
            foreach ($question_list as $questionId) {
6444
                $question_result = $objExercise->manage_answer(
6445
                    $exe_id,
6446
                    $questionId,
6447
                    '',
6448
                    'exercise_show',
6449
                    [],
6450
                    false,
6451
                    true,
6452
                    false,
6453
                    $objExercise->selectPropagateNeg()
6454
                );
6455
                $totalScore += $question_result['score'];
6456
            }
6457
6458
            if ($objExercise->selectPropagateNeg() == 0 && $totalScore < 0) {
6459
                $totalScore = 0;
6460
            }
6461
            $result = [
6462
                'score' => $totalScore,
6463
                'weight' => $track_exercise_info['exe_weighting'],
6464
            ];
6465
        }
6466
6467
        return $result;
6468
    }
6469
6470
    /**
6471
     * Checks if the exercise is visible due a lot of conditions
6472
     * visibility, time limits, student attempts
6473
     * Return associative array
6474
     * value : true if exercise visible
6475
     * message : HTML formatted message
6476
     * rawMessage : text message.
6477
     *
6478
     * @param int  $lpId
6479
     * @param int  $lpItemId
6480
     * @param int  $lpItemViewId
6481
     * @param bool $filterByAdmin
6482
     *
6483
     * @return array
6484
     */
6485
    public function is_visible(
6486
        $lpId = 0,
6487
        $lpItemId = 0,
6488
        $lpItemViewId = 0,
6489
        $filterByAdmin = true
6490
    ) {
6491
        // 1. By default the exercise is visible
6492
        $isVisible = true;
6493
        $message = null;
6494
6495
        // 1.1 Admins, teachers and tutors can access to the exercise
6496
        if ($filterByAdmin) {
6497
            if (api_is_platform_admin() || api_is_course_admin() || api_is_course_tutor()) {
6498
                return ['value' => true, 'message' => ''];
6499
            }
6500
        }
6501
6502
        // Deleted exercise.
6503
        if ($this->active == -1) {
6504
            return [
6505
                'value' => false,
6506
                'message' => Display::return_message(
6507
                    get_lang('ExerciseNotFound'),
6508
                    'warning',
6509
                    false
6510
                ),
6511
                'rawMessage' => get_lang('ExerciseNotFound'),
6512
            ];
6513
        }
6514
6515
        // Checking visibility in the item_property table.
6516
        $visibility = api_get_item_visibility(
6517
            api_get_course_info(),
6518
            TOOL_QUIZ,
6519
            $this->id,
6520
            api_get_session_id()
6521
        );
6522
6523
        if ($visibility == 0 || $visibility == 2) {
6524
            $this->active = 0;
6525
        }
6526
6527
        // 2. If the exercise is not active.
6528
        if (empty($lpId)) {
6529
            // 2.1 LP is OFF
6530
            if ($this->active == 0) {
6531
                return [
6532
                    'value' => false,
6533
                    'message' => Display::return_message(
6534
                        get_lang('ExerciseNotFound'),
6535
                        'warning',
6536
                        false
6537
                    ),
6538
                    'rawMessage' => get_lang('ExerciseNotFound'),
6539
                ];
6540
            }
6541
        } else {
6542
            // 2.1 LP is loaded
6543
            if ($this->active == 0 &&
6544
                !learnpath::is_lp_visible_for_student($lpId, api_get_user_id())
6545
            ) {
6546
                return [
6547
                    'value' => false,
6548
                    'message' => Display::return_message(
6549
                        get_lang('ExerciseNotFound'),
6550
                        'warning',
6551
                        false
6552
                    ),
6553
                    'rawMessage' => get_lang('ExerciseNotFound'),
6554
                ];
6555
            }
6556
        }
6557
6558
        // 3. We check if the time limits are on
6559
        $limitTimeExists = false;
6560
        if (!empty($this->start_time) || !empty($this->end_time)) {
6561
            $limitTimeExists = true;
6562
        }
6563
6564
        if ($limitTimeExists) {
6565
            $timeNow = time();
6566
            $existsStartDate = false;
6567
            $nowIsAfterStartDate = true;
6568
            $existsEndDate = false;
6569
            $nowIsBeforeEndDate = true;
6570
6571
            if (!empty($this->start_time)) {
6572
                $existsStartDate = true;
6573
            }
6574
6575
            if (!empty($this->end_time)) {
6576
                $existsEndDate = true;
6577
            }
6578
6579
            // check if we are before-or-after end-or-start date
6580
            if ($existsStartDate && $timeNow < api_strtotime($this->start_time, 'UTC')) {
6581
                $nowIsAfterStartDate = false;
6582
            }
6583
6584
            if ($existsEndDate & $timeNow >= api_strtotime($this->end_time, 'UTC')) {
6585
                $nowIsBeforeEndDate = false;
6586
            }
6587
6588
            // lets check all cases
6589
            if ($existsStartDate && !$existsEndDate) {
6590
                // exists start date and dont exists end date
6591
                if ($nowIsAfterStartDate) {
6592
                    // after start date, no end date
6593
                    $isVisible = true;
6594
                    $message = sprintf(
6595
                        get_lang('ExerciseAvailableSinceX'),
6596
                        api_convert_and_format_date($this->start_time)
6597
                    );
6598
                } else {
6599
                    // before start date, no end date
6600
                    $isVisible = false;
6601
                    $message = sprintf(
6602
                        get_lang('ExerciseAvailableFromX'),
6603
                        api_convert_and_format_date($this->start_time)
6604
                    );
6605
                }
6606
            } elseif (!$existsStartDate && $existsEndDate) {
6607
                // doesnt exist start date, exists end date
6608
                if ($nowIsBeforeEndDate) {
6609
                    // before end date, no start date
6610
                    $isVisible = true;
6611
                    $message = sprintf(
6612
                        get_lang('ExerciseAvailableUntilX'),
6613
                        api_convert_and_format_date($this->end_time)
6614
                    );
6615
                } else {
6616
                    // after end date, no start date
6617
                    $isVisible = false;
6618
                    $message = sprintf(
6619
                        get_lang('ExerciseAvailableUntilX'),
6620
                        api_convert_and_format_date($this->end_time)
6621
                    );
6622
                }
6623
            } elseif ($existsStartDate && $existsEndDate) {
6624
                // exists start date and end date
6625
                if ($nowIsAfterStartDate) {
6626
                    if ($nowIsBeforeEndDate) {
6627
                        // after start date and before end date
6628
                        $isVisible = true;
6629
                        $message = sprintf(
6630
                            get_lang('ExerciseIsActivatedFromXToY'),
6631
                            api_convert_and_format_date($this->start_time),
6632
                            api_convert_and_format_date($this->end_time)
6633
                        );
6634
                    } else {
6635
                        // after start date and after end date
6636
                        $isVisible = false;
6637
                        $message = sprintf(
6638
                            get_lang('ExerciseWasActivatedFromXToY'),
6639
                            api_convert_and_format_date($this->start_time),
6640
                            api_convert_and_format_date($this->end_time)
6641
                        );
6642
                    }
6643
                } else {
6644
                    if ($nowIsBeforeEndDate) {
6645
                        // before start date and before end date
6646
                        $isVisible = false;
6647
                        $message = sprintf(
6648
                            get_lang('ExerciseWillBeActivatedFromXToY'),
6649
                            api_convert_and_format_date($this->start_time),
6650
                            api_convert_and_format_date($this->end_time)
6651
                        );
6652
                    }
6653
                    // case before start date and after end date is impossible
6654
                }
6655
            } elseif (!$existsStartDate && !$existsEndDate) {
6656
                // doesnt exist start date nor end date
6657
                $isVisible = true;
6658
                $message = '';
6659
            }
6660
        }
6661
6662
        // 4. We check if the student have attempts
6663
        if ($isVisible) {
6664
            $exerciseAttempts = $this->selectAttempts();
6665
6666
            if ($exerciseAttempts > 0) {
6667
                $attemptCount = Event::get_attempt_count_not_finished(
6668
                    api_get_user_id(),
6669
                    $this->id,
6670
                    $lpId,
6671
                    $lpItemId,
6672
                    $lpItemViewId
6673
                );
6674
6675
                if ($attemptCount >= $exerciseAttempts) {
6676
                    $message = sprintf(
6677
                        get_lang('ReachedMaxAttempts'),
6678
                        $this->name,
6679
                        $exerciseAttempts
6680
                    );
6681
                    $isVisible = false;
6682
                }
6683
            }
6684
        }
6685
6686
        $rawMessage = '';
6687
        if (!empty($message)) {
6688
            $rawMessage = $message;
6689
            $message = Display::return_message($message, 'warning', false);
6690
        }
6691
6692
        return [
6693
            'value' => $isVisible,
6694
            'message' => $message,
6695
            'rawMessage' => $rawMessage,
6696
        ];
6697
    }
6698
6699
    /**
6700
     * @return bool
6701
     */
6702
    public function added_in_lp()
6703
    {
6704
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
6705
        $sql = "SELECT max_score FROM $TBL_LP_ITEM
6706
                WHERE
6707
                    c_id = {$this->course_id} AND
6708
                    item_type = '".TOOL_QUIZ."' AND
6709
                    path = '{$this->id}'";
6710
        $result = Database::query($sql);
6711
        if (Database::num_rows($result) > 0) {
6712
            return true;
6713
        }
6714
6715
        return false;
6716
    }
6717
6718
    /**
6719
     * Returns an array with this form.
6720
     *
6721
     * @example
6722
     * <code>
6723
     * array (size=3)
6724
     * 999 =>
6725
     * array (size=3)
6726
     * 0 => int 3422
6727
     * 1 => int 3423
6728
     * 2 => int 3424
6729
     * 100 =>
6730
     * array (size=2)
6731
     * 0 => int 3469
6732
     * 1 => int 3470
6733
     * 101 =>
6734
     * array (size=1)
6735
     * 0 => int 3482
6736
     * </code>
6737
     * The array inside the key 999 means the question list that belongs to the media id = 999,
6738
     * this case is special because 999 means "no media".
6739
     *
6740
     * @return array
6741
     */
6742
    public function getMediaList()
6743
    {
6744
        return $this->mediaList;
6745
    }
6746
6747
    /**
6748
     * Is media question activated?
6749
     *
6750
     * @return bool
6751
     */
6752
    public function mediaIsActivated()
6753
    {
6754
        $mediaQuestions = $this->getMediaList();
6755
        $active = false;
6756
        if (isset($mediaQuestions) && !empty($mediaQuestions)) {
6757
            $media_count = count($mediaQuestions);
6758
            if ($media_count > 1) {
6759
                return true;
6760
            } elseif ($media_count == 1) {
6761
                if (isset($mediaQuestions[999])) {
6762
                    return false;
6763
                } else {
6764
                    return true;
6765
                }
6766
            }
6767
        }
6768
6769
        return $active;
6770
    }
6771
6772
    /**
6773
     * Gets question list from the exercise.
6774
     *
6775
     * @return array
6776
     */
6777
    public function getQuestionList()
6778
    {
6779
        return $this->questionList;
6780
    }
6781
6782
    /**
6783
     * Question list with medias compressed like this.
6784
     *
6785
     * @example
6786
     * <code>
6787
     * array(
6788
     *      question_id_1,
6789
     *      question_id_2,
6790
     *      media_id, <- this media id contains question ids
6791
     *      question_id_3,
6792
     * )
6793
     * </code>
6794
     *
6795
     * @return array
6796
     */
6797
    public function getQuestionListWithMediasCompressed()
6798
    {
6799
        return $this->questionList;
6800
    }
6801
6802
    /**
6803
     * Question list with medias uncompressed like this.
6804
     *
6805
     * @example
6806
     * <code>
6807
     * array(
6808
     *      question_id,
6809
     *      question_id,
6810
     *      question_id, <- belongs to a media id
6811
     *      question_id, <- belongs to a media id
6812
     *      question_id,
6813
     * )
6814
     * </code>
6815
     *
6816
     * @return array
6817
     */
6818
    public function getQuestionListWithMediasUncompressed()
6819
    {
6820
        return $this->questionListUncompressed;
6821
    }
6822
6823
    /**
6824
     * Sets the question list when the exercise->read() is executed.
6825
     *
6826
     * @param bool $adminView Whether to view the set the list of *all* questions or just the normal student view
6827
     */
6828
    public function setQuestionList($adminView = false)
6829
    {
6830
        // Getting question list.
6831
        $questionList = $this->selectQuestionList(true, $adminView);
6832
        $this->setMediaList($questionList);
6833
        $this->questionList = $this->transformQuestionListWithMedias($questionList, false);
6834
        $this->questionListUncompressed = $this->transformQuestionListWithMedias(
6835
            $questionList,
6836
            true
6837
        );
6838
    }
6839
6840
    /**
6841
     * @params array question list
6842
     * @params bool expand or not question list (true show all questions,
6843
     * false show media question id instead of the question ids)
6844
     */
6845
    public function transformQuestionListWithMedias(
6846
        $question_list,
6847
        $expand_media_questions = false
6848
    ) {
6849
        $new_question_list = [];
6850
        if (!empty($question_list)) {
6851
            $media_questions = $this->getMediaList();
6852
            $media_active = $this->mediaIsActivated($media_questions);
6853
6854
            if ($media_active) {
6855
                $counter = 1;
6856
                foreach ($question_list as $question_id) {
6857
                    $add_question = true;
6858
                    foreach ($media_questions as $media_id => $question_list_in_media) {
6859
                        if ($media_id != 999 && in_array($question_id, $question_list_in_media)) {
6860
                            $add_question = false;
6861
                            if (!in_array($media_id, $new_question_list)) {
6862
                                $new_question_list[$counter] = $media_id;
6863
                                $counter++;
6864
                            }
6865
                            break;
6866
                        }
6867
                    }
6868
                    if ($add_question) {
6869
                        $new_question_list[$counter] = $question_id;
6870
                        $counter++;
6871
                    }
6872
                }
6873
                if ($expand_media_questions) {
6874
                    $media_key_list = array_keys($media_questions);
6875
                    foreach ($new_question_list as &$question_id) {
6876
                        if (in_array($question_id, $media_key_list)) {
6877
                            $question_id = $media_questions[$question_id];
6878
                        }
6879
                    }
6880
                    $new_question_list = array_flatten($new_question_list);
6881
                }
6882
            } else {
6883
                $new_question_list = $question_list;
6884
            }
6885
        }
6886
6887
        return $new_question_list;
6888
    }
6889
6890
    /**
6891
     * Get question list depend on the random settings.
6892
     *
6893
     * @return array
6894
     */
6895
    public function get_validated_question_list()
6896
    {
6897
        $isRandomByCategory = $this->isRandomByCat();
6898
        if ($isRandomByCategory == 0) {
6899
            if ($this->isRandom()) {
6900
                return $this->getRandomList();
6901
            }
6902
6903
            return $this->selectQuestionList();
6904
        }
6905
6906
        if ($this->isRandom()) {
6907
            // USE question categories
6908
            // get questions by category for this exercise
6909
            // we have to choice $objExercise->random question in each array values of $categoryQuestions
6910
            // key of $categoryQuestions are the categopy id (0 for not in a category)
6911
            // value is the array of question id of this category
6912
            $questionList = [];
6913
            $categoryQuestions = TestCategory::getQuestionsByCat($this->id);
6914
            $isRandomByCategory = $this->getRandomByCategory();
6915
            // We sort categories based on the term between [] in the head
6916
            // of the category's description
6917
            /* examples of categories :
6918
             * [biologie] Maitriser les mecanismes de base de la genetique
6919
             * [biologie] Relier les moyens de depenses et les agents infectieux
6920
             * [biologie] Savoir ou est produite l'enrgie dans les cellules et sous quelle forme
6921
             * [chimie] Classer les molles suivant leur pouvoir oxydant ou reacteur
6922
             * [chimie] Connaître la denition de la theoie acide/base selon Brönsted
6923
             * [chimie] Connaître les charges des particules
6924
             * We want that in the order of the groups defined by the term
6925
             * between brackets at the beginning of the category title
6926
            */
6927
            // If test option is Grouped By Categories
6928
            if ($isRandomByCategory == 2) {
6929
                $categoryQuestions = TestCategory::sortTabByBracketLabel($categoryQuestions);
6930
            }
6931
            foreach ($categoryQuestions as $question) {
6932
                $number_of_random_question = $this->random;
6933
                if ($this->random == -1) {
6934
                    $number_of_random_question = count($this->questionList);
6935
                }
6936
                $questionList = array_merge(
6937
                    $questionList,
6938
                    TestCategory::getNElementsFromArray(
6939
                        $question,
6940
                        $number_of_random_question
6941
                    )
6942
                );
6943
            }
6944
            // shuffle the question list if test is not grouped by categories
6945
            if ($isRandomByCategory == 1) {
6946
                shuffle($questionList); // or not
6947
            }
6948
6949
            return $questionList;
6950
        }
6951
6952
        // Problem, random by category has been selected and
6953
        // we have no $this->isRandom number of question selected
6954
        // Should not happened
6955
6956
        return [];
6957
    }
6958
6959
    public function get_question_list($expand_media_questions = false)
6960
    {
6961
        $question_list = $this->get_validated_question_list();
6962
        $question_list = $this->transform_question_list_with_medias($question_list, $expand_media_questions);
6963
6964
        return $question_list;
6965
    }
6966
6967
    public function transform_question_list_with_medias($question_list, $expand_media_questions = false)
6968
    {
6969
        $new_question_list = [];
6970
        if (!empty($question_list)) {
6971
            $media_questions = $this->getMediaList();
6972
            $media_active = $this->mediaIsActivated($media_questions);
6973
6974
            if ($media_active) {
6975
                $counter = 1;
6976
                foreach ($question_list as $question_id) {
6977
                    $add_question = true;
6978
                    foreach ($media_questions as $media_id => $question_list_in_media) {
6979
                        if ($media_id != 999 && in_array($question_id, $question_list_in_media)) {
6980
                            $add_question = false;
6981
                            if (!in_array($media_id, $new_question_list)) {
6982
                                $new_question_list[$counter] = $media_id;
6983
                                $counter++;
6984
                            }
6985
                            break;
6986
                        }
6987
                    }
6988
                    if ($add_question) {
6989
                        $new_question_list[$counter] = $question_id;
6990
                        $counter++;
6991
                    }
6992
                }
6993
                if ($expand_media_questions) {
6994
                    $media_key_list = array_keys($media_questions);
6995
                    foreach ($new_question_list as &$question_id) {
6996
                        if (in_array($question_id, $media_key_list)) {
6997
                            $question_id = $media_questions[$question_id];
6998
                        }
6999
                    }
7000
                    $new_question_list = array_flatten($new_question_list);
7001
                }
7002
            } else {
7003
                $new_question_list = $question_list;
7004
            }
7005
        }
7006
7007
        return $new_question_list;
7008
    }
7009
7010
    /**
7011
     * @param int $exe_id
7012
     *
7013
     * @return array
7014
     */
7015
    public function get_stat_track_exercise_info_by_exe_id($exe_id)
7016
    {
7017
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7018
        $exe_id = (int) $exe_id;
7019
        $sql_track = "SELECT * FROM $table WHERE exe_id = $exe_id ";
7020
        $result = Database::query($sql_track);
7021
        $new_array = [];
7022
        if (Database::num_rows($result) > 0) {
7023
            $new_array = Database::fetch_array($result, 'ASSOC');
7024
            $start_date = api_get_utc_datetime($new_array['start_date'], true);
7025
            $end_date = api_get_utc_datetime($new_array['exe_date'], true);
7026
            $new_array['duration_formatted'] = '';
7027
            if (!empty($new_array['exe_duration']) && !empty($start_date) && !empty($end_date)) {
7028
                $time = api_format_time($new_array['exe_duration'], 'js');
7029
                $new_array['duration_formatted'] = $time;
7030
            }
7031
        }
7032
7033
        return $new_array;
7034
    }
7035
7036
    /**
7037
     * @param int $exeId
7038
     *
7039
     * @return bool
7040
     */
7041
    public function removeAllQuestionToRemind($exeId)
7042
    {
7043
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7044
        $exeId = (int) $exeId;
7045
        if (empty($exeId)) {
7046
            return false;
7047
        }
7048
        $sql = "UPDATE $table
7049
                SET questions_to_check = ''
7050
                WHERE exe_id = $exeId ";
7051
        Database::query($sql);
7052
7053
        return true;
7054
    }
7055
7056
    /**
7057
     * @param int   $exeId
7058
     * @param array $questionList
7059
     *
7060
     * @return bool
7061
     */
7062
    public function addAllQuestionToRemind($exeId, $questionList = [])
7063
    {
7064
        $exeId = (int) $exeId;
7065
        if (empty($questionList)) {
7066
            return false;
7067
        }
7068
7069
        $questionListToString = implode(',', $questionList);
7070
        $questionListToString = Database::escape_string($questionListToString);
7071
7072
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7073
        $sql = "UPDATE $table
7074
                SET questions_to_check = '$questionListToString'
7075
                WHERE exe_id = $exeId";
7076
        Database::query($sql);
7077
7078
        return true;
7079
    }
7080
7081
    /**
7082
     * @param int    $exeId
7083
     * @param int    $questionId
7084
     * @param string $action
7085
     */
7086
    public function editQuestionToRemind($exeId, $questionId, $action = 'add')
7087
    {
7088
        $exercise_info = self::get_stat_track_exercise_info_by_exe_id($exeId);
7089
        $questionId = (int) $questionId;
7090
        $exeId = (int) $exeId;
7091
7092
        if ($exercise_info) {
7093
            $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7094
            if (empty($exercise_info['questions_to_check'])) {
7095
                if ($action === 'add') {
7096
                    $sql = "UPDATE $track_exercises
7097
                            SET questions_to_check = '$questionId'
7098
                            WHERE exe_id = $exeId ";
7099
                    Database::query($sql);
7100
                }
7101
            } else {
7102
                $remind_list = explode(',', $exercise_info['questions_to_check']);
7103
                $remind_list_string = '';
7104
                if ($action === 'add') {
7105
                    if (!in_array($questionId, $remind_list)) {
7106
                        $newRemindList = [];
7107
                        $remind_list[] = $questionId;
7108
                        $questionListInSession = Session::read('questionList');
7109
                        if (!empty($questionListInSession)) {
7110
                            foreach ($questionListInSession as $originalQuestionId) {
7111
                                if (in_array($originalQuestionId, $remind_list)) {
7112
                                    $newRemindList[] = $originalQuestionId;
7113
                                }
7114
                            }
7115
                        }
7116
                        $remind_list_string = implode(',', $newRemindList);
7117
                    }
7118
                } elseif ($action === 'delete') {
7119
                    if (!empty($remind_list)) {
7120
                        if (in_array($questionId, $remind_list)) {
7121
                            $remind_list = array_flip($remind_list);
7122
                            unset($remind_list[$questionId]);
7123
                            $remind_list = array_flip($remind_list);
7124
7125
                            if (!empty($remind_list)) {
7126
                                sort($remind_list);
7127
                                array_filter($remind_list);
7128
                                $remind_list_string = implode(',', $remind_list);
7129
                            }
7130
                        }
7131
                    }
7132
                }
7133
                $value = Database::escape_string($remind_list_string);
7134
                $sql = "UPDATE $track_exercises
7135
                        SET questions_to_check = '$value'
7136
                        WHERE exe_id = $exeId ";
7137
                Database::query($sql);
7138
            }
7139
        }
7140
    }
7141
7142
    /**
7143
     * @param string $answer
7144
     *
7145
     * @return mixed
7146
     */
7147
    public function fill_in_blank_answer_to_array($answer)
7148
    {
7149
        api_preg_match_all('/\[[^]]+\]/', $answer, $teacher_answer_list);
7150
        $teacher_answer_list = $teacher_answer_list[0];
7151
7152
        return $teacher_answer_list;
7153
    }
7154
7155
    /**
7156
     * @param string $answer
7157
     *
7158
     * @return string
7159
     */
7160
    public function fill_in_blank_answer_to_string($answer)
7161
    {
7162
        $teacher_answer_list = $this->fill_in_blank_answer_to_array($answer);
7163
        $result = '';
7164
        if (!empty($teacher_answer_list)) {
7165
            foreach ($teacher_answer_list as $teacher_item) {
7166
                // Cleaning student answer list
7167
                $value = strip_tags($teacher_item);
7168
                $value = api_substr($value, 1, api_strlen($value) - 2);
7169
                $value = explode('/', $value);
7170
                if (!empty($value[0])) {
7171
                    $value = trim($value[0]);
7172
                    $value = str_replace('&nbsp;', '', $value);
7173
                    $result .= $value;
7174
                }
7175
            }
7176
        }
7177
7178
        return $result;
7179
    }
7180
7181
    /**
7182
     * @return string
7183
     */
7184
    public function returnTimeLeftDiv()
7185
    {
7186
        $html = '<div id="clock_warning" style="display:none">';
7187
        $html .= Display::return_message(
7188
            get_lang('ReachedTimeLimit'),
7189
            'warning'
7190
        );
7191
        $html .= ' ';
7192
        $html .= sprintf(
7193
            get_lang('YouWillBeRedirectedInXSeconds'),
7194
            '<span id="counter_to_redirect" class="red_alert"></span>'
7195
        );
7196
        $html .= '</div>';
7197
7198
        $icon = Display::returnFontAwesomeIcon('clock-o');
7199
        $html .= '<div class="count_down">
7200
                    '.get_lang('RemainingTimeToFinishExercise').'
7201
                    '.$icon.'<span id="exercise_clock_warning"></span>
7202
                </div>';
7203
7204
        return $html;
7205
    }
7206
7207
    /**
7208
     * Get categories added in the exercise--category matrix.
7209
     *
7210
     * @return array
7211
     */
7212
    public function getCategoriesInExercise()
7213
    {
7214
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7215
        if (!empty($this->id)) {
7216
            $sql = "SELECT * FROM $table
7217
                    WHERE exercise_id = {$this->id} AND c_id = {$this->course_id} ";
7218
            $result = Database::query($sql);
7219
            $list = [];
7220
            if (Database::num_rows($result)) {
7221
                while ($row = Database::fetch_array($result, 'ASSOC')) {
7222
                    $list[$row['category_id']] = $row;
7223
                }
7224
7225
                return $list;
7226
            }
7227
        }
7228
7229
        return [];
7230
    }
7231
7232
    /**
7233
     * Get total number of question that will be parsed when using the category/exercise.
7234
     *
7235
     * @return int
7236
     */
7237
    public function getNumberQuestionExerciseCategory()
7238
    {
7239
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7240
        if (!empty($this->id)) {
7241
            $sql = "SELECT SUM(count_questions) count_questions
7242
                    FROM $table
7243
                    WHERE exercise_id = {$this->id} AND c_id = {$this->course_id}";
7244
            $result = Database::query($sql);
7245
            if (Database::num_rows($result)) {
7246
                $row = Database::fetch_array($result);
7247
7248
                return (int) $row['count_questions'];
7249
            }
7250
        }
7251
7252
        return 0;
7253
    }
7254
7255
    /**
7256
     * Save categories in the TABLE_QUIZ_REL_CATEGORY table.
7257
     *
7258
     * @param array $categories
7259
     */
7260
    public function save_categories_in_exercise($categories)
7261
    {
7262
        if (!empty($categories) && !empty($this->id)) {
7263
            $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7264
            $sql = "DELETE FROM $table
7265
                    WHERE exercise_id = {$this->id} AND c_id = {$this->course_id}";
7266
            Database::query($sql);
7267
            if (!empty($categories)) {
7268
                foreach ($categories as $categoryId => $countQuestions) {
7269
                    $params = [
7270
                        'c_id' => $this->course_id,
7271
                        'exercise_id' => $this->id,
7272
                        'category_id' => $categoryId,
7273
                        'count_questions' => $countQuestions,
7274
                    ];
7275
                    Database::insert($table, $params);
7276
                }
7277
            }
7278
        }
7279
    }
7280
7281
    /**
7282
     * @param array  $questionList
7283
     * @param int    $currentQuestion
7284
     * @param array  $conditions
7285
     * @param string $link
7286
     *
7287
     * @return string
7288
     */
7289
    public function progressExercisePaginationBar(
7290
        $questionList,
7291
        $currentQuestion,
7292
        $conditions,
7293
        $link
7294
    ) {
7295
        $mediaQuestions = $this->getMediaList();
7296
7297
        $html = '<div class="exercise_pagination pagination pagination-mini"><ul>';
7298
        $counter = 0;
7299
        $nextValue = 0;
7300
        $wasMedia = false;
7301
        $before = 0;
7302
        $counterNoMedias = 0;
7303
        foreach ($questionList as $questionId) {
7304
            $isCurrent = $currentQuestion == ($counterNoMedias + 1) ? true : false;
7305
7306
            if (!empty($nextValue)) {
7307
                if ($wasMedia) {
7308
                    $nextValue = $nextValue - $before + 1;
7309
                }
7310
            }
7311
7312
            if (isset($mediaQuestions) && isset($mediaQuestions[$questionId])) {
7313
                $fixedValue = $counterNoMedias;
7314
7315
                $html .= Display::progressPaginationBar(
7316
                    $nextValue,
7317
                    $mediaQuestions[$questionId],
7318
                    $currentQuestion,
7319
                    $fixedValue,
7320
                    $conditions,
7321
                    $link,
7322
                    true,
7323
                    true
7324
                );
7325
7326
                $counter += count($mediaQuestions[$questionId]) - 1;
7327
                $before = count($questionList);
7328
                $wasMedia = true;
7329
                $nextValue += count($questionList);
7330
            } else {
7331
                $html .= Display::parsePaginationItem(
7332
                    $questionId,
7333
                    $isCurrent,
7334
                    $conditions,
7335
                    $link,
7336
                    $counter
7337
                );
7338
                $counter++;
7339
                $nextValue++;
7340
                $wasMedia = false;
7341
            }
7342
            $counterNoMedias++;
7343
        }
7344
        $html .= '</ul></div>';
7345
7346
        return $html;
7347
    }
7348
7349
    /**
7350
     *  Shows a list of numbers that represents the question to answer in a exercise.
7351
     *
7352
     * @param array  $categories
7353
     * @param int    $current
7354
     * @param array  $conditions
7355
     * @param string $link
7356
     *
7357
     * @return string
7358
     */
7359
    public function progressExercisePaginationBarWithCategories(
7360
        $categories,
7361
        $current,
7362
        $conditions = [],
7363
        $link = null
7364
    ) {
7365
        $html = null;
7366
        $counterNoMedias = 0;
7367
        $nextValue = 0;
7368
        $wasMedia = false;
7369
        $before = 0;
7370
7371
        if (!empty($categories)) {
7372
            $selectionType = $this->getQuestionSelectionType();
7373
            $useRootAsCategoryTitle = false;
7374
7375
            // Grouping questions per parent category see BT#6540
7376
            if (in_array(
7377
                $selectionType,
7378
                [
7379
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED,
7380
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM,
7381
                ]
7382
            )) {
7383
                $useRootAsCategoryTitle = true;
7384
            }
7385
7386
            // If the exercise is set to only show the titles of the categories
7387
            // at the root of the tree, then pre-order the categories tree by
7388
            // removing children and summing their questions into the parent
7389
            // categories
7390
            if ($useRootAsCategoryTitle) {
7391
                // The new categories list starts empty
7392
                $newCategoryList = [];
7393
                foreach ($categories as $category) {
7394
                    $rootElement = $category['root'];
7395
7396
                    if (isset($category['parent_info'])) {
7397
                        $rootElement = $category['parent_info']['id'];
7398
                    }
7399
7400
                    //$rootElement = $category['id'];
7401
                    // If the current category's ancestor was never seen
7402
                    // before, then declare it and assign the current
7403
                    // category to it.
7404
                    if (!isset($newCategoryList[$rootElement])) {
7405
                        $newCategoryList[$rootElement] = $category;
7406
                    } else {
7407
                        // If it was already seen, then merge the previous with
7408
                        // the current category
7409
                        $oldQuestionList = $newCategoryList[$rootElement]['question_list'];
7410
                        $category['question_list'] = array_merge($oldQuestionList, $category['question_list']);
7411
                        $newCategoryList[$rootElement] = $category;
7412
                    }
7413
                }
7414
                // Now use the newly built categories list, with only parents
7415
                $categories = $newCategoryList;
7416
            }
7417
7418
            foreach ($categories as $category) {
7419
                $questionList = $category['question_list'];
7420
                // Check if in this category there questions added in a media
7421
                $mediaQuestionId = $category['media_question'];
7422
                $isMedia = false;
7423
                $fixedValue = null;
7424
7425
                // Media exists!
7426
                if ($mediaQuestionId != 999) {
7427
                    $isMedia = true;
7428
                    $fixedValue = $counterNoMedias;
7429
                }
7430
7431
                //$categoryName = $category['path']; << show the path
7432
                $categoryName = $category['name'];
7433
7434
                if ($useRootAsCategoryTitle) {
7435
                    if (isset($category['parent_info'])) {
7436
                        $categoryName = $category['parent_info']['title'];
7437
                    }
7438
                }
7439
                $html .= '<div class="row">';
7440
                $html .= '<div class="span2">'.$categoryName.'</div>';
7441
                $html .= '<div class="span8">';
7442
7443
                if (!empty($nextValue)) {
7444
                    if ($wasMedia) {
7445
                        $nextValue = $nextValue - $before + 1;
7446
                    }
7447
                }
7448
                $html .= Display::progressPaginationBar(
7449
                    $nextValue,
7450
                    $questionList,
7451
                    $current,
7452
                    $fixedValue,
7453
                    $conditions,
7454
                    $link,
7455
                    $isMedia,
7456
                    true
7457
                );
7458
                $html .= '</div>';
7459
                $html .= '</div>';
7460
7461
                if ($mediaQuestionId == 999) {
7462
                    $counterNoMedias += count($questionList);
7463
                } else {
7464
                    $counterNoMedias++;
7465
                }
7466
7467
                $nextValue += count($questionList);
7468
                $before = count($questionList);
7469
7470
                if ($mediaQuestionId != 999) {
7471
                    $wasMedia = true;
7472
                } else {
7473
                    $wasMedia = false;
7474
                }
7475
            }
7476
        }
7477
7478
        return $html;
7479
    }
7480
7481
    /**
7482
     * Renders a question list.
7483
     *
7484
     * @param array $questionList    (with media questions compressed)
7485
     * @param int   $currentQuestion
7486
     * @param array $exerciseResult
7487
     * @param array $attemptList
7488
     * @param array $remindList
7489
     */
7490
    public function renderQuestionList(
7491
        $questionList,
7492
        $currentQuestion,
7493
        $exerciseResult,
7494
        $attemptList,
7495
        $remindList
7496
    ) {
7497
        $mediaQuestions = $this->getMediaList();
7498
        $i = 0;
7499
7500
        // Normal question list render (medias compressed)
7501
        foreach ($questionList as $questionId) {
7502
            $i++;
7503
            // For sequential exercises
7504
7505
            if ($this->type == ONE_PER_PAGE) {
7506
                // If it is not the right question, goes to the next loop iteration
7507
                if ($currentQuestion != $i) {
7508
                    continue;
7509
                } else {
7510
                    if (!in_array(
7511
                        $this->getFeedbackType(),
7512
                        [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
7513
                    )) {
7514
                        // if the user has already answered this question
7515
                        if (isset($exerciseResult[$questionId])) {
7516
                            echo Display::return_message(
7517
                                get_lang('AlreadyAnswered'),
7518
                                'normal'
7519
                            );
7520
                            break;
7521
                        }
7522
                    }
7523
                }
7524
            }
7525
7526
            // The $questionList contains the media id we check
7527
            // if this questionId is a media question type
7528
            if (isset($mediaQuestions[$questionId]) &&
7529
                $mediaQuestions[$questionId] != 999
7530
            ) {
7531
                // The question belongs to a media
7532
                $mediaQuestionList = $mediaQuestions[$questionId];
7533
                $objQuestionTmp = Question::read($questionId);
7534
7535
                $counter = 1;
7536
                if ($objQuestionTmp->type == MEDIA_QUESTION) {
7537
                    echo $objQuestionTmp->show_media_content();
7538
7539
                    $countQuestionsInsideMedia = count($mediaQuestionList);
7540
7541
                    // Show questions that belongs to a media
7542
                    if (!empty($mediaQuestionList)) {
7543
                        // In order to parse media questions we use letters a, b, c, etc.
7544
                        $letterCounter = 97;
7545
                        foreach ($mediaQuestionList as $questionIdInsideMedia) {
7546
                            $isLastQuestionInMedia = false;
7547
                            if ($counter == $countQuestionsInsideMedia) {
7548
                                $isLastQuestionInMedia = true;
7549
                            }
7550
                            $this->renderQuestion(
7551
                                $questionIdInsideMedia,
7552
                                $attemptList,
7553
                                $remindList,
7554
                                chr($letterCounter),
7555
                                $currentQuestion,
7556
                                $mediaQuestionList,
7557
                                $isLastQuestionInMedia,
7558
                                $questionList
7559
                            );
7560
                            $letterCounter++;
7561
                            $counter++;
7562
                        }
7563
                    }
7564
                } else {
7565
                    $this->renderQuestion(
7566
                        $questionId,
7567
                        $attemptList,
7568
                        $remindList,
7569
                        $i,
7570
                        $currentQuestion,
7571
                        null,
7572
                        null,
7573
                        $questionList
7574
                    );
7575
                    $i++;
7576
                }
7577
            } else {
7578
                // Normal question render.
7579
                $this->renderQuestion(
7580
                    $questionId,
7581
                    $attemptList,
7582
                    $remindList,
7583
                    $i,
7584
                    $currentQuestion,
7585
                    null,
7586
                    null,
7587
                    $questionList
7588
                );
7589
            }
7590
7591
            // For sequential exercises.
7592
            if ($this->type == ONE_PER_PAGE) {
7593
                // quits the loop
7594
                break;
7595
            }
7596
        }
7597
        // end foreach()
7598
7599
        if ($this->type == ALL_ON_ONE_PAGE) {
7600
            $exercise_actions = $this->show_button($questionId, $currentQuestion);
7601
            echo Display::div($exercise_actions, ['class' => 'exercise_actions']);
7602
        }
7603
    }
7604
7605
    /**
7606
     * @param int   $questionId
7607
     * @param array $attemptList
7608
     * @param array $remindList
7609
     * @param int   $i
7610
     * @param int   $current_question
7611
     * @param array $questions_in_media
7612
     * @param bool  $last_question_in_media
7613
     * @param array $realQuestionList
7614
     * @param bool  $generateJS
7615
     */
7616
    public function renderQuestion(
7617
        $questionId,
7618
        $attemptList,
7619
        $remindList,
7620
        $i,
7621
        $current_question,
7622
        $questions_in_media = [],
7623
        $last_question_in_media = false,
7624
        $realQuestionList,
7625
        $generateJS = true
7626
    ) {
7627
        // With this option on the question is loaded via AJAX
7628
        //$generateJS = true;
7629
        //$this->loadQuestionAJAX = true;
7630
7631
        if ($generateJS && $this->loadQuestionAJAX) {
7632
            $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?a=get_question&id='.$questionId.'&'.api_get_cidreq();
7633
            $params = [
7634
                'questionId' => $questionId,
7635
                'attemptList' => $attemptList,
7636
                'remindList' => $remindList,
7637
                'i' => $i,
7638
                'current_question' => $current_question,
7639
                'questions_in_media' => $questions_in_media,
7640
                'last_question_in_media' => $last_question_in_media,
7641
            ];
7642
            $params = json_encode($params);
7643
7644
            $script = '<script>
7645
            $(function(){
7646
                var params = '.$params.';
7647
                $.ajax({
7648
                    type: "GET",
7649
                    data: params,
7650
                    url: "'.$url.'",
7651
                    success: function(return_value) {
7652
                        $("#ajaxquestiondiv'.$questionId.'").html(return_value);
7653
                    }
7654
                });
7655
            });
7656
            </script>
7657
            <div id="ajaxquestiondiv'.$questionId.'"></div>';
7658
            echo $script;
7659
        } else {
7660
            global $origin;
7661
            $question_obj = Question::read($questionId);
7662
            $user_choice = isset($attemptList[$questionId]) ? $attemptList[$questionId] : null;
7663
            $remind_highlight = null;
7664
7665
            // Hides questions when reviewing a ALL_ON_ONE_PAGE exercise
7666
            // see #4542 no_remind_highlight class hide with jquery
7667
            if ($this->type == ALL_ON_ONE_PAGE && isset($_GET['reminder']) && $_GET['reminder'] == 2) {
7668
                $remind_highlight = 'no_remind_highlight';
7669
                if (in_array($question_obj->type, Question::question_type_no_review())) {
7670
                    return null;
7671
                }
7672
            }
7673
7674
            $attributes = ['id' => 'remind_list['.$questionId.']'];
7675
7676
            // Showing the question
7677
            $exercise_actions = null;
7678
            echo '<a id="questionanchor'.$questionId.'"></a><br />';
7679
            echo '<div id="question_div_'.$questionId.'" class="main_question '.$remind_highlight.'" >';
7680
7681
            // Shows the question + possible answers
7682
            $showTitle = $this->getHideQuestionTitle() == 1 ? false : true;
7683
            echo $this->showQuestion(
7684
                $question_obj,
7685
                false,
7686
                $origin,
7687
                $i,
7688
                $showTitle,
7689
                false,
7690
                $user_choice,
7691
                false,
7692
                null,
7693
                false,
7694
                $this->getModelType(),
7695
                $this->categoryMinusOne
7696
            );
7697
7698
            // Button save and continue
7699
            switch ($this->type) {
7700
                case ONE_PER_PAGE:
7701
                    $exercise_actions .= $this->show_button(
7702
                        $questionId,
7703
                        $current_question,
7704
                        null,
7705
                        $remindList
7706
                    );
7707
                    break;
7708
                case ALL_ON_ONE_PAGE:
7709
                    if (api_is_allowed_to_session_edit()) {
7710
                        $button = [
7711
                            Display::button(
7712
                                'save_now',
7713
                                get_lang('SaveForNow'),
7714
                                ['type' => 'button', 'class' => 'btn btn-primary', 'data-question' => $questionId]
7715
                            ),
7716
                            '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>',
7717
                        ];
7718
                        $exercise_actions .= Display::div(
7719
                            implode(PHP_EOL, $button),
7720
                            ['class' => 'exercise_save_now_button']
7721
                        );
7722
                    }
7723
                    break;
7724
            }
7725
7726
            if (!empty($questions_in_media)) {
7727
                $count_of_questions_inside_media = count($questions_in_media);
7728
                if ($count_of_questions_inside_media > 1 && api_is_allowed_to_session_edit()) {
7729
                    $button = [
7730
                        Display::button(
7731
                            'save_now',
7732
                            get_lang('SaveForNow'),
7733
                            ['type' => 'button', 'class' => 'btn btn-primary', 'data-question' => $questionId]
7734
                        ),
7735
                        '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>&nbsp;',
7736
                    ];
7737
                    $exercise_actions = Display::div(
7738
                        implode(PHP_EOL, $button),
7739
                        ['class' => 'exercise_save_now_button']
7740
                    );
7741
                }
7742
7743
                if ($last_question_in_media && $this->type == ONE_PER_PAGE) {
7744
                    $exercise_actions = $this->show_button($questionId, $current_question, $questions_in_media);
7745
                }
7746
            }
7747
7748
            // Checkbox review answers
7749
            if ($this->review_answers &&
7750
                !in_array($question_obj->type, Question::question_type_no_review())
7751
            ) {
7752
                $remind_question_div = Display::tag(
7753
                    'label',
7754
                    Display::input(
7755
                        'checkbox',
7756
                        'remind_list['.$questionId.']',
7757
                        '',
7758
                        $attributes
7759
                    ).get_lang('ReviewQuestionLater'),
7760
                    [
7761
                        'class' => 'checkbox',
7762
                        'for' => 'remind_list['.$questionId.']',
7763
                    ]
7764
                );
7765
                $exercise_actions .= Display::div(
7766
                    $remind_question_div,
7767
                    ['class' => 'exercise_save_now_button']
7768
                );
7769
            }
7770
7771
            echo Display::div(' ', ['class' => 'clear']);
7772
7773
            $paginationCounter = null;
7774
            if ($this->type == ONE_PER_PAGE) {
7775
                if (empty($questions_in_media)) {
7776
                    $paginationCounter = Display::paginationIndicator(
7777
                        $current_question,
7778
                        count($realQuestionList)
7779
                    );
7780
                } else {
7781
                    if ($last_question_in_media) {
7782
                        $paginationCounter = Display::paginationIndicator(
7783
                            $current_question,
7784
                            count($realQuestionList)
7785
                        );
7786
                    }
7787
                }
7788
            }
7789
7790
            echo '<div class="row"><div class="pull-right">'.$paginationCounter.'</div></div>';
7791
            echo Display::div($exercise_actions, ['class' => 'form-actions']);
7792
            echo '</div>';
7793
        }
7794
    }
7795
7796
    /**
7797
     * Returns an array of categories details for the questions of the current
7798
     * exercise.
7799
     *
7800
     * @return array
7801
     */
7802
    public function getQuestionWithCategories()
7803
    {
7804
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
7805
        $categoryRelTable = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
7806
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
7807
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
7808
        $sql = "SELECT DISTINCT cat.*
7809
                FROM $TBL_EXERCICE_QUESTION e
7810
                INNER JOIN $TBL_QUESTIONS q
7811
                ON (e.question_id = q.id AND e.c_id = q.c_id)
7812
                INNER JOIN $categoryRelTable catRel
7813
                ON (catRel.question_id = e.question_id AND catRel.c_id = e.c_id)
7814
                INNER JOIN $categoryTable cat
7815
                ON (cat.id = catRel.category_id AND cat.c_id = e.c_id)
7816
                WHERE
7817
                  e.c_id = {$this->course_id} AND
7818
                  e.exercice_id	= ".intval($this->id);
7819
7820
        $result = Database::query($sql);
7821
        $categoriesInExercise = [];
7822
        if (Database::num_rows($result)) {
7823
            $categoriesInExercise = Database::store_result($result, 'ASSOC');
7824
        }
7825
7826
        return $categoriesInExercise;
7827
    }
7828
7829
    /**
7830
     * Calculate the max_score of the quiz, depending of question inside, and quiz advanced option.
7831
     */
7832
    public function get_max_score()
7833
    {
7834
        $out_max_score = 0;
7835
        // list of question's id !!! the array key start at 1 !!!
7836
        $questionList = $this->selectQuestionList(true);
7837
7838
        // test is randomQuestions - see field random of test
7839
        if ($this->random > 0 && 0 == $this->randomByCat) {
7840
            $numberRandomQuestions = $this->random;
7841
            $questionScoreList = [];
7842
            foreach ($questionList as $questionId) {
7843
                $tmpobj_question = Question::read($questionId);
7844
                if (is_object($tmpobj_question)) {
7845
                    $questionScoreList[] = $tmpobj_question->weighting;
7846
                }
7847
            }
7848
7849
            rsort($questionScoreList);
7850
            // add the first $numberRandomQuestions value of score array to get max_score
7851
            for ($i = 0; $i < min($numberRandomQuestions, count($questionScoreList)); $i++) {
7852
                $out_max_score += $questionScoreList[$i];
7853
            }
7854
        } elseif ($this->random > 0 && $this->randomByCat > 0) {
7855
            // test is random by category
7856
            // get the $numberRandomQuestions best score question of each category
7857
            $numberRandomQuestions = $this->random;
7858
            $tab_categories_scores = [];
7859
            foreach ($questionList as $questionId) {
7860
                $question_category_id = TestCategory::getCategoryForQuestion($questionId);
7861
                if (!is_array($tab_categories_scores[$question_category_id])) {
7862
                    $tab_categories_scores[$question_category_id] = [];
7863
                }
7864
                $tmpobj_question = Question::read($questionId);
7865
                if (is_object($tmpobj_question)) {
7866
                    $tab_categories_scores[$question_category_id][] = $tmpobj_question->weighting;
7867
                }
7868
            }
7869
7870
            // here we've got an array with first key, the category_id, second key, score of question for this cat
7871
            foreach ($tab_categories_scores as $tab_scores) {
7872
                rsort($tab_scores);
7873
                for ($i = 0; $i < min($numberRandomQuestions, count($tab_scores)); $i++) {
7874
                    $out_max_score += $tab_scores[$i];
7875
                }
7876
            }
7877
        } else {
7878
            // standard test, just add each question score
7879
            foreach ($questionList as $questionId) {
7880
                $question = Question::read($questionId, $this->course);
7881
                $out_max_score += $question->weighting;
7882
            }
7883
        }
7884
7885
        return $out_max_score;
7886
    }
7887
7888
    /**
7889
     * @return string
7890
     */
7891
    public function get_formated_title()
7892
    {
7893
        if (api_get_configuration_value('save_titles_as_html')) {
7894
        }
7895
7896
        return api_html_entity_decode($this->selectTitle());
7897
    }
7898
7899
    /**
7900
     * @param string $title
7901
     *
7902
     * @return string
7903
     */
7904
    public static function get_formated_title_variable($title)
7905
    {
7906
        return api_html_entity_decode($title);
7907
    }
7908
7909
    /**
7910
     * @return string
7911
     */
7912
    public function format_title()
7913
    {
7914
        return api_htmlentities($this->title);
7915
    }
7916
7917
    /**
7918
     * @param string $title
7919
     *
7920
     * @return string
7921
     */
7922
    public static function format_title_variable($title)
7923
    {
7924
        return api_htmlentities($title);
7925
    }
7926
7927
    /**
7928
     * @param int $courseId
7929
     * @param int $sessionId
7930
     *
7931
     * @return array exercises
7932
     */
7933
    public function getExercisesByCourseSession($courseId, $sessionId)
7934
    {
7935
        $courseId = (int) $courseId;
7936
        $sessionId = (int) $sessionId;
7937
7938
        $tbl_quiz = Database::get_course_table(TABLE_QUIZ_TEST);
7939
        $sql = "SELECT * FROM $tbl_quiz cq
7940
                WHERE
7941
                    cq.c_id = %s AND
7942
                    (cq.session_id = %s OR cq.session_id = 0) AND
7943
                    cq.active = 0
7944
                ORDER BY cq.id";
7945
        $sql = sprintf($sql, $courseId, $sessionId);
7946
7947
        $result = Database::query($sql);
7948
7949
        $rows = [];
7950
        while ($row = Database::fetch_array($result, 'ASSOC')) {
7951
            $rows[] = $row;
7952
        }
7953
7954
        return $rows;
7955
    }
7956
7957
    /**
7958
     * @param int   $courseId
7959
     * @param int   $sessionId
7960
     * @param array $quizId
7961
     *
7962
     * @return array exercises
7963
     */
7964
    public function getExerciseAndResult($courseId, $sessionId, $quizId = [])
7965
    {
7966
        if (empty($quizId)) {
7967
            return [];
7968
        }
7969
7970
        $sessionId = (int) $sessionId;
7971
        $courseId = (int) $courseId;
7972
7973
        $ids = is_array($quizId) ? $quizId : [$quizId];
7974
        $ids = array_map('intval', $ids);
7975
        $ids = implode(',', $ids);
7976
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7977
        if (0 != $sessionId) {
7978
            $sql = "SELECT * FROM $track_exercises te
7979
              INNER JOIN c_quiz cq ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id
7980
              WHERE
7981
              te.id = %s AND
7982
              te.session_id = %s AND
7983
              cq.id IN (%s)
7984
              ORDER BY cq.id";
7985
7986
            $sql = sprintf($sql, $courseId, $sessionId, $ids);
7987
        } else {
7988
            $sql = "SELECT * FROM $track_exercises te
7989
              INNER JOIN c_quiz cq ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id
7990
              WHERE
7991
              te.id = %s AND
7992
              cq.id IN (%s)
7993
              ORDER BY cq.id";
7994
            $sql = sprintf($sql, $courseId, $ids);
7995
        }
7996
        $result = Database::query($sql);
7997
        $rows = [];
7998
        while ($row = Database::fetch_array($result, 'ASSOC')) {
7999
            $rows[] = $row;
8000
        }
8001
8002
        return $rows;
8003
    }
8004
8005
    /**
8006
     * @param $exeId
8007
     * @param $exercise_stat_info
8008
     * @param $remindList
8009
     * @param $currentQuestion
8010
     *
8011
     * @return int|null
8012
     */
8013
    public static function getNextQuestionId(
8014
        $exeId,
8015
        $exercise_stat_info,
8016
        $remindList,
8017
        $currentQuestion
8018
    ) {
8019
        $result = Event::get_exercise_results_by_attempt($exeId, 'incomplete');
8020
8021
        if (isset($result[$exeId])) {
8022
            $result = $result[$exeId];
8023
        } else {
8024
            return null;
8025
        }
8026
8027
        $data_tracking = $exercise_stat_info['data_tracking'];
8028
        $data_tracking = explode(',', $data_tracking);
8029
8030
        // if this is the final question do nothing.
8031
        if ($currentQuestion == count($data_tracking)) {
8032
            return null;
8033
        }
8034
8035
        $currentQuestion--;
8036
8037
        if (!empty($result['question_list'])) {
8038
            $answeredQuestions = [];
8039
            foreach ($result['question_list'] as $question) {
8040
                if (!empty($question['answer'])) {
8041
                    $answeredQuestions[] = $question['question_id'];
8042
                }
8043
            }
8044
8045
            // Checking answered questions
8046
            $counterAnsweredQuestions = 0;
8047
            foreach ($data_tracking as $questionId) {
8048
                if (!in_array($questionId, $answeredQuestions)) {
8049
                    if ($currentQuestion != $counterAnsweredQuestions) {
8050
                        break;
8051
                    }
8052
                }
8053
                $counterAnsweredQuestions++;
8054
            }
8055
8056
            $counterRemindListQuestions = 0;
8057
            // Checking questions saved in the reminder list
8058
            if (!empty($remindList)) {
8059
                foreach ($data_tracking as $questionId) {
8060
                    if (in_array($questionId, $remindList)) {
8061
                        // Skip the current question
8062
                        if ($currentQuestion != $counterRemindListQuestions) {
8063
                            break;
8064
                        }
8065
                    }
8066
                    $counterRemindListQuestions++;
8067
                }
8068
8069
                if ($counterRemindListQuestions < $currentQuestion) {
8070
                    return null;
8071
                }
8072
8073
                if (!empty($counterRemindListQuestions)) {
8074
                    if ($counterRemindListQuestions > $counterAnsweredQuestions) {
8075
                        return $counterAnsweredQuestions;
8076
                    } else {
8077
                        return $counterRemindListQuestions;
8078
                    }
8079
                }
8080
            }
8081
8082
            return $counterAnsweredQuestions;
8083
        }
8084
    }
8085
8086
    /**
8087
     * Gets the position of a questionId in the question list.
8088
     *
8089
     * @param $questionId
8090
     *
8091
     * @return int
8092
     */
8093
    public function getPositionInCompressedQuestionList($questionId)
8094
    {
8095
        $questionList = $this->getQuestionListWithMediasCompressed();
8096
        $mediaQuestions = $this->getMediaList();
8097
        $position = 1;
8098
        foreach ($questionList as $id) {
8099
            if (isset($mediaQuestions[$id]) && in_array($questionId, $mediaQuestions[$id])) {
8100
                $mediaQuestionList = $mediaQuestions[$id];
8101
                if (in_array($questionId, $mediaQuestionList)) {
8102
                    return $position;
8103
                } else {
8104
                    $position++;
8105
                }
8106
            } else {
8107
                if ($id == $questionId) {
8108
                    return $position;
8109
                } else {
8110
                    $position++;
8111
                }
8112
            }
8113
        }
8114
8115
        return 1;
8116
    }
8117
8118
    /**
8119
     * Get the correct answers in all attempts.
8120
     *
8121
     * @param int  $learnPathId
8122
     * @param int  $learnPathItemId
8123
     * @param bool $onlyCorrect
8124
     *
8125
     * @return array
8126
     */
8127
    public function getAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0, $onlyCorrect = true)
8128
    {
8129
        $attempts = Event::getExerciseResultsByUser(
8130
            api_get_user_id(),
8131
            $this->id,
8132
            api_get_course_int_id(),
8133
            api_get_session_id(),
8134
            $learnPathId,
8135
            $learnPathItemId,
8136
            'DESC'
8137
        );
8138
8139
        $list = [];
8140
        foreach ($attempts as $attempt) {
8141
            foreach ($attempt['question_list'] as $answers) {
8142
                foreach ($answers as $answer) {
8143
                    $objAnswer = new Answer($answer['question_id']);
8144
                    if ($onlyCorrect) {
8145
                        switch ($objAnswer->getQuestionType()) {
8146
                            case FILL_IN_BLANKS:
8147
                                $isCorrect = FillBlanks::isCorrect($answer['answer']);
8148
                                break;
8149
                            case MATCHING:
8150
                            case DRAGGABLE:
8151
                            case MATCHING_DRAGGABLE:
8152
                                $isCorrect = Matching::isCorrect(
8153
                                    $answer['position'],
8154
                                    $answer['answer'],
8155
                                    $answer['question_id']
8156
                                );
8157
                                break;
8158
                            case ORAL_EXPRESSION:
8159
                                $isCorrect = false;
8160
                                break;
8161
                            default:
8162
                                $isCorrect = $objAnswer->isCorrectByAutoId($answer['answer']);
8163
                        }
8164
                        if ($isCorrect) {
8165
                            $list[$answer['question_id']][] = $answer;
8166
                        }
8167
                    } else {
8168
                        $list[$answer['question_id']][] = $answer;
8169
                    }
8170
                }
8171
            }
8172
8173
            if (false === $onlyCorrect) {
8174
                // Only take latest attempt
8175
                break;
8176
            }
8177
        }
8178
8179
        return $list;
8180
    }
8181
8182
    /**
8183
     * Get the correct answers in all attempts.
8184
     *
8185
     * @param int $learnPathId
8186
     * @param int $learnPathItemId
8187
     *
8188
     * @return array
8189
     */
8190
    public function getCorrectAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0)
8191
    {
8192
        return $this->getAnswersInAllAttempts($learnPathId, $learnPathItemId);
8193
    }
8194
8195
    /**
8196
     * @return bool
8197
     */
8198
    public function showPreviousButton()
8199
    {
8200
        $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
8201
        if (false === $allow) {
8202
            return true;
8203
        }
8204
8205
        return $this->showPreviousButton;
8206
    }
8207
8208
    public function getPreventBackwards()
8209
    {
8210
        $allow = api_get_configuration_value('quiz_prevent_backwards_move');
8211
        if (false === $allow) {
8212
            return 0;
8213
        }
8214
8215
        return (int) $this->preventBackwards;
8216
    }
8217
8218
    /**
8219
     * @return int
8220
     */
8221
    public function getExerciseCategoryId()
8222
    {
8223
        if (empty($this->exerciseCategoryId)) {
8224
            return null;
8225
        }
8226
8227
        return (int) $this->exerciseCategoryId;
8228
    }
8229
8230
    /**
8231
     * @param int $value
8232
     */
8233
    public function setExerciseCategoryId($value)
8234
    {
8235
        if (!empty($value)) {
8236
            $this->exerciseCategoryId = (int) $value;
8237
        }
8238
    }
8239
8240
    /**
8241
     * @param array $values
8242
     *
8243
     * @throws \Doctrine\DBAL\DBALException
8244
     */
8245
    public function setPageResultConfiguration($values)
8246
    {
8247
        $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
8248
        if ($pageConfig) {
8249
            $params = [
8250
                'hide_expected_answer' => isset($values['hide_expected_answer']) ? $values['hide_expected_answer'] : '',
8251
                'hide_question_score' => isset($values['hide_question_score']) ? $values['hide_question_score'] : '',
8252
                'hide_total_score' => isset($values['hide_total_score']) ? $values['hide_total_score'] : '',
8253
            ];
8254
            $type = Type::getType('array');
8255
            $platform = Database::getManager()->getConnection()->getDatabasePlatform();
8256
            $this->pageResultConfiguration = $type->convertToDatabaseValue($params, $platform);
8257
        }
8258
    }
8259
8260
    /**
8261
     * @param array $defaults
8262
     */
8263
    public function setPageResultConfigurationDefaults(&$defaults)
8264
    {
8265
        $configuration = $this->getPageResultConfiguration();
8266
        if (!empty($configuration) && !empty($defaults)) {
8267
            $defaults = array_merge($defaults, $configuration);
8268
        }
8269
    }
8270
8271
    /**
8272
     * @return array
8273
     */
8274
    public function getPageResultConfiguration()
8275
    {
8276
        $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
8277
        if ($pageConfig) {
8278
            /*$params = [
8279
                'hide_expected_answer' => isset($values['hide_expected_answer']) ? $values['hide_expected_answer'] : '',
8280
                'hide_question_score' => isset($values['hide_question_score']) ? $values['hide_question_score'] : '',
8281
                'hide_total_score' => isset($values['hide_total_score']) ? $values['hide_total_score'] : ''
8282
            ];*/
8283
            $type = Type::getType('array');
8284
            $platform = Database::getManager()->getConnection()->getDatabasePlatform();
8285
8286
            return $type->convertToPHPValue($this->pageResultConfiguration, $platform);
8287
        }
8288
8289
        return [];
8290
    }
8291
8292
    /**
8293
     * @param string $attribute
8294
     *
8295
     * @return mixed|null
8296
     */
8297
    public function getPageConfigurationAttribute($attribute)
8298
    {
8299
        $result = $this->getPageResultConfiguration();
8300
8301
        if (!empty($result)) {
8302
            return isset($result[$attribute]) ? $result[$attribute] : null;
8303
        }
8304
8305
        return null;
8306
    }
8307
8308
    /**
8309
     * @param bool $showPreviousButton
8310
     *
8311
     * @return Exercise
8312
     */
8313
    public function setShowPreviousButton($showPreviousButton)
8314
    {
8315
        $this->showPreviousButton = $showPreviousButton;
8316
8317
        return $this;
8318
    }
8319
8320
    /**
8321
     * @param array $notifications
8322
     */
8323
    public function setNotifications($notifications)
8324
    {
8325
        $this->notifications = $notifications;
8326
    }
8327
8328
    /**
8329
     * @return array
8330
     */
8331
    public function getNotifications()
8332
    {
8333
        return $this->notifications;
8334
    }
8335
8336
    /**
8337
     * @return bool
8338
     */
8339
    public function showExpectedChoice()
8340
    {
8341
        return api_get_configuration_value('show_exercise_expected_choice');
8342
    }
8343
8344
    /**
8345
     * @return bool
8346
     */
8347
    public function showExpectedChoiceColumn()
8348
    {
8349
        if ($this->hideExpectedAnswer) {
8350
            return false;
8351
        }
8352
8353
        if (!in_array($this->results_disabled, [
8354
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
8355
        ])
8356
        ) {
8357
            $hide = (int) $this->getPageConfigurationAttribute('hide_expected_answer');
8358
            if (1 === $hide) {
8359
                return false;
8360
            }
8361
8362
            return true;
8363
        }
8364
8365
        return false;
8366
    }
8367
8368
    /**
8369
     * @param string $class
8370
     * @param string $scoreLabel
8371
     * @param string $result
8372
     * @param array
8373
     *
8374
     * @return string
8375
     */
8376
    public function getQuestionRibbon($class, $scoreLabel, $result, $array)
8377
    {
8378
        $hide = (int) $this->getPageConfigurationAttribute('hide_question_score');
8379
        if (1 === $hide) {
8380
            return '';
8381
        }
8382
8383
        if ($this->showExpectedChoice()) {
8384
            $html = null;
8385
            $hideLabel = api_get_configuration_value('exercise_hide_label');
8386
            $label = '<div class="rib rib-'.$class.'">
8387
                        <h3>'.$scoreLabel.'</h3>
8388
                      </div>';
8389
            if (!empty($result)) {
8390
                $label .= '<h4>'.get_lang('Score').': '.$result.'</h4>';
8391
            }
8392
            if (true === $hideLabel) {
8393
                $answerUsed = (int) $array['used'];
8394
                $answerMissing = (int) $array['missing'] - $answerUsed;
8395
                for ($i = 1; $i <= $answerUsed; $i++) {
8396
                    $html .= '<span class="score-img">'.
8397
                        Display::return_icon('attempt-check.png', null, null, ICON_SIZE_SMALL).
8398
                        '</span>';
8399
                }
8400
                for ($i = 1; $i <= $answerMissing; $i++) {
8401
                    $html .= '<span class="score-img">'.
8402
                        Display::return_icon('attempt-nocheck.png', null, null, ICON_SIZE_SMALL).
8403
                        '</span>';
8404
                }
8405
                $label = '<div class="score-title">'.get_lang('CorrectAnswers').': '.$result.'</div>';
8406
                $label .= '<div class="score-limits">';
8407
                $label .= $html;
8408
                $label .= '</div>';
8409
            }
8410
8411
            return '<div class="ribbon">
8412
                '.$label.'
8413
                </div>'
8414
                ;
8415
        } else {
8416
            $html = '<div class="ribbon">
8417
                        <div class="rib rib-'.$class.'">
8418
                            <h3>'.$scoreLabel.'</h3>
8419
                        </div>';
8420
            if (!empty($result)) {
8421
                $html .= '<h4>'.get_lang('Score').': '.$result.'</h4>';
8422
            }
8423
            $html .= '</div>';
8424
8425
            return $html;
8426
        }
8427
    }
8428
8429
    /**
8430
     * @return int
8431
     */
8432
    public function getAutoLaunch()
8433
    {
8434
        return $this->autolaunch;
8435
    }
8436
8437
    /**
8438
     * Clean auto launch settings for all exercise in course/course-session.
8439
     */
8440
    public function enableAutoLaunch()
8441
    {
8442
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8443
        $sql = "UPDATE $table SET autolaunch = 1
8444
                WHERE iid = ".$this->iId;
8445
        Database::query($sql);
8446
    }
8447
8448
    /**
8449
     * Clean auto launch settings for all exercise in course/course-session.
8450
     */
8451
    public function cleanCourseLaunchSettings()
8452
    {
8453
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8454
        $sql = "UPDATE $table SET autolaunch = 0
8455
                WHERE c_id = ".$this->course_id." AND session_id = ".$this->sessionId;
8456
        Database::query($sql);
8457
    }
8458
8459
    /**
8460
     * Get the title without HTML tags.
8461
     *
8462
     * @return string
8463
     */
8464
    public function getUnformattedTitle()
8465
    {
8466
        return strip_tags(api_html_entity_decode($this->title));
8467
    }
8468
8469
    /**
8470
     * Get the question IDs from quiz_rel_question for the current quiz,
8471
     * using the parameters as the arguments to the SQL's LIMIT clause.
8472
     * Because the exercise_id is known, it also comes with a filter on
8473
     * the session, so sessions are not specified here.
8474
     *
8475
     * @param int $start  At which question do we want to start the list
8476
     * @param int $length Up to how many results we want
8477
     *
8478
     * @return array A list of question IDs
8479
     */
8480
    public function getQuestionForTeacher($start = 0, $length = 10)
8481
    {
8482
        $start = (int) $start;
8483
        if ($start < 0) {
8484
            $start = 0;
8485
        }
8486
8487
        $length = (int) $length;
8488
8489
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8490
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
8491
        $sql = "SELECT DISTINCT e.question_id
8492
                FROM $quizRelQuestion e
8493
                INNER JOIN $question q
8494
                ON (e.question_id = q.iid AND e.c_id = q.c_id)
8495
                WHERE
8496
                    e.c_id = {$this->course_id} AND
8497
                    e.exercice_id = '".$this->id."'
8498
                ORDER BY question_order
8499
                LIMIT $start, $length
8500
            ";
8501
        $result = Database::query($sql);
8502
        $questionList = [];
8503
        while ($object = Database::fetch_object($result)) {
8504
            $questionList[] = $object->question_id;
8505
        }
8506
8507
        return $questionList;
8508
    }
8509
8510
    /**
8511
     * @param int   $exerciseId
8512
     * @param array $courseInfo
8513
     * @param int   $sessionId
8514
     *
8515
     * @throws \Doctrine\ORM\OptimisticLockException
8516
     *
8517
     * @return bool
8518
     */
8519
    public function generateStats($exerciseId, $courseInfo, $sessionId)
8520
    {
8521
        $allowStats = api_get_configuration_value('allow_gradebook_stats');
8522
        if (!$allowStats) {
8523
            return false;
8524
        }
8525
8526
        if (empty($courseInfo)) {
8527
            return false;
8528
        }
8529
8530
        $courseId = $courseInfo['real_id'];
8531
8532
        $sessionId = (int) $sessionId;
8533
        $exerciseId = (int) $exerciseId;
8534
8535
        $result = $this->read($exerciseId);
8536
8537
        if (empty($result)) {
8538
            api_not_allowed(true);
8539
        }
8540
8541
        $statusToFilter = empty($sessionId) ? STUDENT : 0;
8542
8543
        $studentList = CourseManager::get_user_list_from_course_code(
8544
            $courseInfo['code'],
8545
            $sessionId,
8546
            null,
8547
            null,
8548
            $statusToFilter
8549
        );
8550
8551
        if (empty($studentList)) {
8552
            Display::addFlash(Display::return_message(get_lang('NoUsersInCourse')));
8553
            header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
8554
            exit;
8555
        }
8556
8557
        $tblStats = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8558
8559
        $studentIdList = [];
8560
        if (!empty($studentList)) {
8561
            $studentIdList = array_column($studentList, 'user_id');
8562
        }
8563
8564
        if (false == $this->exercise_was_added_in_lp) {
8565
            $sql = "SELECT * FROM $tblStats
8566
                        WHERE
8567
                            exe_exo_id = $exerciseId AND
8568
                            orig_lp_id = 0 AND
8569
                            orig_lp_item_id = 0 AND
8570
                            status <> 'incomplete' AND
8571
                            session_id = $sessionId AND
8572
                            c_id = $courseId
8573
                        ";
8574
        } else {
8575
            $lpId = null;
8576
            if (!empty($this->lpList)) {
8577
                // Taking only the first LP
8578
                $lpId = $this->getLpBySession($sessionId);
8579
                $lpId = $lpId['lp_id'];
8580
            }
8581
8582
            $sql = "SELECT *
8583
                        FROM $tblStats
8584
                        WHERE
8585
                            exe_exo_id = $exerciseId AND
8586
                            orig_lp_id = $lpId AND
8587
                            status <> 'incomplete' AND
8588
                            session_id = $sessionId AND
8589
                            c_id = $courseId ";
8590
        }
8591
8592
        $sql .= ' ORDER BY exe_id DESC';
8593
8594
        $studentCount = 0;
8595
        $sum = 0;
8596
        $bestResult = 0;
8597
        $sumResult = 0;
8598
        $result = Database::query($sql);
8599
        while ($data = Database::fetch_array($result, 'ASSOC')) {
8600
            // Only take into account users in the current student list.
8601
            if (!empty($studentIdList)) {
8602
                if (!in_array($data['exe_user_id'], $studentIdList)) {
8603
                    continue;
8604
                }
8605
            }
8606
8607
            if (!isset($students[$data['exe_user_id']])) {
8608
                if ($data['exe_weighting'] != 0) {
8609
                    $students[$data['exe_user_id']] = $data['exe_result'];
8610
                    if ($data['exe_result'] > $bestResult) {
8611
                        $bestResult = $data['exe_result'];
8612
                    }
8613
                    $sumResult += $data['exe_result'];
8614
                }
8615
            }
8616
        }
8617
8618
        $count = count($studentList);
8619
        $average = $sumResult / $count;
8620
        $em = Database::getManager();
8621
8622
        $links = AbstractLink::getGradebookLinksFromItem(
8623
            $this->id,
8624
            LINK_EXERCISE,
8625
            $courseInfo['code'],
8626
            $sessionId
8627
        );
8628
8629
        if (empty($links)) {
8630
            $links = AbstractLink::getGradebookLinksFromItem(
8631
                $this->iId,
8632
                LINK_EXERCISE,
8633
                $courseInfo['code'],
8634
                $sessionId
8635
            );
8636
        }
8637
8638
        if (!empty($links)) {
8639
            $repo = $em->getRepository('ChamiloCoreBundle:GradebookLink');
8640
8641
            foreach ($links as $link) {
8642
                $linkId = $link['id'];
8643
                /** @var GradebookLink $exerciseLink */
8644
                $exerciseLink = $repo->find($linkId);
8645
                if ($exerciseLink) {
8646
                    $exerciseLink
8647
                        ->setUserScoreList($students)
8648
                        ->setBestScore($bestResult)
8649
                        ->setAverageScore($average)
8650
                        ->setScoreWeight($this->get_max_score());
8651
                    $em->persist($exerciseLink);
8652
                    $em->flush();
8653
                }
8654
            }
8655
        }
8656
    }
8657
8658
    /**
8659
     * Return an HTML table of exercises for on-screen printing, including
8660
     * action icons. If no exercise is present and the user can edit the
8661
     * course, show a "create test" button.
8662
     *
8663
     * @param int    $categoryId
8664
     * @param string $keyword
8665
     * @param int    $userId
8666
     * @param int    $courseId
8667
     * @param int    $sessionId
8668
     * @param bool   $returnData
8669
     * @param int    $minCategoriesInExercise
8670
     * @param int    $filterByResultDisabled
8671
     * @param int    $filterByAttempt
8672
     *
8673
     * @return string
8674
     */
8675
    public static function exerciseGrid(
8676
        $categoryId,
8677
        $keyword = '',
8678
        $userId = 0,
8679
        $courseId = 0,
8680
        $sessionId = 0,
8681
        $returnData = false,
8682
        $minCategoriesInExercise = 0,
8683
        $filterByResultDisabled = 0,
8684
        $filterByAttempt = 0,
8685
        $myActions = null,
8686
        $returnTable = false
8687
    ) {
8688
        //$allowDelete = Exercise::allowAction('delete');
8689
        $allowClean = self::allowAction('clean_results');
8690
8691
        $TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
8692
        $TBL_ITEM_PROPERTY = Database::get_course_table(TABLE_ITEM_PROPERTY);
8693
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8694
        $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
8695
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8696
8697
        $categoryId = (int) $categoryId;
8698
        $keyword = Database::escape_string($keyword);
8699
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
8700
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
8701
8702
        $autoLaunchAvailable = false;
8703
        if (api_get_course_setting('enable_exercise_auto_launch') == 1 &&
8704
            api_get_configuration_value('allow_exercise_auto_launch')
8705
        ) {
8706
            $autoLaunchAvailable = true;
8707
        }
8708
8709
        $is_allowedToEdit = api_is_allowed_to_edit(null, true);
8710
        $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : api_get_course_info();
8711
        $sessionId = $sessionId ? (int) $sessionId : api_get_session_id();
8712
        $courseId = $courseInfo['real_id'];
8713
        $tableRows = [];
8714
        $uploadPath = DIR_HOTPOTATOES; //defined in main_api
8715
        $exercisePath = api_get_self();
8716
        $origin = api_get_origin();
8717
        $userInfo = $userId ? api_get_user_info($userId) : api_get_user_info();
8718
        $charset = 'utf-8';
8719
        $token = Security::get_token();
8720
        $userId = $userId ? (int) $userId : api_get_user_id();
8721
        $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh($userId, $courseInfo);
8722
        $documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
8723
        $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
8724
8725
        // Condition for the session
8726
        $condition_session = api_get_session_condition($sessionId, true, true, 'e.session_id');
8727
        $content = '';
8728
        $column = 0;
8729
        if ($is_allowedToEdit) {
8730
            $column = 1;
8731
        }
8732
8733
        $table = new SortableTableFromArrayConfig(
8734
            [],
8735
            $column,
8736
            self::PAGINATION_ITEMS_PER_PAGE,
8737
            'exercises_cat_'.$categoryId
8738
        );
8739
8740
        $limit = $table->per_page;
8741
        $page = $table->page_nr;
8742
        $from = $limit * ($page - 1);
8743
8744
        $categoryCondition = '';
8745
        if (api_get_configuration_value('allow_exercise_categories')) {
8746
            if (!empty($categoryId)) {
8747
                $categoryCondition = " AND exercise_category_id = $categoryId ";
8748
            } else {
8749
                $categoryCondition = ' AND exercise_category_id IS NULL ';
8750
            }
8751
        }
8752
8753
        $keywordCondition = '';
8754
        if (!empty($keyword)) {
8755
            $keywordCondition = " AND title LIKE '%$keyword%' ";
8756
        }
8757
8758
        $filterByResultDisabledCondition = '';
8759
        $filterByResultDisabled = (int) $filterByResultDisabled;
8760
        if (!empty($filterByResultDisabled)) {
8761
            $filterByResultDisabledCondition = ' AND e.results_disabled = '.$filterByResultDisabled;
8762
        }
8763
        $filterByAttemptCondition = '';
8764
        $filterByAttempt = (int) $filterByAttempt;
8765
        if (!empty($filterByAttempt)) {
8766
            $filterByAttemptCondition = ' AND e.max_attempt = '.$filterByAttempt;
8767
        }
8768
8769
        // Only for administrators
8770
        if ($is_allowedToEdit) {
8771
            $total_sql = "SELECT count(iid) as count
8772
                          FROM $TBL_EXERCISES e
8773
                          WHERE
8774
                                c_id = $courseId AND
8775
                                active <> -1
8776
                                $condition_session
8777
                                $categoryCondition
8778
                                $keywordCondition
8779
                                $filterByResultDisabledCondition
8780
                                $filterByAttemptCondition
8781
                                ";
8782
            $sql = "SELECT * FROM $TBL_EXERCISES e
8783
                    WHERE
8784
                        c_id = $courseId AND
8785
                        active <> -1
8786
                        $condition_session
8787
                        $categoryCondition
8788
                        $keywordCondition
8789
                        $filterByResultDisabledCondition
8790
                        $filterByAttemptCondition
8791
                    ORDER BY title
8792
                    LIMIT $from , $limit";
8793
        } else {
8794
            // Only for students
8795
            if (empty($sessionId)) {
8796
                $condition_session = ' AND ( session_id = 0 OR session_id IS NULL) ';
8797
                $total_sql = "SELECT count(DISTINCT(e.iid)) as count
8798
                              FROM $TBL_EXERCISES e
8799
                              WHERE
8800
                                    e.c_id = $courseId AND
8801
                                    e.active = 1
8802
                                    $condition_session
8803
                                    $categoryCondition
8804
                                    $keywordCondition
8805
                              ";
8806
8807
                $sql = "SELECT DISTINCT e.* FROM $TBL_EXERCISES e
8808
                        WHERE
8809
                             e.c_id = $courseId AND
8810
                             e.active = 1
8811
                             $condition_session
8812
                             $categoryCondition
8813
                             $keywordCondition
8814
                        ORDER BY title
8815
                        LIMIT $from , $limit";
8816
            } else {
8817
                $invisibleSql = "SELECT e.iid
8818
                                  FROM $TBL_EXERCISES e
8819
                                  INNER JOIN $TBL_ITEM_PROPERTY ip
8820
                                  ON (e.id = ip.ref AND e.c_id = ip.c_id)
8821
                                  WHERE
8822
                                        ip.tool = '".TOOL_QUIZ."' AND
8823
                                        e.c_id = $courseId AND
8824
                                        e.active = 1 AND
8825
                                        ip.visibility = 0 AND
8826
                                        ip.session_id = $sessionId
8827
                                        $categoryCondition
8828
                                        $keywordCondition
8829
                                  ";
8830
8831
                $result = Database::query($invisibleSql);
8832
                $result = Database::store_result($result);
8833
                $hiddenFromSessionCondition = ' 1=1 ';
8834
                if (!empty($result)) {
8835
                    $hiddenFromSession = implode("','", array_column($result, 'iid'));
8836
                    $hiddenFromSessionCondition = " e.iid not in ('$hiddenFromSession')";
8837
                }
8838
8839
                $condition_session = " AND (
8840
                        (e.session_id = $sessionId OR e.session_id = 0 OR e.session_id IS NULL) AND
8841
                        $hiddenFromSessionCondition
8842
                )
8843
                ";
8844
8845
                // Only for students
8846
                $total_sql = "SELECT count(DISTINCT(e.iid)) as count
8847
                              FROM $TBL_EXERCISES e
8848
                              WHERE
8849
                                    e.c_id = $courseId AND
8850
                                    e.active = 1
8851
                                    $condition_session
8852
                                    $categoryCondition
8853
                                    $keywordCondition
8854
                              ";
8855
                $sql = "SELECT DISTINCT e.* FROM $TBL_EXERCISES e
8856
                        WHERE
8857
                             e.c_id = $courseId AND
8858
                             e.active = 1
8859
                             $condition_session
8860
                             $categoryCondition
8861
                             $keywordCondition
8862
                        ORDER BY title
8863
                        LIMIT $from , $limit";
8864
            }
8865
        }
8866
8867
        $result = Database::query($sql);
8868
        $result_total = Database::query($total_sql);
8869
8870
        $total_exercises = 0;
8871
        if (Database::num_rows($result_total)) {
8872
            $result_total = Database::fetch_array($result_total);
8873
            $total_exercises = $result_total['count'];
8874
        }
8875
8876
        //get HotPotatoes files (active and inactive)
8877
        if ($is_allowedToEdit) {
8878
            $sql = "SELECT * FROM $TBL_DOCUMENT
8879
                    WHERE
8880
                        c_id = $courseId AND
8881
                        path LIKE '".Database::escape_string($uploadPath.'/%/%')."'";
8882
            $res = Database::query($sql);
8883
            $hp_count = Database :: num_rows($res);
8884
        } else {
8885
            $sql = "SELECT * FROM $TBL_DOCUMENT d
8886
                    INNER JOIN $TBL_ITEM_PROPERTY ip
8887
                    ON (d.id = ip.ref AND d.c_id = ip.c_id)
8888
                    WHERE
8889
                        ip.tool = '".TOOL_DOCUMENT."' AND
8890
                        d.path LIKE '".Database::escape_string($uploadPath.'/%/%')."' AND
8891
                        ip.visibility = 1 AND
8892
                        d.c_id = $courseId AND
8893
                        ip.c_id  = $courseId";
8894
            $res = Database::query($sql);
8895
            $hp_count = Database::num_rows($res);
8896
        }
8897
8898
        $total = $total_exercises + $hp_count;
8899
        $exerciseList = [];
8900
        while ($row = Database::fetch_array($result, 'ASSOC')) {
8901
            $exerciseList[] = $row;
8902
        }
8903
8904
        if (!empty($exerciseList) &&
8905
            api_get_setting('exercise_invisible_in_session') === 'true'
8906
        ) {
8907
            if (!empty($sessionId)) {
8908
                $changeDefaultVisibility = true;
8909
                if (api_get_setting('configure_exercise_visibility_in_course') === 'true') {
8910
                    $changeDefaultVisibility = false;
8911
                    if (api_get_course_setting('exercise_invisible_in_session') == 1) {
8912
                        $changeDefaultVisibility = true;
8913
                    }
8914
                }
8915
8916
                if ($changeDefaultVisibility) {
8917
                    // Check exercise
8918
                    foreach ($exerciseList as $exercise) {
8919
                        if ($exercise['session_id'] == 0) {
8920
                            $visibilityInfo = api_get_item_property_info(
8921
                                $courseId,
8922
                                TOOL_QUIZ,
8923
                                $exercise['iid'],
8924
                                $sessionId
8925
                            );
8926
8927
                            if (empty($visibilityInfo)) {
8928
                                // Create a record for this
8929
                                api_item_property_update(
8930
                                    $courseInfo,
8931
                                    TOOL_QUIZ,
8932
                                    $exercise['iid'],
8933
                                    'invisible',
8934
                                    api_get_user_id(),
8935
                                    0,
8936
                                    null,
8937
                                    '',
8938
                                    '',
8939
                                    $sessionId
8940
                                );
8941
                            }
8942
                        }
8943
                    }
8944
                }
8945
            }
8946
        }
8947
8948
        $webPath = api_get_path(WEB_CODE_PATH);
8949
8950
        if (!empty($exerciseList)) {
8951
            if ($origin !== 'learnpath') {
8952
                $visibilitySetting = api_get_configuration_value('show_hidden_exercise_added_to_lp');
8953
                //avoid sending empty parameters
8954
                $mylpid = empty($learnpath_id) ? '' : '&learnpath_id='.$learnpath_id;
8955
                $mylpitemid = empty($learnpath_item_id) ? '' : '&learnpath_item_id='.$learnpath_item_id;
8956
                foreach ($exerciseList as $row) {
8957
                    $currentRow = [];
8958
                    $my_exercise_id = $row['id'];
8959
                    $attempt_text = '';
8960
                    $actions = '';
8961
                    $exercise = new Exercise($returnData ? $courseId : 0);
8962
                    $exercise->read($my_exercise_id, false);
8963
8964
                    if (empty($exercise->id)) {
8965
                        continue;
8966
                    }
8967
8968
                    $locked = $exercise->is_gradebook_locked;
8969
                    // Validation when belongs to a session
8970
                    $session_img = api_get_session_image($row['session_id'], $userInfo['status']);
8971
8972
                    $time_limits = false;
8973
                    if (!empty($row['start_time']) || !empty($row['end_time'])) {
8974
                        $time_limits = true;
8975
                    }
8976
8977
                    $is_actived_time = false;
8978
                    if ($time_limits) {
8979
                        // check if start time
8980
                        $start_time = false;
8981
                        if (!empty($row['start_time'])) {
8982
                            $start_time = api_strtotime($row['start_time'], 'UTC');
8983
                        }
8984
                        $end_time = false;
8985
                        if (!empty($row['end_time'])) {
8986
                            $end_time = api_strtotime($row['end_time'], 'UTC');
8987
                        }
8988
                        $now = time();
8989
8990
                        //If both "clocks" are enable
8991
                        if ($start_time && $end_time) {
8992
                            if ($now > $start_time && $end_time > $now) {
8993
                                $is_actived_time = true;
8994
                            }
8995
                        } else {
8996
                            //we check the start and end
8997
                            if ($start_time) {
8998
                                if ($now > $start_time) {
8999
                                    $is_actived_time = true;
9000
                                }
9001
                            }
9002
                            if ($end_time) {
9003
                                if ($end_time > $now) {
9004
                                    $is_actived_time = true;
9005
                                }
9006
                            }
9007
                        }
9008
                    }
9009
9010
                    // Blocking empty start times see BT#2800
9011
                    global $_custom;
9012
                    if (isset($_custom['exercises_hidden_when_no_start_date']) &&
9013
                        $_custom['exercises_hidden_when_no_start_date']
9014
                    ) {
9015
                        if (empty($row['start_time'])) {
9016
                            $time_limits = true;
9017
                            $is_actived_time = false;
9018
                        }
9019
                    }
9020
9021
                    $cut_title = $exercise->getCutTitle();
9022
                    $alt_title = '';
9023
                    if ($cut_title != $row['title']) {
9024
                        $alt_title = ' title = "'.$exercise->getUnformattedTitle().'" ';
9025
                    }
9026
9027
                    // Teacher only
9028
                    if ($is_allowedToEdit) {
9029
                        $lp_blocked = null;
9030
                        if ($exercise->exercise_was_added_in_lp == true) {
9031
                            $lp_blocked = Display::div(
9032
                                get_lang('AddedToLPCannotBeAccessed'),
9033
                                ['class' => 'lp_content_type_label']
9034
                            );
9035
                        }
9036
9037
                        // Get visibility in base course
9038
                        $visibility = api_get_item_visibility(
9039
                            $courseInfo,
9040
                            TOOL_QUIZ,
9041
                            $my_exercise_id,
9042
                            0
9043
                        );
9044
9045
                        if (!empty($sessionId)) {
9046
                            // If we are in a session, the test is invisible
9047
                            // in the base course, it is included in a LP
9048
                            // *and* the setting to show it is *not*
9049
                            // specifically set to true, then hide it.
9050
                            if ($visibility == 0) {
9051
                                if (!$visibilitySetting) {
9052
                                    if ($exercise->exercise_was_added_in_lp == true) {
9053
                                        continue;
9054
                                    }
9055
                                }
9056
                            }
9057
9058
                            $visibility = api_get_item_visibility(
9059
                                $courseInfo,
9060
                                TOOL_QUIZ,
9061
                                $my_exercise_id,
9062
                                $sessionId
9063
                            );
9064
                        }
9065
9066
                        if ($row['active'] == 0 || $visibility == 0) {
9067
                            $title = Display::tag('font', $cut_title, ['style' => 'color:grey']);
9068
                        } else {
9069
                            $title = $cut_title;
9070
                        }
9071
9072
                        /*$count_exercise_not_validated = (int) Event::count_exercise_result_not_validated(
9073
                            $my_exercise_id,
9074
                            $courseId,
9075
                            $sessionId
9076
                        );*/
9077
                        $move = null;
9078
                        $class_tip = '';
9079
                        /*if (!empty($count_exercise_not_validated)) {
9080
                            $results_text = $count_exercise_not_validated == 1 ? get_lang('ResultNotRevised') : get_lang('ResultsNotRevised');
9081
                            $title .= '<span class="exercise_tooltip" style="display: none;">'.$count_exercise_not_validated.' '.$results_text.' </span>';
9082
                        }*/
9083
                        $overviewUrl = api_get_path(WEB_CODE_PATH).'exercise/overview.php';
9084
                        $url = $move.
9085
                            '<a
9086
                                '.$alt_title.'
9087
                                class="'.$class_tip.'"
9088
                                id="tooltip_'.$row['id'].'"
9089
                                href="'.$overviewUrl.'?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$row['id'].'"
9090
                            >
9091
                             '.Display::return_icon('quiz.png', $row['title']).'
9092
                             '.$title.'
9093
                             </a>'.PHP_EOL;
9094
9095
                        if (ExerciseLib::isQuizEmbeddable($row)) {
9096
                            $embeddableIcon = Display::return_icon('om_integration.png', get_lang('ThisQuizCanBeEmbeddable'));
9097
                            $url .= Display::div($embeddableIcon, ['class' => 'pull-right']);
9098
                        }
9099
9100
                        $currentRow['title'] = $url.' '.$session_img.$lp_blocked;
9101
9102
                        // Count number exercise - teacher
9103
                        $sql = "SELECT count(*) count FROM $TBL_EXERCISE_QUESTION
9104
                                WHERE c_id = $courseId AND exercice_id = $my_exercise_id";
9105
                        $sqlresult = Database::query($sql);
9106
                        $rowi = (int) Database::result($sqlresult, 0, 0);
9107
9108
                        if ($sessionId == $row['session_id']) {
9109
                            // Questions list
9110
                            $actions = Display::url(
9111
                                Display::return_icon('edit.png', get_lang('Edit'), '', ICON_SIZE_SMALL),
9112
                                'admin.php?'.api_get_cidreq().'&exerciseId='.$row['id']
9113
                            );
9114
9115
                            // Test settings
9116
                            $settings = Display::url(
9117
                                Display::return_icon('settings.png', get_lang('Configure'), '', ICON_SIZE_SMALL),
9118
                                'exercise_admin.php?'.api_get_cidreq().'&exerciseId='.$row['id']
9119
                            );
9120
9121
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
9122
                                $settings = '';
9123
                            }
9124
                            $actions .= $settings;
9125
9126
                            // Exercise results
9127
                            $resultsLink = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$row['id'].'">'.
9128
                                Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
9129
9130
                            if ($limitTeacherAccess) {
9131
                                if (api_is_platform_admin()) {
9132
                                    $actions .= $resultsLink;
9133
                                }
9134
                            } else {
9135
                                // Exercise results
9136
                                $actions .= $resultsLink;
9137
                            }
9138
9139
                            // Auto launch
9140
                            if ($autoLaunchAvailable) {
9141
                                $autoLaunch = $exercise->getAutoLaunch();
9142
                                if (empty($autoLaunch)) {
9143
                                    $actions .= Display::url(
9144
                                        Display::return_icon(
9145
                                            'launch_na.png',
9146
                                            get_lang('Enable'),
9147
                                            '',
9148
                                            ICON_SIZE_SMALL
9149
                                        ),
9150
                                        'exercise.php?'.api_get_cidreq().'&choice=enable_launch&sec_token='.$token.'&exerciseId='.$row['id']
9151
                                    );
9152
                                } else {
9153
                                    $actions .= Display::url(
9154
                                        Display::return_icon(
9155
                                            'launch.png',
9156
                                            get_lang('Disable'),
9157
                                            '',
9158
                                            ICON_SIZE_SMALL
9159
                                        ),
9160
                                        'exercise.php?'.api_get_cidreq().'&choice=disable_launch&sec_token='.$token.'&exerciseId='.$row['id']
9161
                                    );
9162
                                }
9163
                            }
9164
9165
                            // Export
9166
                            $actions .= Display::url(
9167
                                Display::return_icon('cd.png', get_lang('CopyExercise')),
9168
                                '',
9169
                                [
9170
                                    'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('AreYouSureToCopy'), ENT_QUOTES, $charset))." ".addslashes($row['title'])."?"."')) return false;",
9171
                                    'href' => 'exercise.php?'.api_get_cidreq().'&choice=copy_exercise&sec_token='.$token.'&exerciseId='.$row['id'],
9172
                                ]
9173
                            );
9174
9175
                            // Clean exercise
9176
                            $clean = '';
9177
                            if (true === $allowClean) {
9178
                                if (false == $locked) {
9179
                                    $clean = Display::url(
9180
                                        Display::return_icon(
9181
                                            'clean.png',
9182
                                            get_lang('CleanStudentResults'),
9183
                                            '',
9184
                                            ICON_SIZE_SMALL
9185
                                        ),
9186
                                        '',
9187
                                        [
9188
                                            'onclick' => "javascript:if(!confirm('".addslashes(
9189
                                                    api_htmlentities(
9190
                                                        get_lang('AreYouSureToDeleteResults'),
9191
                                                        ENT_QUOTES,
9192
                                                        $charset
9193
                                                    )
9194
                                                )." ".addslashes($row['title'])."?"."')) return false;",
9195
                                            'href' => 'exercise.php?'.api_get_cidreq(
9196
                                                ).'&choice=clean_results&sec_token='.$token.'&exerciseId='.$row['id'],
9197
                                        ]
9198
                                    );
9199
                                } else {
9200
                                    $clean = Display::return_icon(
9201
                                        'clean_na.png',
9202
                                        get_lang('ResourceLockedByGradebook'),
9203
                                        '',
9204
                                        ICON_SIZE_SMALL
9205
                                    );
9206
                                }
9207
                            }
9208
9209
                            $actions .= $clean;
9210
9211
                            // Visible / invisible
9212
                            // Check if this exercise was added in a LP
9213
                            if ($exercise->exercise_was_added_in_lp == true) {
9214
                                $visibility = Display::return_icon(
9215
                                    'invisible.png',
9216
                                    get_lang('AddedToLPCannotBeAccessed'),
9217
                                    '',
9218
                                    ICON_SIZE_SMALL
9219
                                );
9220
                            } else {
9221
                                if ($row['active'] == 0 || $visibility == 0) {
9222
                                    $visibility = Display::url(
9223
                                        Display::return_icon(
9224
                                            'invisible.png',
9225
                                            get_lang('Activate'),
9226
                                            '',
9227
                                            ICON_SIZE_SMALL
9228
                                        ),
9229
                                        'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&exerciseId='.$row['id']
9230
                                    );
9231
                                } else {
9232
                                    // else if not active
9233
                                    $visibility = Display::url(
9234
                                        Display::return_icon(
9235
                                            'visible.png',
9236
                                            get_lang('Deactivate'),
9237
                                            '',
9238
                                            ICON_SIZE_SMALL
9239
                                        ),
9240
                                        'exercise.php?'.api_get_cidreq().'&choice=disable&sec_token='.$token.'&exerciseId='.$row['id']
9241
                                    );
9242
                                }
9243
                            }
9244
9245
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
9246
                                $visibility = '';
9247
                            }
9248
9249
                            $actions .= $visibility;
9250
9251
                            // Export qti ...
9252
                            $export = Display::url(
9253
                                Display::return_icon(
9254
                                    'export_qti2.png',
9255
                                    'IMS/QTI',
9256
                                    '',
9257
                                    ICON_SIZE_SMALL
9258
                                ),
9259
                                'exercise.php?action=exportqti2&exerciseId='.$row['id'].'&'.api_get_cidreq()
9260
                            );
9261
9262
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
9263
                                $export = '';
9264
                            }
9265
9266
                            $actions .= $export;
9267
                        } else {
9268
                            // not session
9269
                            $actions = Display::return_icon(
9270
                                'edit_na.png',
9271
                                get_lang('ExerciseEditionNotAvailableInSession')
9272
                            );
9273
9274
                            // Check if this exercise was added in a LP
9275
                            if ($exercise->exercise_was_added_in_lp == true) {
9276
                                $visibility = Display::return_icon(
9277
                                    'invisible.png',
9278
                                    get_lang('AddedToLPCannotBeAccessed'),
9279
                                    '',
9280
                                    ICON_SIZE_SMALL
9281
                                );
9282
                            } else {
9283
                                if ($row['active'] == 0 || $visibility == 0) {
9284
                                    $visibility = Display::url(
9285
                                        Display::return_icon(
9286
                                            'invisible.png',
9287
                                            get_lang('Activate'),
9288
                                            '',
9289
                                            ICON_SIZE_SMALL
9290
                                        ),
9291
                                        'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&exerciseId='.$row['id']
9292
                                    );
9293
                                } else {
9294
                                    // else if not active
9295
                                    $visibility = Display::url(
9296
                                        Display::return_icon(
9297
                                            'visible.png',
9298
                                            get_lang('Deactivate'),
9299
                                            '',
9300
                                            ICON_SIZE_SMALL
9301
                                        ),
9302
                                        'exercise.php?'.api_get_cidreq().'&choice=disable&sec_token='.$token.'&exerciseId='.$row['id']
9303
                                    );
9304
                                }
9305
                            }
9306
9307
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
9308
                                $visibility = '';
9309
                            }
9310
9311
                            $actions .= $visibility;
9312
                            $actions .= '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$row['id'].'">'.
9313
                                Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
9314
                            $actions .= Display::url(
9315
                                Display::return_icon('cd.gif', get_lang('CopyExercise')),
9316
                                '',
9317
                                [
9318
                                    'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('AreYouSureToCopy'), ENT_QUOTES, $charset))." ".addslashes($row['title'])."?"."')) return false;",
9319
                                    'href' => 'exercise.php?'.api_get_cidreq().'&choice=copy_exercise&sec_token='.$token.'&exerciseId='.$row['id'],
9320
                                ]
9321
                            );
9322
                        }
9323
9324
                        // Delete
9325
                        $delete = '';
9326
                        if ($sessionId == $row['session_id']) {
9327
                            if ($locked == false) {
9328
                                $delete = Display::url(
9329
                                    Display::return_icon(
9330
                                        'delete.png',
9331
                                        get_lang('Delete'),
9332
                                        '',
9333
                                        ICON_SIZE_SMALL
9334
                                    ),
9335
                                    '',
9336
                                    [
9337
                                        'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('AreYouSureToDeleteJS'), ENT_QUOTES, $charset))." ".addslashes($exercise->getUnformattedTitle())."?"."')) return false;",
9338
                                        'href' => 'exercise.php?'.api_get_cidreq().'&choice=delete&sec_token='.$token.'&exerciseId='.$row['id'],
9339
                                    ]
9340
                                );
9341
                            } else {
9342
                                $delete = Display::return_icon(
9343
                                    'delete_na.png',
9344
                                    get_lang('ResourceLockedByGradebook'),
9345
                                    '',
9346
                                    ICON_SIZE_SMALL
9347
                                );
9348
                            }
9349
                        }
9350
9351
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9352
                            $delete = '';
9353
                        }
9354
9355
                        if (!empty($minCategoriesInExercise)) {
9356
                            $cats = TestCategory::getListOfCategoriesForTest($exercise);
9357
                            if (!(count($cats) >= $minCategoriesInExercise)) {
9358
                                continue;
9359
                            }
9360
                        }
9361
9362
                        $actions .= $delete;
9363
9364
                        // Number of questions
9365
                        $random_label = null;
9366
                        if ($row['random'] > 0 || $row['random'] == -1) {
9367
                            // if random == -1 means use random questions with all questions
9368
                            $random_number_of_question = $row['random'];
9369
                            if ($random_number_of_question == -1) {
9370
                                $random_number_of_question = $rowi;
9371
                            }
9372
                            if ($row['random_by_category'] > 0) {
9373
                                $nbQuestionsTotal = TestCategory::getNumberOfQuestionRandomByCategory(
9374
                                    $my_exercise_id,
9375
                                    $random_number_of_question
9376
                                );
9377
                                $number_of_questions = $nbQuestionsTotal.' ';
9378
                                $number_of_questions .= ($nbQuestionsTotal > 1) ? get_lang('QuestionsLowerCase') : get_lang('QuestionLowerCase');
9379
                                $number_of_questions .= ' - ';
9380
                                $number_of_questions .= min(
9381
                                        TestCategory::getNumberMaxQuestionByCat($my_exercise_id), $random_number_of_question
9382
                                    ).' '.get_lang('QuestionByCategory');
9383
                            } else {
9384
                                $random_label = ' ('.get_lang('Random').') ';
9385
                                $number_of_questions = $random_number_of_question.' '.$random_label.' / '.$rowi;
9386
                                // Bug if we set a random value bigger than the real number of questions
9387
                                if ($random_number_of_question > $rowi) {
9388
                                    $number_of_questions = $rowi.' '.$random_label;
9389
                                }
9390
                            }
9391
                        } else {
9392
                            $number_of_questions = $rowi;
9393
                        }
9394
9395
                        $currentRow['count_questions'] = $number_of_questions;
9396
                    } else {
9397
                        // Student only.
9398
                        $visibility = api_get_item_visibility(
9399
                            $courseInfo,
9400
                            TOOL_QUIZ,
9401
                            $my_exercise_id,
9402
                            $sessionId
9403
                        );
9404
9405
                        if ($visibility == 0) {
9406
                            continue;
9407
                        }
9408
9409
                        $url = '<a '.$alt_title.'  href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$row['id'].'">'.
9410
                            $cut_title.'</a>';
9411
9412
                        // Link of the exercise.
9413
                        $currentRow['title'] = $url.' '.$session_img;
9414
9415
                        if ($returnData) {
9416
                            $currentRow['title'] = $exercise->getUnformattedTitle();
9417
                        }
9418
9419
                        // This query might be improved later on by ordering by the new "tms" field rather than by exe_id
9420
                        // Don't remove this marker: note-query-exe-results
9421
                        $sql = "SELECT * FROM $TBL_TRACK_EXERCISES
9422
                                WHERE
9423
                                    exe_exo_id = ".$row['id']." AND
9424
                                    exe_user_id = $userId AND
9425
                                    c_id = ".api_get_course_int_id()." AND
9426
                                    status <> 'incomplete' AND
9427
                                    orig_lp_id = 0 AND
9428
                                    orig_lp_item_id = 0 AND
9429
                                    session_id =  '".api_get_session_id()."'
9430
                                ORDER BY exe_id DESC";
9431
9432
                        $qryres = Database::query($sql);
9433
                        $num = Database :: num_rows($qryres);
9434
9435
                        // Hide the results.
9436
                        $my_result_disabled = $row['results_disabled'];
9437
9438
                        $attempt_text = '-';
9439
                        // Time limits are on
9440
                        if ($time_limits) {
9441
                            // Exam is ready to be taken
9442
                            if ($is_actived_time) {
9443
                                // Show results
9444
                                if (
9445
                                in_array(
9446
                                    $my_result_disabled,
9447
                                    [
9448
                                        RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9449
                                        RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9450
                                        RESULT_DISABLE_SHOW_SCORE_ONLY,
9451
                                        RESULT_DISABLE_RANKING,
9452
                                    ]
9453
                                )
9454
                                ) {
9455
                                    // More than one attempt
9456
                                    if ($num > 0) {
9457
                                        $row_track = Database :: fetch_array($qryres);
9458
                                        $attempt_text = get_lang('LatestAttempt').' : ';
9459
                                        $attempt_text .= ExerciseLib::show_score(
9460
                                            $row_track['exe_result'],
9461
                                            $row_track['exe_weighting']
9462
                                        );
9463
                                    } else {
9464
                                        //No attempts
9465
                                        $attempt_text = get_lang('NotAttempted');
9466
                                    }
9467
                                } else {
9468
                                    $attempt_text = '-';
9469
                                }
9470
                            } else {
9471
                                // Quiz not ready due to time limits
9472
                                //@todo use the is_visible function
9473
                                if (!empty($row['start_time']) && !empty($row['end_time'])) {
9474
                                    $today = time();
9475
                                    $start_time = api_strtotime($row['start_time'], 'UTC');
9476
                                    $end_time = api_strtotime($row['end_time'], 'UTC');
9477
                                    if ($today < $start_time) {
9478
                                        $attempt_text = sprintf(
9479
                                            get_lang('ExerciseWillBeActivatedFromXToY'),
9480
                                            api_convert_and_format_date($row['start_time']),
9481
                                            api_convert_and_format_date($row['end_time'])
9482
                                        );
9483
                                    } else {
9484
                                        if ($today > $end_time) {
9485
                                            $attempt_text = sprintf(
9486
                                                get_lang('ExerciseWasActivatedFromXToY'),
9487
                                                api_convert_and_format_date($row['start_time']),
9488
                                                api_convert_and_format_date($row['end_time'])
9489
                                            );
9490
                                        }
9491
                                    }
9492
                                } else {
9493
                                    //$attempt_text = get_lang('ExamNotAvailableAtThisTime');
9494
                                    if (!empty($row['start_time'])) {
9495
                                        $attempt_text = sprintf(
9496
                                            get_lang('ExerciseAvailableFromX'),
9497
                                            api_convert_and_format_date($row['start_time'])
9498
                                        );
9499
                                    }
9500
                                    if (!empty($row['end_time'])) {
9501
                                        $attempt_text = sprintf(
9502
                                            get_lang('ExerciseAvailableUntilX'),
9503
                                            api_convert_and_format_date($row['end_time'])
9504
                                        );
9505
                                    }
9506
                                }
9507
                            }
9508
                        } else {
9509
                            // Normal behaviour.
9510
                            // Show results.
9511
                            if (
9512
                            in_array(
9513
                                $my_result_disabled,
9514
                                [
9515
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9516
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9517
                                    RESULT_DISABLE_SHOW_SCORE_ONLY,
9518
                                    RESULT_DISABLE_RANKING,
9519
                                ]
9520
                            )
9521
                            ) {
9522
                                if ($num > 0) {
9523
                                    $row_track = Database :: fetch_array($qryres);
9524
                                    $attempt_text = get_lang('LatestAttempt').' : ';
9525
                                    $attempt_text .= ExerciseLib::show_score(
9526
                                        $row_track['exe_result'],
9527
                                        $row_track['exe_weighting']
9528
                                    );
9529
                                } else {
9530
                                    $attempt_text = get_lang('NotAttempted');
9531
                                }
9532
                            }
9533
                        }
9534
9535
                        if ($returnData) {
9536
                            $attempt_text = $num;
9537
                        }
9538
                    }
9539
9540
                    $currentRow['attempt'] = $attempt_text;
9541
9542
                    if ($is_allowedToEdit) {
9543
                        $additionalActions = ExerciseLib::getAdditionalTeacherActions($row['id']);
9544
9545
                        if (!empty($additionalActions)) {
9546
                            $actions .= $additionalActions.PHP_EOL;
9547
                        }
9548
9549
                        // Replace with custom actions.
9550
                        if (!empty($myActions) && is_callable($myActions)) {
9551
                            $actions = $myActions($row);
9552
                        }
9553
9554
                        $currentRow = [
9555
                            $row['id'],
9556
                            $currentRow['title'],
9557
                            $currentRow['count_questions'],
9558
                            $actions,
9559
                        ];
9560
                    } else {
9561
                        $currentRow = [
9562
                            $currentRow['title'],
9563
                            $currentRow['attempt'],
9564
                        ];
9565
9566
                        if ($isDrhOfCourse) {
9567
                            $currentRow[] = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$row['id'].'">'.
9568
                                Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
9569
                        }
9570
9571
                        if ($returnData) {
9572
                            $currentRow['id'] = $exercise->id;
9573
                            $currentRow['url'] = $webPath.'exercise/overview.php?'
9574
                                .api_get_cidreq_params($courseInfo['code'], $sessionId).'&'
9575
                                ."$mylpid$mylpitemid&exerciseId={$row['id']}";
9576
                            $currentRow['name'] = $currentRow[0];
9577
                        }
9578
                    }
9579
9580
                    $tableRows[] = $currentRow;
9581
                }
9582
            }
9583
        }
9584
9585
        // end exercise list
9586
        // Hotpotatoes results
9587
        if ($is_allowedToEdit) {
9588
            $sql = "SELECT d.iid, d.path as path, d.comment as comment
9589
                    FROM $TBL_DOCUMENT d
9590
                    WHERE
9591
                        d.c_id = $courseId AND
9592
                        (d.path LIKE '%htm%') AND
9593
                        d.path  LIKE '".Database :: escape_string($uploadPath.'/%/%')."'
9594
                    LIMIT $from , $limit"; // only .htm or .html files listed
9595
        } else {
9596
            $sql = "SELECT d.iid, d.path as path, d.comment as comment
9597
                    FROM $TBL_DOCUMENT d
9598
                    WHERE
9599
                        d.c_id = $courseId AND
9600
                        (d.path LIKE '%htm%') AND
9601
                        d.path  LIKE '".Database :: escape_string($uploadPath.'/%/%')."'
9602
                    LIMIT $from , $limit";
9603
        }
9604
9605
        $result = Database::query($sql);
9606
        $attributes = [];
9607
        while ($row = Database::fetch_array($result, 'ASSOC')) {
9608
            $attributes[$row['iid']] = $row;
9609
        }
9610
9611
        $nbrActiveTests = 0;
9612
        if (!empty($attributes)) {
9613
            foreach ($attributes as $item) {
9614
                $id = $item['iid'];
9615
                $path = $item['path'];
9616
9617
                $title = GetQuizName($path, $documentPath);
9618
                if ($title == '') {
9619
                    $title = basename($path);
9620
                }
9621
9622
                // prof only
9623
                if ($is_allowedToEdit) {
9624
                    $visibility = api_get_item_visibility(
9625
                        ['real_id' => $courseId],
9626
                        TOOL_DOCUMENT,
9627
                        $id,
9628
                        0
9629
                    );
9630
9631
                    if (!empty($sessionId)) {
9632
                        if (0 == $visibility) {
9633
                            continue;
9634
                        }
9635
9636
                        $visibility = api_get_item_visibility(
9637
                            ['real_id' => $courseId],
9638
                            TOOL_DOCUMENT,
9639
                            $id,
9640
                            $sessionId
9641
                        );
9642
                    }
9643
9644
                    $title =
9645
                        implode(PHP_EOL, [
9646
                            Display::return_icon('hotpotatoes_s.png', 'HotPotatoes'),
9647
                            Display::url(
9648
                                $title,
9649
                                'showinframes.php?'.api_get_cidreq().'&'.http_build_query([
9650
                                    'file' => $path,
9651
                                    'uid' => $userId,
9652
                                ]),
9653
                                ['class' => $visibility == 0 ? 'text-muted' : null]
9654
                            ),
9655
                        ]);
9656
9657
                    $actions = Display::url(
9658
                        Display::return_icon(
9659
                            'edit.png',
9660
                            get_lang('Edit'),
9661
                            '',
9662
                            ICON_SIZE_SMALL
9663
                        ),
9664
                        'adminhp.php?'.api_get_cidreq().'&hotpotatoesName='.$path
9665
                    );
9666
9667
                    $actions .= '<a href="hotpotatoes_exercise_report.php?'.api_get_cidreq().'&path='.$path.'">'.
9668
                        Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).
9669
                        '</a>';
9670
9671
                    // if active
9672
                    if ($visibility != 0) {
9673
                        $nbrActiveTests = $nbrActiveTests + 1;
9674
                        $actions .= '      <a href="'.$exercisePath.'?'.api_get_cidreq().'&hpchoice=disable&file='.$path.'">'.
9675
                            Display::return_icon('visible.png', get_lang('Deactivate'), '', ICON_SIZE_SMALL).'</a>';
9676
                    } else { // else if not active
9677
                        $actions .= '    <a href="'.$exercisePath.'?'.api_get_cidreq().'&hpchoice=enable&file='.$path.'">'.
9678
                            Display::return_icon('invisible.png', get_lang('Activate'), '', ICON_SIZE_SMALL).'</a>';
9679
                    }
9680
                    $actions .= '<a href="'.$exercisePath.'?'.api_get_cidreq().'&hpchoice=delete&file='.$path.'" onclick="javascript:if(!confirm(\''.addslashes(api_htmlentities(get_lang('AreYouSureToDeleteJS'), ENT_QUOTES, $charset)).'\')) return false;">'.
9681
                        Display::return_icon('delete.png', get_lang('Delete'), '', ICON_SIZE_SMALL).'</a>';
9682
9683
                    $currentRow = [
9684
                        '',
9685
                        $title,
9686
                        '',
9687
                        $actions,
9688
                    ];
9689
                } else {
9690
                    $visibility = api_get_item_visibility(
9691
                        ['real_id' => $courseId],
9692
                        TOOL_DOCUMENT,
9693
                        $id,
9694
                        $sessionId
9695
                    );
9696
9697
                    if (0 == $visibility) {
9698
                        continue;
9699
                    }
9700
9701
                    // Student only
9702
                    $attempt = ExerciseLib::getLatestHotPotatoResult(
9703
                        $path,
9704
                        $userId,
9705
                        api_get_course_int_id(),
9706
                        api_get_session_id()
9707
                    );
9708
9709
                    $nbrActiveTests = $nbrActiveTests + 1;
9710
                    $title = Display::url(
9711
                        $title,
9712
                        'showinframes.php?'.api_get_cidreq().'&'.http_build_query(
9713
                            [
9714
                                'file' => $path,
9715
                                'cid' => api_get_course_id(),
9716
                                'uid' => $userId,
9717
                            ]
9718
                        )
9719
                    );
9720
9721
                    if (!empty($attempt)) {
9722
                        $actions = '<a href="hotpotatoes_exercise_report.php?'.api_get_cidreq().'&path='.$path.'&filter_by_user='.$userId.'">'.Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
9723
                        $attemptText = get_lang('LatestAttempt').' : ';
9724
                        $attemptText .= ExerciseLib::show_score(
9725
                                $attempt['exe_result'],
9726
                                $attempt['exe_weighting']
9727
                            ).' ';
9728
                        $attemptText .= $actions;
9729
                    } else {
9730
                        // No attempts.
9731
                        $attemptText = get_lang('NotAttempted').' ';
9732
                    }
9733
9734
                    $currentRow = [
9735
                        $title,
9736
                        $attemptText,
9737
                    ];
9738
9739
                    if ($isDrhOfCourse) {
9740
                        $currentRow[] = '<a href="hotpotatoes_exercise_report.php?'.api_get_cidreq().'&path='.$path.'">'.
9741
                            Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
9742
                    }
9743
                }
9744
9745
                $tableRows[] = $currentRow;
9746
            }
9747
        }
9748
9749
        if ($returnData) {
9750
            return $tableRows;
9751
        }
9752
9753
        if (empty($tableRows) && empty($categoryId)) {
9754
            if ($is_allowedToEdit && $origin !== 'learnpath') {
9755
                $content .= '<div id="no-data-view">';
9756
                $content .= '<h3>'.get_lang('Quiz').'</h3>';
9757
                $content .= Display::return_icon('quiz.png', '', [], 64);
9758
                $content .= '<div class="controls">';
9759
                $content .= Display::url(
9760
                    '<em class="fa fa-plus"></em> '.get_lang('NewEx'),
9761
                    'exercise_admin.php?'.api_get_cidreq(),
9762
                    ['class' => 'btn btn-primary']
9763
                );
9764
                $content .= '</div>';
9765
                $content .= '</div>';
9766
            }
9767
        } else {
9768
            if (empty($tableRows)) {
9769
                return '';
9770
            }
9771
            $table->setTableData($tableRows);
9772
            $table->setTotalNumberOfItems($total);
9773
            $table->set_additional_parameters([
9774
                'cidReq' => api_get_course_id(),
9775
                'id_session' => api_get_session_id(),
9776
                'category_id' => $categoryId,
9777
            ]);
9778
9779
            if ($is_allowedToEdit) {
9780
                $formActions = [];
9781
                $formActions['visible'] = get_lang('Activate');
9782
                $formActions['invisible'] = get_lang('Deactivate');
9783
                $formActions['delete'] = get_lang('Delete');
9784
                $table->set_form_actions($formActions);
9785
            }
9786
9787
            $i = 0;
9788
            if ($is_allowedToEdit) {
9789
                $table->set_header($i++, '', false, 'width="18px"');
9790
            }
9791
            $table->set_header($i++, get_lang('ExerciseName'), false);
9792
9793
            if ($is_allowedToEdit) {
9794
                $table->set_header($i++, get_lang('QuantityQuestions'), false);
9795
                $table->set_header($i++, get_lang('Actions'), false);
9796
            } else {
9797
                $table->set_header($i++, get_lang('Status'), false);
9798
                if ($isDrhOfCourse) {
9799
                    $table->set_header($i++, get_lang('Actions'), false);
9800
                }
9801
            }
9802
9803
            if ($returnTable) {
9804
                return $table;
9805
            }
9806
9807
            $content .= $table->return_table();
9808
        }
9809
9810
        return $content;
9811
    }
9812
9813
    /**
9814
     * @return int value in minutes
9815
     */
9816
    public function getResultAccess()
9817
    {
9818
        $extraFieldValue = new ExtraFieldValue('exercise');
9819
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9820
            $this->iId,
9821
            'results_available_for_x_minutes'
9822
        );
9823
9824
        if (!empty($value) && isset($value['value'])) {
9825
            return (int) $value['value'];
9826
        }
9827
9828
        return 0;
9829
    }
9830
9831
    /**
9832
     * @param array $exerciseResultInfo
9833
     *
9834
     * @return bool
9835
     */
9836
    public function getResultAccessTimeDiff($exerciseResultInfo)
9837
    {
9838
        $value = $this->getResultAccess();
9839
        if (!empty($value)) {
9840
            $endDate = new DateTime($exerciseResultInfo['exe_date'], new DateTimeZone('UTC'));
9841
            $endDate->add(new DateInterval('PT'.$value.'M'));
9842
            $now = time();
9843
            if ($endDate->getTimestamp() > $now) {
9844
                return (int) $endDate->getTimestamp() - $now;
9845
            }
9846
        }
9847
9848
        return 0;
9849
    }
9850
9851
    /**
9852
     * @param array $exerciseResultInfo
9853
     *
9854
     * @return bool
9855
     */
9856
    public function hasResultsAccess($exerciseResultInfo)
9857
    {
9858
        $diff = $this->getResultAccessTimeDiff($exerciseResultInfo);
9859
        if (0 === $diff) {
9860
            return false;
9861
        }
9862
9863
        return true;
9864
    }
9865
9866
    /**
9867
     * @return int
9868
     */
9869
    public function getResultsAccess()
9870
    {
9871
        $extraFieldValue = new ExtraFieldValue('exercise');
9872
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9873
            $this->iId,
9874
            'results_available_for_x_minutes'
9875
        );
9876
        if (!empty($value)) {
9877
            return (int) $value;
9878
        }
9879
9880
        return 0;
9881
    }
9882
9883
    /**
9884
     * @param int   $questionId
9885
     * @param bool  $show_results
9886
     * @param array $question_result
9887
     */
9888
    public function getDelineationResult(Question $objQuestionTmp, $questionId, $show_results, $question_result)
9889
    {
9890
        $id = (int) $objQuestionTmp->id;
9891
        $questionId = (int) $questionId;
9892
9893
        $final_overlap = $question_result['extra']['final_overlap'];
9894
        $final_missing = $question_result['extra']['final_missing'];
9895
        $final_excess = $question_result['extra']['final_excess'];
9896
9897
        $overlap_color = $question_result['extra']['overlap_color'];
9898
        $missing_color = $question_result['extra']['missing_color'];
9899
        $excess_color = $question_result['extra']['excess_color'];
9900
9901
        $threadhold1 = $question_result['extra']['threadhold1'];
9902
        $threadhold2 = $question_result['extra']['threadhold2'];
9903
        $threadhold3 = $question_result['extra']['threadhold3'];
9904
9905
        if ($show_results) {
9906
            if ($overlap_color) {
9907
                $overlap_color = 'green';
9908
            } else {
9909
                $overlap_color = 'red';
9910
            }
9911
9912
            if ($missing_color) {
9913
                $missing_color = 'green';
9914
            } else {
9915
                $missing_color = 'red';
9916
            }
9917
            if ($excess_color) {
9918
                $excess_color = 'green';
9919
            } else {
9920
                $excess_color = 'red';
9921
            }
9922
9923
            if (!is_numeric($final_overlap)) {
9924
                $final_overlap = 0;
9925
            }
9926
9927
            if (!is_numeric($final_missing)) {
9928
                $final_missing = 0;
9929
            }
9930
            if (!is_numeric($final_excess)) {
9931
                $final_excess = 0;
9932
            }
9933
9934
            if ($final_excess > 100) {
9935
                $final_excess = 100;
9936
            }
9937
9938
            $table_resume = '
9939
                    <table class="table table-hover table-striped data_table">
9940
                        <tr class="row_odd" >
9941
                            <td>&nbsp;</td>
9942
                            <td><b>'.get_lang('Requirements').'</b></td>
9943
                            <td><b>'.get_lang('YourAnswer').'</b></td>
9944
                        </tr>
9945
                        <tr class="row_even">
9946
                            <td><b>'.get_lang('Overlap').'</b></td>
9947
                            <td>'.get_lang('Min').' '.$threadhold1.'</td>
9948
                            <td>
9949
                                <div style="color:'.$overlap_color.'">
9950
                                    '.(($final_overlap < 0) ? 0 : intval($final_overlap)).'
9951
                                </div>
9952
                            </td>
9953
                        </tr>
9954
                        <tr>
9955
                            <td><b>'.get_lang('Excess').'</b></td>
9956
                            <td>'.get_lang('Max').' '.$threadhold2.'</td>
9957
                            <td>
9958
                                <div style="color:'.$excess_color.'">
9959
                                    '.(($final_excess < 0) ? 0 : intval($final_excess)).'
9960
                                </div>
9961
                            </td>
9962
                        </tr>
9963
                        <tr class="row_even">
9964
                            <td><b>'.get_lang('Missing').'</b></td>
9965
                            <td>'.get_lang('Max').' '.$threadhold3.'</td>
9966
                            <td>
9967
                                <div style="color:'.$missing_color.'">
9968
                                    '.(($final_missing < 0) ? 0 : intval($final_missing)).'
9969
                                </div>
9970
                            </td>
9971
                        </tr>
9972
                    </table>
9973
                ';
9974
9975
            $answerType = $objQuestionTmp->selectType();
9976
            if ($answerType != HOT_SPOT_DELINEATION) {
9977
                $item_list = explode('@@', $destination);
9978
                $try = $item_list[0];
9979
                $lp = $item_list[1];
9980
                $destinationid = $item_list[2];
9981
                $url = $item_list[3];
9982
                $table_resume = '';
9983
            } else {
9984
                if ($next == 0) {
9985
                    $try = $try_hotspot;
9986
                    $lp = $lp_hotspot;
9987
                    $destinationid = $select_question_hotspot;
9988
                    $url = $url_hotspot;
9989
                } else {
9990
                    //show if no error
9991
                    $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
9992
                    $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
9993
                }
9994
            }
9995
9996
            echo '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>';
9997
            if ($answerType == HOT_SPOT_DELINEATION) {
9998
                if ($organs_at_risk_hit > 0) {
9999
                    $message = '<br />'.get_lang('ResultIs').' <b>'.$result_comment.'</b><br />';
10000
                    $message .= '<p style="color:#DC0A0A;"><b>'.get_lang('OARHit').'</b></p>';
10001
                } else {
10002
                    $message = '<p>'.get_lang('YourDelineation').'</p>';
10003
                    $message .= $table_resume;
10004
                    $message .= '<br />'.get_lang('ResultIs').' <b>'.$result_comment.'</b><br />';
10005
                }
10006
                $message .= '<p>'.$comment.'</p>';
10007
                echo $message;
10008
            } else {
10009
                echo '<p>'.$comment.'</p>';
10010
            }
10011
10012
            // Showing the score
10013
            /*$queryfree = "SELECT marks FROM $TBL_TRACK_ATTEMPT
10014
                          WHERE exe_id = $id AND question_id =  $questionId";
10015
            $resfree = Database::query($queryfree);
10016
            $questionScore = Database::result($resfree, 0, 'marks');
10017
            $totalScore += $questionScore;*/
10018
            $relPath = api_get_path(REL_CODE_PATH);
10019
            echo '</table></td></tr>';
10020
            echo "
10021
                        <tr>
10022
                            <td colspan=\"2\">
10023
                                <div id=\"hotspot-solution\"></div>
10024
                                <script>
10025
                                    $(function() {
10026
                                        new HotspotQuestion({
10027
                                            questionId: $questionId,
10028
                                            exerciseId: {$this->id},
10029
                                            exeId: $id,
10030
                                            selector: '#hotspot-solution',
10031
                                            for: 'solution',
10032
                                            relPath: '$relPath'
10033
                                        });
10034
                                    });
10035
                                </script>
10036
                            </td>
10037
                        </tr>
10038
                    </table>
10039
                ";
10040
        }
10041
    }
10042
10043
    /**
10044
     * Clean exercise session variables.
10045
     */
10046
    public static function cleanSessionVariables()
10047
    {
10048
        Session::erase('objExercise');
10049
        Session::erase('exe_id');
10050
        Session::erase('calculatedAnswerId');
10051
        Session::erase('duration_time_previous');
10052
        Session::erase('duration_time');
10053
        Session::erase('objQuestion');
10054
        Session::erase('objAnswer');
10055
        Session::erase('questionList');
10056
        Session::erase('categoryList');
10057
        Session::erase('exerciseResult');
10058
        Session::erase('firstTime');
10059
        Session::erase('time_per_question');
10060
        Session::erase('question_start');
10061
        Session::erase('exerciseResultCoordinates');
10062
        Session::erase('hotspot_coord');
10063
        Session::erase('hotspot_dest');
10064
        Session::erase('hotspot_delineation_result');
10065
    }
10066
10067
    /**
10068
     * Get the first LP found matching the session ID.
10069
     *
10070
     * @param int $sessionId
10071
     *
10072
     * @return array
10073
     */
10074
    public function getLpBySession($sessionId)
10075
    {
10076
        if (!empty($this->lpList)) {
10077
            $sessionId = (int) $sessionId;
10078
10079
            foreach ($this->lpList as $lp) {
10080
                if ((int) $lp['session_id'] == $sessionId) {
10081
                    return $lp;
10082
                }
10083
            }
10084
10085
            return current($this->lpList);
10086
        }
10087
10088
        return [
10089
            'lp_id' => 0,
10090
            'max_score' => 0,
10091
            'session_id' => 0,
10092
        ];
10093
    }
10094
10095
    public static function saveExerciseInLp($safe_item_id, $safe_exe_id)
10096
    {
10097
        $lp = Session::read('oLP');
10098
10099
        $safe_exe_id = (int) $safe_exe_id;
10100
        $safe_item_id = (int) $safe_item_id;
10101
10102
        if (empty($lp) || empty($safe_exe_id) || empty($safe_item_id)) {
10103
            return false;
10104
        }
10105
10106
        $viewId = $lp->get_view_id();
10107
        $course_id = api_get_course_int_id();
10108
        $userId = (int) api_get_user_id();
10109
        $viewId = (int) $viewId;
10110
10111
        $TBL_TRACK_EXERCICES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
10112
        $TBL_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
10113
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
10114
10115
        $sql = "SELECT start_date, exe_date, exe_result, exe_weighting, exe_exo_id, exe_duration
10116
                FROM $TBL_TRACK_EXERCICES
10117
                WHERE exe_id = $safe_exe_id AND exe_user_id = $userId";
10118
        $res = Database::query($sql);
10119
        $row_dates = Database::fetch_array($res);
10120
10121
        if (empty($row_dates)) {
10122
            return false;
10123
        }
10124
10125
        $duration = (int) $row_dates['exe_duration'];
10126
        $score = (float) $row_dates['exe_result'];
10127
        $max_score = (float) $row_dates['exe_weighting'];
10128
10129
        $sql = "UPDATE $TBL_LP_ITEM SET
10130
                    max_score = '$max_score'
10131
                WHERE iid = $safe_item_id";
10132
        Database::query($sql);
10133
10134
        $sql = "SELECT id FROM $TBL_LP_ITEM_VIEW
10135
                WHERE
10136
                    c_id = $course_id AND
10137
                    lp_item_id = $safe_item_id AND
10138
                    lp_view_id = $viewId
10139
                ORDER BY id DESC
10140
                LIMIT 1";
10141
        $res_last_attempt = Database::query($sql);
10142
10143
        if (Database::num_rows($res_last_attempt) && !api_is_invitee()) {
10144
            $row_last_attempt = Database::fetch_row($res_last_attempt);
10145
            $lp_item_view_id = $row_last_attempt[0];
10146
10147
            $exercise = new Exercise($course_id);
10148
            $exercise->read($row_dates['exe_exo_id']);
10149
            $status = 'completed';
10150
10151
            if (!empty($exercise->pass_percentage)) {
10152
                $status = 'failed';
10153
                $success = ExerciseLib::isSuccessExerciseResult(
10154
                    $score,
10155
                    $max_score,
10156
                    $exercise->pass_percentage
10157
                );
10158
                if ($success) {
10159
                    $status = 'passed';
10160
                }
10161
            }
10162
10163
            $sql = "UPDATE $TBL_LP_ITEM_VIEW SET
10164
                        status = '$status',
10165
                        score = $score,
10166
                        total_time = $duration
10167
                    WHERE iid = $lp_item_view_id";
10168
            Database::query($sql);
10169
10170
            $sql = "UPDATE $TBL_TRACK_EXERCICES SET
10171
                        orig_lp_item_view_id = $lp_item_view_id
10172
                    WHERE exe_id = ".$safe_exe_id;
10173
            Database::query($sql);
10174
        }
10175
    }
10176
10177
    /**
10178
     * Get the user answers saved in exercise.
10179
     *
10180
     * @param int $attemptId
10181
     *
10182
     * @return array
10183
     */
10184
    public function getUserAnswersSavedInExercise($attemptId)
10185
    {
10186
        $exerciseResult = [];
10187
        $attemptList = Event::getAllExerciseEventByExeId($attemptId);
10188
10189
        foreach ($attemptList as $questionId => $options) {
10190
            foreach ($options as $option) {
10191
                $question = Question::read($option['question_id']);
10192
                if ($question) {
10193
                    switch ($question->type) {
10194
                        case FILL_IN_BLANKS:
10195
                            $option['answer'] = $this->fill_in_blank_answer_to_string($option['answer']);
10196
                            break;
10197
                    }
10198
                }
10199
10200
                if (!empty($option['answer'])) {
10201
                    $exerciseResult[] = $questionId;
10202
10203
                    break;
10204
                }
10205
            }
10206
        }
10207
10208
        return $exerciseResult;
10209
    }
10210
10211
    /**
10212
     * Get the number of user answers saved in exercise.
10213
     *
10214
     * @param int $attemptId
10215
     *
10216
     * @return int
10217
     */
10218
    public function countUserAnswersSavedInExercise($attemptId)
10219
    {
10220
        $answers = $this->getUserAnswersSavedInExercise($attemptId);
10221
10222
        return count($answers);
10223
    }
10224
10225
    public static function allowAction($action)
10226
    {
10227
        if (api_is_platform_admin()) {
10228
            return true;
10229
        }
10230
10231
        $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
10232
        $disableClean = api_get_configuration_value('disable_clean_exercise_results_for_teachers');
10233
10234
        switch ($action) {
10235
            case 'delete':
10236
                if (api_is_allowed_to_edit(null, true)) {
10237
                    if ($limitTeacherAccess) {
10238
                        return false;
10239
                    }
10240
10241
                    return true;
10242
                }
10243
                break;
10244
            case 'clean_results':
10245
                if (api_is_allowed_to_edit(null, true)) {
10246
                    if ($limitTeacherAccess) {
10247
                        return false;
10248
                    }
10249
10250
                    if ($disableClean) {
10251
                        return false;
10252
                    }
10253
10254
                    return true;
10255
                }
10256
10257
                break;
10258
        }
10259
10260
        return false;
10261
    }
10262
10263
    public static function getLpListFromExercise($exerciseId, $courseId)
10264
    {
10265
        $tableLpItem = Database::get_course_table(TABLE_LP_ITEM);
10266
        $tblLp = Database::get_course_table(TABLE_LP_MAIN);
10267
10268
        $exerciseId = (int) $exerciseId;
10269
        $courseId = (int) $courseId;
10270
10271
        $sql = "SELECT
10272
                    lp.name,
10273
                    lpi.lp_id,
10274
                    lpi.max_score,
10275
                    lp.session_id
10276
                FROM $tableLpItem lpi
10277
                INNER JOIN $tblLp lp
10278
                ON (lpi.lp_id = lp.iid AND lpi.c_id = lp.c_id)
10279
                WHERE
10280
                    lpi.c_id = $courseId AND
10281
                    lpi.item_type = '".TOOL_QUIZ."' AND
10282
                    lpi.path = '$exerciseId'";
10283
        $result = Database::query($sql);
10284
        $lpList = [];
10285
        if (Database::num_rows($result) > 0) {
10286
            $lpList = Database::store_result($result, 'ASSOC');
10287
        }
10288
10289
        return $lpList;
10290
    }
10291
10292
    public function getReminderTable($questionList, $exercise_stat_info)
10293
    {
10294
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10295
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10296
        $learnpath_item_view_id = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10297
10298
        if (empty($exercise_stat_info)) {
10299
            return '';
10300
        }
10301
10302
        $remindList = $exercise_stat_info['questions_to_check'];
10303
        $remindList = explode(',', $remindList);
10304
10305
        $exeId = $exercise_stat_info['exe_id'];
10306
        $exerciseId = $exercise_stat_info['exe_exo_id'];
10307
        $exercise_result = $this->getUserAnswersSavedInExercise($exeId);
10308
10309
        $content = Display::label(get_lang('QuestionWithNoAnswer'), 'danger');
10310
        $content .= '<div class="clear"></div><br />';
10311
        $table = '';
10312
        $counter = 0;
10313
        // Loop over all question to show results for each of them, one by one
10314
        foreach ($questionList as $questionId) {
10315
            $objQuestionTmp = Question::read($questionId);
10316
            $check_id = 'remind_list['.$questionId.']';
10317
            $attributes = ['id' => $check_id, 'onclick' => "save_remind_item(this, '$questionId');"];
10318
            if (in_array($questionId, $remindList)) {
10319
                $attributes['checked'] = 1;
10320
            }
10321
10322
            $checkbox = Display::input('checkbox', 'remind_list['.$questionId.']', '', $attributes);
10323
            $checkbox = '<div class="pretty p-svg p-curve">
10324
                        '.$checkbox.'
10325
                        <div class="state p-primary ">
10326
                         <svg class="svg svg-icon" viewBox="0 0 20 20">
10327
                            <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>
10328
                         </svg>
10329
                         <label>&nbsp;</label>
10330
                        </div>
10331
                    </div>';
10332
            $counter++;
10333
            $questionTitle = $counter.'. '.strip_tags($objQuestionTmp->selectTitle());
10334
            // Check if the question doesn't have an answer.
10335
            if (!in_array($questionId, $exercise_result)) {
10336
                $questionTitle = Display::label($questionTitle, 'danger');
10337
            }
10338
10339
            $label_attributes = [];
10340
            $label_attributes['for'] = $check_id;
10341
            $questionTitle = Display::tag('label', $checkbox.$questionTitle, $label_attributes);
10342
            $table .= Display::div($questionTitle, ['class' => 'exercise_reminder_item ']);
10343
        }
10344
10345
        $content .= Display::div('', ['id' => 'message']).
10346
                    Display::div($table, ['class' => 'question-check-test']);
10347
10348
        $content .= '<script>
10349
        var lp_data = $.param({
10350
            "learnpath_id": '.$learnpath_id.',
10351
            "learnpath_item_id" : '.$learnpath_item_id.',
10352
            "learnpath_item_view_id": '.$learnpath_item_view_id.'
10353
        });
10354
10355
        function final_submit() {
10356
            // Normal inputs.
10357
            window.location = "'.api_get_path(WEB_CODE_PATH).'exercise/exercise_result.php?'.api_get_cidreq().'&exe_id='.$exeId.'&" + lp_data;
10358
        }
10359
10360
        function changeOptionStatus(status)
10361
        {
10362
            $("input[type=checkbox]").each(function () {
10363
                $(this).prop("checked", status);
10364
            });
10365
10366
            var action = "";
10367
            var extraOption = "remove_all";
10368
            if (status == 1) {
10369
                extraOption = "add_all";
10370
            }
10371
            $.ajax({
10372
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10373
                data: "option="+extraOption+"&exe_id='.$exeId.'&action="+action,
10374
                success: function(returnValue) {
10375
                }
10376
            });
10377
        }
10378
10379
        function review_questions() {
10380
            var isChecked = 1;
10381
            $("input[type=checkbox]").each(function () {
10382
                if ($(this).prop("checked")) {
10383
                    isChecked = 2;
10384
                    return false;
10385
                }
10386
            });
10387
10388
            if (isChecked == 1) {
10389
                $("#message").addClass("warning-message");
10390
                $("#message").html("'.addslashes(get_lang('SelectAQuestionToReview')).'");
10391
            } else {
10392
                window.location = "exercise_submit.php?'.api_get_cidreq().'&exerciseId='.$exerciseId.'&reminder=2&" + lp_data;
10393
            }
10394
        }
10395
10396
        function save_remind_item(obj, question_id) {
10397
            var action = "";
10398
            if ($(obj).prop("checked")) {
10399
                action = "add";
10400
            } else {
10401
                action = "delete";
10402
            }
10403
            $.ajax({
10404
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10405
                data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
10406
                success: function(returnValue) {
10407
                }
10408
            });
10409
        }
10410
        </script>';
10411
10412
        return $content;
10413
    }
10414
10415
    /**
10416
     * Get number of questions in exercise by user attempt.
10417
     *
10418
     * @return int
10419
     */
10420
    private function countQuestionsInExercise()
10421
    {
10422
        $lpId = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10423
        $lpItemId = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10424
        $lpItemViewId = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10425
10426
        $trackInfo = $this->get_stat_track_exercise_info($lpId, $lpItemId, $lpItemViewId);
10427
10428
        if (!empty($trackInfo)) {
10429
            $questionIds = explode(',', $trackInfo['data_tracking']);
10430
10431
            return count($questionIds);
10432
        }
10433
10434
        return $this->getQuestionCount();
10435
    }
10436
10437
    /**
10438
     * Gets the question list ordered by the question_order setting (drag and drop).
10439
     *
10440
     * @param bool $adminView Optional.
10441
     *
10442
     * @return array
10443
     */
10444
    private function getQuestionOrderedList($adminView = false)
10445
    {
10446
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
10447
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
10448
10449
        // Getting question_order to verify that the question
10450
        // list is correct and all question_order's were set
10451
        $sql = "SELECT DISTINCT count(e.question_order) as count
10452
                FROM $TBL_EXERCICE_QUESTION e
10453
                INNER JOIN $TBL_QUESTIONS q
10454
                ON (e.question_id = q.id AND e.c_id = q.c_id)
10455
                WHERE
10456
                  e.c_id = {$this->course_id} AND
10457
                  e.exercice_id	= ".$this->id;
10458
10459
        $result = Database::query($sql);
10460
        $row = Database::fetch_array($result);
10461
        $count_question_orders = $row['count'];
10462
10463
        // Getting question list from the order (question list drag n drop interface).
10464
        $sql = "SELECT DISTINCT e.question_id, e.question_order
10465
                FROM $TBL_EXERCICE_QUESTION e
10466
                INNER JOIN $TBL_QUESTIONS q
10467
                ON (e.question_id = q.id AND e.c_id = q.c_id)
10468
                WHERE
10469
                    e.c_id = {$this->course_id} AND
10470
                    e.exercice_id = '".$this->id."'
10471
                ORDER BY question_order";
10472
        $result = Database::query($sql);
10473
10474
        // Fills the array with the question ID for this exercise
10475
        // the key of the array is the question position
10476
        $temp_question_list = [];
10477
        $counter = 1;
10478
        $questionList = [];
10479
        while ($new_object = Database::fetch_object($result)) {
10480
            if (!$adminView) {
10481
                // Correct order.
10482
                $questionList[$new_object->question_order] = $new_object->question_id;
10483
            } else {
10484
                $questionList[$counter] = $new_object->question_id;
10485
            }
10486
10487
            // Just in case we save the order in other array
10488
            $temp_question_list[$counter] = $new_object->question_id;
10489
            $counter++;
10490
        }
10491
10492
        if (!empty($temp_question_list)) {
10493
            /* If both array don't match it means that question_order was not correctly set
10494
               for all questions using the default mysql order */
10495
            if (count($temp_question_list) != $count_question_orders) {
10496
                $questionList = $temp_question_list;
10497
            }
10498
        }
10499
10500
        return $questionList;
10501
    }
10502
10503
    /**
10504
     * Select N values from the questions per category array.
10505
     *
10506
     * @param array $categoriesAddedInExercise
10507
     * @param array $question_list
10508
     * @param array $questions_by_category
10509
     * @param bool  $flatResult
10510
     * @param bool  $randomizeQuestions
10511
     * @param array $questionsByCategoryMandatory
10512
     *
10513
     * @return array
10514
     */
10515
    private function pickQuestionsPerCategory(
10516
        $categoriesAddedInExercise,
10517
        $question_list,
10518
        &$questions_by_category,
10519
        $flatResult = true,
10520
        $randomizeQuestions = false,
10521
        $questionsByCategoryMandatory = []
10522
    ) {
10523
        $addAll = true;
10524
        $categoryCountArray = [];
10525
10526
        // Getting how many questions will be selected per category.
10527
        if (!empty($categoriesAddedInExercise)) {
10528
            $addAll = false;
10529
            // Parsing question according the category rel exercise settings
10530
            foreach ($categoriesAddedInExercise as $category_info) {
10531
                $category_id = $category_info['category_id'];
10532
                if (isset($questions_by_category[$category_id])) {
10533
                    // How many question will be picked from this category.
10534
                    $count = $category_info['count_questions'];
10535
                    // -1 means all questions
10536
                    $categoryCountArray[$category_id] = $count;
10537
                    if (-1 == $count) {
10538
                        $categoryCountArray[$category_id] = 999;
10539
                    }
10540
                }
10541
            }
10542
        }
10543
10544
        if (!empty($questions_by_category)) {
10545
            $temp_question_list = [];
10546
            foreach ($questions_by_category as $category_id => &$categoryQuestionList) {
10547
                if (isset($categoryCountArray) && !empty($categoryCountArray)) {
10548
                    $numberOfQuestions = 0;
10549
                    if (isset($categoryCountArray[$category_id])) {
10550
                        $numberOfQuestions = $categoryCountArray[$category_id];
10551
                    }
10552
                }
10553
10554
                if ($addAll) {
10555
                    $numberOfQuestions = 999;
10556
                }
10557
                if (!empty($numberOfQuestions)) {
10558
                    $mandatoryQuestions = [];
10559
                    if (isset($questionsByCategoryMandatory[$category_id])) {
10560
                        $mandatoryQuestions = $questionsByCategoryMandatory[$category_id];
10561
                    }
10562
10563
                    $elements = TestCategory::getNElementsFromArray(
10564
                        $categoryQuestionList,
10565
                        $numberOfQuestions,
10566
                        $randomizeQuestions,
10567
                        $mandatoryQuestions
10568
                    );
10569
                    if (!empty($elements)) {
10570
                        $temp_question_list[$category_id] = $elements;
10571
                        $categoryQuestionList = $elements;
10572
                    }
10573
                }
10574
            }
10575
10576
            if (!empty($temp_question_list)) {
10577
                if ($flatResult) {
10578
                    $temp_question_list = array_flatten($temp_question_list);
10579
                }
10580
                $question_list = $temp_question_list;
10581
            }
10582
        }
10583
10584
        return $question_list;
10585
    }
10586
10587
    /**
10588
     * Changes the exercise id.
10589
     *
10590
     * @param int $id - exercise id
10591
     */
10592
    private function updateId($id)
10593
    {
10594
        $this->id = $id;
10595
    }
10596
10597
    /**
10598
     * Sends a notification when a user ends an examn.
10599
     *
10600
     * @param array  $question_list_answers
10601
     * @param string $origin
10602
     * @param array  $user_info
10603
     * @param string $url_email
10604
     * @param array  $teachers
10605
     */
10606
    private function sendNotificationForOpenQuestions(
10607
        $question_list_answers,
10608
        $origin,
10609
        $user_info,
10610
        $url_email,
10611
        $teachers
10612
    ) {
10613
        // Email configuration settings
10614
        $courseCode = api_get_course_id();
10615
        $courseInfo = api_get_course_info($courseCode);
10616
        $sessionId = api_get_session_id();
10617
        $sessionData = '';
10618
        if (!empty($sessionId)) {
10619
            $sessionInfo = api_get_session_info($sessionId);
10620
            if (!empty($sessionInfo)) {
10621
                $sessionData = '<tr>'
10622
                    .'<td><em>'.get_lang('SessionName').'</em></td>'
10623
                    .'<td>&nbsp;<b>'.$sessionInfo['name'].'</b></td>'
10624
                    .'</tr>';
10625
            }
10626
        }
10627
10628
        $msg = get_lang('OpenQuestionsAttempted').'<br /><br />'
10629
            .get_lang('AttemptDetails').' : <br /><br />'
10630
            .'<table>'
10631
            .'<tr>'
10632
            .'<td><em>'.get_lang('CourseName').'</em></td>'
10633
            .'<td>&nbsp;<b>#course#</b></td>'
10634
            .'</tr>'
10635
            .$sessionData
10636
            .'<tr>'
10637
            .'<td>'.get_lang('TestAttempted').'</td>'
10638
            .'<td>&nbsp;#exercise#</td>'
10639
            .'</tr>'
10640
            .'<tr>'
10641
            .'<td>'.get_lang('StudentName').'</td>'
10642
            .'<td>&nbsp;#firstName# #lastName#</td>'
10643
            .'</tr>'
10644
            .'<tr>'
10645
            .'<td>'.get_lang('StudentEmail').'</td>'
10646
            .'<td>&nbsp;#mail#</td>'
10647
            .'</tr>'
10648
            .'</table>';
10649
10650
        $open_question_list = null;
10651
        foreach ($question_list_answers as $item) {
10652
            $question = $item['question'];
10653
            $answer = $item['answer'];
10654
            $answer_type = $item['answer_type'];
10655
            if (!empty($question) && !empty($answer) && $answer_type == FREE_ANSWER) {
10656
                $open_question_list .=
10657
                    '<tr>'
10658
                    .'<td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>'
10659
                    .'<td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>'
10660
                    .'</tr>'
10661
                    .'<tr>'
10662
                    .'<td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>'
10663
                    .'<td valign="top" bgcolor="#F3F3F3">'.$answer.'</td>'
10664
                    .'</tr>';
10665
            }
10666
        }
10667
10668
        if (!empty($open_question_list)) {
10669
            $msg .= '<p><br />'.get_lang('OpenQuestionsAttemptedAre').' :</p>'.
10670
                '<table width="730" height="136" border="0" cellpadding="3" cellspacing="3">';
10671
            $msg .= $open_question_list;
10672
            $msg .= '</table><br />';
10673
10674
            $msg = str_replace('#exercise#', $this->exercise, $msg);
10675
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg);
10676
            $msg = str_replace('#lastName#', $user_info['lastname'], $msg);
10677
            $msg = str_replace('#mail#', $user_info['email'], $msg);
10678
            $msg = str_replace(
10679
                '#course#',
10680
                Display::url($courseInfo['title'], $courseInfo['course_public_url'].'?id_session='.$sessionId),
10681
                $msg
10682
            );
10683
10684
            if ($origin != 'learnpath') {
10685
                $msg .= '<br /><a href="#url#">'.get_lang('ClickToCommentAndGiveFeedback').'</a>';
10686
            }
10687
            $msg = str_replace('#url#', $url_email, $msg);
10688
            $subject = get_lang('OpenQuestionsAttempted');
10689
10690
            if (!empty($teachers)) {
10691
                foreach ($teachers as $user_id => $teacher_data) {
10692
                    MessageManager::send_message_simple(
10693
                        $user_id,
10694
                        $subject,
10695
                        $msg
10696
                    );
10697
                }
10698
            }
10699
        }
10700
    }
10701
10702
    /**
10703
     * Send notification for oral questions.
10704
     *
10705
     * @param array  $question_list_answers
10706
     * @param string $origin
10707
     * @param int    $exe_id
10708
     * @param array  $user_info
10709
     * @param string $url_email
10710
     * @param array  $teachers
10711
     */
10712
    private function sendNotificationForOralQuestions(
10713
        $question_list_answers,
10714
        $origin,
10715
        $exe_id,
10716
        $user_info,
10717
        $url_email,
10718
        $teachers
10719
    ) {
10720
        // Email configuration settings
10721
        $courseCode = api_get_course_id();
10722
        $courseInfo = api_get_course_info($courseCode);
10723
        $oral_question_list = null;
10724
        foreach ($question_list_answers as $item) {
10725
            $question = $item['question'];
10726
            $file = $item['generated_oral_file'];
10727
            $answer = $item['answer'];
10728
            if (0 == $answer) {
10729
                $answer = '';
10730
            }
10731
            $answer_type = $item['answer_type'];
10732
            if (!empty($question) && (!empty($answer) || !empty($file)) && $answer_type == ORAL_EXPRESSION) {
10733
                if (!empty($file)) {
10734
                    $file = Display::url($file, $file);
10735
                }
10736
                $oral_question_list .= '<br />
10737
                    <table width="730" height="136" border="0" cellpadding="3" cellspacing="3">
10738
                    <tr>
10739
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
10740
                        <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
10741
                    </tr>
10742
                    <tr>
10743
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
10744
                        <td valign="top" bgcolor="#F3F3F3">'.$answer.$file.'</td>
10745
                    </tr></table>';
10746
            }
10747
        }
10748
10749
        if (!empty($oral_question_list)) {
10750
            $msg = get_lang('OralQuestionsAttempted').'<br /><br />
10751
                    '.get_lang('AttemptDetails').' : <br /><br />
10752
                    <table>
10753
                        <tr>
10754
                            <td><em>'.get_lang('CourseName').'</em></td>
10755
                            <td>&nbsp;<b>#course#</b></td>
10756
                        </tr>
10757
                        <tr>
10758
                            <td>'.get_lang('TestAttempted').'</td>
10759
                            <td>&nbsp;#exercise#</td>
10760
                        </tr>
10761
                        <tr>
10762
                            <td>'.get_lang('StudentName').'</td>
10763
                            <td>&nbsp;#firstName# #lastName#</td>
10764
                        </tr>
10765
                        <tr>
10766
                            <td>'.get_lang('StudentEmail').'</td>
10767
                            <td>&nbsp;#mail#</td>
10768
                        </tr>
10769
                    </table>';
10770
            $msg .= '<br />'.sprintf(get_lang('OralQuestionsAttemptedAreX'), $oral_question_list).'<br />';
10771
            $msg1 = str_replace("#exercise#", $this->exercise, $msg);
10772
            $msg = str_replace("#firstName#", $user_info['firstname'], $msg1);
10773
            $msg1 = str_replace("#lastName#", $user_info['lastname'], $msg);
10774
            $msg = str_replace("#mail#", $user_info['email'], $msg1);
10775
            $msg = str_replace("#course#", $courseInfo['name'], $msg1);
10776
10777
            if (!in_array($origin, ['learnpath', 'embeddable'])) {
10778
                $msg .= '<br /><a href="#url#">'.get_lang('ClickToCommentAndGiveFeedback').'</a>';
10779
            }
10780
            $msg1 = str_replace("#url#", $url_email, $msg);
10781
            $mail_content = $msg1;
10782
            $subject = get_lang('OralQuestionsAttempted');
10783
10784
            if (!empty($teachers)) {
10785
                foreach ($teachers as $user_id => $teacher_data) {
10786
                    MessageManager::send_message_simple(
10787
                        $user_id,
10788
                        $subject,
10789
                        $mail_content
10790
                    );
10791
                }
10792
            }
10793
        }
10794
    }
10795
10796
    /**
10797
     * Returns an array with the media list.
10798
     *
10799
     * @param array question list
10800
     *
10801
     * @example there's 1 question with iid 5 that belongs to the media question with iid = 100
10802
     * <code>
10803
     * array (size=2)
10804
     *  999 =>
10805
     *    array (size=3)
10806
     *      0 => int 7
10807
     *      1 => int 6
10808
     *      2 => int 3254
10809
     *  100 =>
10810
     *   array (size=1)
10811
     *      0 => int 5
10812
     *  </code>
10813
     */
10814
    private function setMediaList($questionList)
10815
    {
10816
        $mediaList = [];
10817
        /*
10818
         * Media feature is not activated in 1.11.x
10819
        if (!empty($questionList)) {
10820
            foreach ($questionList as $questionId) {
10821
                $objQuestionTmp = Question::read($questionId, $this->course_id);
10822
                // If a media question exists
10823
                if (isset($objQuestionTmp->parent_id) && $objQuestionTmp->parent_id != 0) {
10824
                    $mediaList[$objQuestionTmp->parent_id][] = $objQuestionTmp->id;
10825
                } else {
10826
                    // Always the last item
10827
                    $mediaList[999][] = $objQuestionTmp->id;
10828
                }
10829
            }
10830
        }*/
10831
10832
        $this->mediaList = $mediaList;
10833
    }
10834
10835
    /**
10836
     * @return HTML_QuickForm_group
10837
     */
10838
    private function setResultDisabledGroup(FormValidator $form)
10839
    {
10840
        $resultDisabledGroup = [];
10841
10842
        $resultDisabledGroup[] = $form->createElement(
10843
            'radio',
10844
            'results_disabled',
10845
            null,
10846
            get_lang('ShowScoreAndRightAnswer'),
10847
            '0',
10848
            ['id' => 'result_disabled_0']
10849
        );
10850
10851
        $resultDisabledGroup[] = $form->createElement(
10852
            'radio',
10853
            'results_disabled',
10854
            null,
10855
            get_lang('DoNotShowScoreNorRightAnswer'),
10856
            '1',
10857
            ['id' => 'result_disabled_1', 'onclick' => 'check_results_disabled()']
10858
        );
10859
10860
        $resultDisabledGroup[] = $form->createElement(
10861
            'radio',
10862
            'results_disabled',
10863
            null,
10864
            get_lang('OnlyShowScore'),
10865
            '2',
10866
            ['id' => 'result_disabled_2', 'onclick' => 'check_results_disabled()']
10867
        );
10868
10869
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
10870
            return $form->addGroup(
10871
                $resultDisabledGroup,
10872
                null,
10873
                get_lang('ShowResultsToStudents')
10874
            );
10875
        }
10876
10877
        $resultDisabledGroup[] = $form->createElement(
10878
            'radio',
10879
            'results_disabled',
10880
            null,
10881
            get_lang('ShowScoreEveryAttemptShowAnswersLastAttempt'),
10882
            '4',
10883
            ['id' => 'result_disabled_4']
10884
        );
10885
10886
        $resultDisabledGroup[] = $form->createElement(
10887
            'radio',
10888
            'results_disabled',
10889
            null,
10890
            get_lang('DontShowScoreOnlyWhenUserFinishesAllAttemptsButShowFeedbackEachAttempt'),
10891
            '5',
10892
            ['id' => 'result_disabled_5', 'onclick' => 'check_results_disabled()']
10893
        );
10894
10895
        $resultDisabledGroup[] = $form->createElement(
10896
            'radio',
10897
            'results_disabled',
10898
            null,
10899
            get_lang('ExerciseRankingMode'),
10900
            RESULT_DISABLE_RANKING,
10901
            ['id' => 'result_disabled_6']
10902
        );
10903
10904
        $resultDisabledGroup[] = $form->createElement(
10905
            'radio',
10906
            'results_disabled',
10907
            null,
10908
            get_lang('ExerciseShowOnlyGlobalScoreAndCorrectAnswers'),
10909
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
10910
            ['id' => 'result_disabled_7']
10911
        );
10912
10913
        $resultDisabledGroup[] = $form->createElement(
10914
            'radio',
10915
            'results_disabled',
10916
            null,
10917
            get_lang('ExerciseAutoEvaluationAndRankingMode'),
10918
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
10919
            ['id' => 'result_disabled_8']
10920
        );
10921
10922
        $resultDisabledGroup[] = $form->createElement(
10923
            'radio',
10924
            'results_disabled',
10925
            null,
10926
            get_lang('ExerciseCategoriesRadarMode'),
10927
            RESULT_DISABLE_RADAR,
10928
            ['id' => 'result_disabled_9']
10929
        );
10930
10931
        return $form->addGroup(
10932
            $resultDisabledGroup,
10933
            null,
10934
            get_lang('ShowResultsToStudents')
10935
        );
10936
    }
10937
10938
    public function getRadarsFromUsers($userList, $exercises, $courseId, $sessionId)
10939
    {
10940
        $dataSet = [];
10941
        $labels = [];
10942
        /** @var Exercise $exercise */
10943
        foreach ($exercises as $exercise) {
10944
            if (empty($labels)) {
10945
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10946
                $labels = array_column($categoryNameList, 'title');
10947
            }
10948
10949
            foreach ($userList as $userId) {
10950
                $results = Event::getExerciseResultsByUser(
10951
                    $userId,
10952
                    $exercise->iId,
10953
                    $courseId,
10954
                    $sessionId
10955
                );
10956
10957
                if ($results) {
10958
                    $firstAttempt = current($results);
10959
                    $exeId = $firstAttempt['exe_id'];
10960
10961
                    ob_start();
10962
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10963
                        $exercise,
10964
                        $exeId,
10965
                        false
10966
                    );
10967
                    ob_end_clean();
10968
10969
                    $categoryList = $stats['category_list'];
10970
                    $resultsArray = [];
10971
                    foreach ($categoryList as $category_id => $category_item) {
10972
                        $resultsArray[] = round($category_item['score'] / $category_item['total'] * 10);
10973
                    }
10974
                    $dataSet[] = $resultsArray;
10975
                }
10976
            }
10977
        }
10978
10979
        return $this->getRadar($labels, $dataSet);
10980
    }
10981
10982
    public function getRadar($labels, $dataSet)
10983
    {
10984
        if (empty($labels) || empty($dataSet)) {
10985
            return '';
10986
        }
10987
10988
        $labels = json_encode($labels);
10989
10990
        // Default preset, after that colors are generated randomly. @todo improve colors. Use a js lib?
10991
        $colorList = [
10992
            'rgb(255, 99, 132, 1.0)',
10993
            'rgb(0,0,200,1.0)', // red
10994
            'rgb(255, 159, 64, 1.0)', // orange
10995
            'rgb(255, 205, 86, 1.0)', //yellow
10996
            'rgb(75, 192, 192, 1.0)', // green
10997
            'rgb(54, 162, 235, 1.0)', // blue
10998
            'rgb(153, 102, 255, 1.0)', // purple
10999
            //'rgb(201, 203, 207)' grey
11000
        ];
11001
11002
        $dataSetToJson = [];
11003
        $counter = 0;
11004
        foreach ($dataSet as $resultsArray) {
11005
            $color = isset($colorList[$counter]) ? $colorList[$counter] : 'rgb('.rand(0,255).', '.rand(0,255).', '.rand(0,255).', 1.0)';
11006
11007
            $background = str_replace('1.0', '0.2', $color);
11008
            $dataSetToJson[] = [
11009
                'fill' => true,
11010
                //'label' =>  '".get_lang('Categories')."',
11011
                'backgroundColor' => $background,
11012
                'borderColor' => $color,
11013
                'pointBackgroundColor' => $color,
11014
                'pointBorderColor' => '#fff',
11015
                'pointHoverBackgroundColor' => '#fff',
11016
                'pointHoverBorderColor' => $color,
11017
                'pointRadius' => 6,
11018
                'pointBorderWidth' => 3,
11019
                'pointHoverRadius' => 10,
11020
                'data' => $resultsArray,
11021
            ];
11022
            $counter++;
11023
        }
11024
        $resultsToJson = json_encode($dataSetToJson);
11025
11026
        return "
11027
                <canvas id='categoryRadar' width='400' height='200'></canvas>
11028
                <script>
11029
                    var data = {
11030
                        labels: $labels,
11031
                        datasets: $resultsToJson
11032
                    }
11033
                    var options = {
11034
                        scale: {
11035
                            angleLines: {
11036
                                display: false
11037
                            },
11038
                            ticks: {
11039
                                beginAtZero: true,
11040
                                  min: 0,
11041
                                  max: 10,
11042
                                  stepSize: 1
11043
                            },
11044
                            pointLabels: {
11045
                              fontSize: 14,
11046
                              //fontStyle: 'bold'
11047
                            },
11048
                        },
11049
                        elements: {
11050
                            line: {
11051
                                tension: 0,
11052
                                borderWidth: 3
11053
                            }
11054
                        },
11055
                        legend: {
11056
                            //position: 'bottom'
11057
                            display: false
11058
                        }
11059
                    };
11060
                    var ctx = document.getElementById('categoryRadar').getContext('2d');
11061
                    var myRadarChart = new Chart(ctx, {
11062
                        type: 'radar',
11063
                        data: data,
11064
                        options: options
11065
                    });
11066
                </script>
11067
                ";
11068
    }
11069
}
11070