Passed
Push — 1.11.x ( c223dc...9a41bf )
by Julito
14:30
created

Exercise::addAllQuestionToRemind()   A

Complexity

Conditions 2

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 11
nop 2
dl 0
loc 17
rs 9.9
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-div-after-closing'] = "question_div_$question_id";
3298
                $params['data-block-closing'] = 'true';
3299
                $params['class'] .= ' no-header ';
3300
            }
3301
3302
            $html .= Display::url($urlTitle, $url, $params);
3303
            $html .= '<br />';
3304
3305
            return $html;
3306
        }
3307
3308
        if (!api_is_allowed_to_session_edit()) {
3309
            return '';
3310
        }
3311
3312
        $isReviewingAnswers = isset($_REQUEST['reminder']) && 2 == $_REQUEST['reminder'];
3313
3314
        // User
3315
        $endReminderValue = false;
3316
        if (!empty($myRemindList) && $isReviewingAnswers) {
3317
            $endValue = end($myRemindList);
3318
            if ($endValue == $question_id) {
3319
                $endReminderValue = true;
3320
            }
3321
        }
3322
        if ($this->type == ALL_ON_ONE_PAGE || $nbrQuestions == $questionNum || $endReminderValue) {
3323
            if ($this->review_answers) {
3324
                $label = get_lang('ReviewQuestions');
3325
                $class = 'btn btn-success';
3326
            } else {
3327
                $label = get_lang('EndTest');
3328
                $class = 'btn btn-warning';
3329
            }
3330
        } else {
3331
            $label = get_lang('NextQuestion');
3332
            $class = 'btn btn-primary';
3333
        }
3334
        // used to select it with jquery
3335
        $class .= ' question-validate-btn';
3336
        if ($this->type == ONE_PER_PAGE) {
3337
            if ($questionNum != 1 && $this->showPreviousButton()) {
3338
                $prev_question = $questionNum - 2;
3339
                $showPreview = true;
3340
                if (!empty($myRemindList) && $isReviewingAnswers) {
3341
                    $beforeId = null;
3342
                    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...
3343
                        if (isset($myRemindList[$i]) && $myRemindList[$i] == $question_id) {
3344
                            $beforeId = isset($myRemindList[$i - 1]) ? $myRemindList[$i - 1] : null;
3345
                            break;
3346
                        }
3347
                    }
3348
3349
                    if (empty($beforeId)) {
3350
                        $showPreview = false;
3351
                    } else {
3352
                        $num = 0;
3353
                        foreach ($this->questionList as $originalQuestionId) {
3354
                            if ($originalQuestionId == $beforeId) {
3355
                                break;
3356
                            }
3357
                            $num++;
3358
                        }
3359
                        $prev_question = $num;
3360
                    }
3361
                }
3362
3363
                if ($showPreviousButton && $showPreview && 0 === $this->getPreventBackwards()) {
3364
                    $buttonList[] = Display::button(
3365
                        'previous_question_and_save',
3366
                        get_lang('PreviousQuestion'),
3367
                        [
3368
                            'type' => 'button',
3369
                            'class' => 'btn btn-default',
3370
                            'data-prev' => $prev_question,
3371
                            'data-question' => $question_id,
3372
                        ]
3373
                    );
3374
                }
3375
            }
3376
3377
            // Next question
3378
            if (!empty($questions_in_media)) {
3379
                $buttonList[] = Display::button(
3380
                    'save_question_list',
3381
                    $label,
3382
                    [
3383
                        'type' => 'button',
3384
                        'class' => $class,
3385
                        'data-list' => implode(",", $questions_in_media),
3386
                    ]
3387
                );
3388
            } else {
3389
                $buttonList[] = Display::button(
3390
                    'save_now',
3391
                    $label,
3392
                    ['type' => 'button', 'class' => $class, 'data-question' => $question_id]
3393
                );
3394
            }
3395
            $buttonList[] = '<span id="save_for_now_'.$question_id.'" class="exercise_save_mini_message"></span>';
3396
3397
            $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3398
3399
            return $html;
3400
        }
3401
3402
        if ($this->review_answers) {
3403
            $all_label = get_lang('ReviewQuestions');
3404
            $class = 'btn btn-success';
3405
        } else {
3406
            $all_label = get_lang('EndTest');
3407
            $class = 'btn btn-warning';
3408
        }
3409
        // used to select it with jquery
3410
        $class .= ' question-validate-btn';
3411
        $buttonList[] = Display::button(
3412
            'validate_all',
3413
            $all_label,
3414
            ['type' => 'button', 'class' => $class]
3415
        );
3416
        $buttonList[] = Display::span(null, ['id' => 'save_all_response']);
3417
        $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3418
3419
        return $html;
3420
    }
3421
3422
    /**
3423
     * @param int    $timeLeft in seconds
3424
     * @param string $url
3425
     *
3426
     * @return string
3427
     */
3428
    public function showSimpleTimeControl($timeLeft, $url = '')
3429
    {
3430
        $timeLeft = (int) $timeLeft;
3431
3432
        return "<script>
3433
            function openClockWarning() {
3434
                $('#clock_warning').dialog({
3435
                    modal:true,
3436
                    height:320,
3437
                    width:550,
3438
                    closeOnEscape: false,
3439
                    resizable: false,
3440
                    buttons: {
3441
                        '".addslashes(get_lang('Close'))."': function() {
3442
                            $('#clock_warning').dialog('close');
3443
                        }
3444
                    },
3445
                    close: function() {
3446
                        window.location.href = '$url';
3447
                    }
3448
                });
3449
                $('#clock_warning').dialog('open');
3450
                $('#counter_to_redirect').epiclock({
3451
                    mode: $.epiclock.modes.countdown,
3452
                    offset: {seconds: 5},
3453
                    format: 's'
3454
                }).bind('timer', function () {
3455
                    window.location.href = '$url';
3456
                });
3457
            }
3458
3459
            function onExpiredTimeExercise() {
3460
                $('#wrapper-clock').hide();
3461
                $('#expired-message-id').show();
3462
                // Fixes bug #5263
3463
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3464
                openClockWarning();
3465
            }
3466
3467
			$(function() {
3468
				// time in seconds when using minutes there are some seconds lost
3469
                var time_left = parseInt(".$timeLeft.");
3470
                $('#exercise_clock_warning').epiclock({
3471
                    mode: $.epiclock.modes.countdown,
3472
                    offset: {seconds: time_left},
3473
                    format: 'x:i:s',
3474
                    renderer: 'minute'
3475
                }).bind('timer', function () {
3476
                    onExpiredTimeExercise();
3477
                });
3478
	       		$('#submit_save').click(function () {});
3479
	        });
3480
	    </script>";
3481
    }
3482
3483
    /**
3484
     * So the time control will work.
3485
     *
3486
     * @param int $timeLeft
3487
     *
3488
     * @return string
3489
     */
3490
    public function showTimeControlJS($timeLeft)
3491
    {
3492
        $timeLeft = (int) $timeLeft;
3493
        $script = 'redirectExerciseToResult();';
3494
        if (ALL_ON_ONE_PAGE == $this->type) {
3495
            $script = "save_now_all('validate');";
3496
        } elseif (ONE_PER_PAGE == $this->type) {
3497
            $script = 'window.quizTimeEnding = true;
3498
                $(\'[name="save_now"]\').trigger(\'click\');';
3499
        }
3500
3501
        return "<script>
3502
            function openClockWarning() {
3503
                $('#clock_warning').dialog({
3504
                    modal:true,
3505
                    height:320,
3506
                    width:550,
3507
                    closeOnEscape: false,
3508
                    resizable: false,
3509
                    buttons: {
3510
                        '".addslashes(get_lang('EndTest'))."': function() {
3511
                            $('#clock_warning').dialog('close');
3512
                        }
3513
                    },
3514
                    close: function() {
3515
                        send_form();
3516
                    }
3517
                });
3518
3519
                $('#clock_warning').dialog('open');
3520
                $('#counter_to_redirect').epiclock({
3521
                    mode: $.epiclock.modes.countdown,
3522
                    offset: {seconds: 5},
3523
                    format: 's'
3524
                }).bind('timer', function () {
3525
                    send_form();
3526
                });
3527
            }
3528
3529
            function send_form() {
3530
                if ($('#exercise_form').length) {
3531
                    $script
3532
                } else {
3533
                    // In exercise_reminder.php
3534
                    final_submit();
3535
                }
3536
            }
3537
3538
            function onExpiredTimeExercise() {
3539
                $('#wrapper-clock').hide();
3540
                $('#expired-message-id').show();
3541
                // Fixes bug #5263
3542
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3543
                openClockWarning();
3544
            }
3545
3546
			$(function() {
3547
				// time in seconds when using minutes there are some seconds lost
3548
                var time_left = parseInt(".$timeLeft.");
3549
                $('#exercise_clock_warning').epiclock({
3550
                    mode: $.epiclock.modes.countdown,
3551
                    offset: {seconds: time_left},
3552
                    format: 'x:C:s',
3553
                    renderer: 'minute'
3554
                }).bind('timer', function () {
3555
                    onExpiredTimeExercise();
3556
                });
3557
	       		$('#submit_save').click(function () {});
3558
	        });
3559
	    </script>";
3560
    }
3561
3562
    /**
3563
     * This function was originally found in the exercise_show.php.
3564
     *
3565
     * @param int    $exeId
3566
     * @param int    $questionId
3567
     * @param mixed  $choice                                    the user-selected option
3568
     * @param string $from                                      function is called from 'exercise_show' or
3569
     *                                                          'exercise_result'
3570
     * @param array  $exerciseResultCoordinates                 the hotspot coordinates $hotspot[$question_id] =
3571
     *                                                          coordinates
3572
     * @param bool   $save_results                              save results in the DB or just show the response
3573
     * @param bool   $from_database                             gets information from DB or from the current selection
3574
     * @param bool   $show_result                               show results or not
3575
     * @param int    $propagate_neg
3576
     * @param array  $hotspot_delineation_result
3577
     * @param bool   $showTotalScoreAndUserChoicesInLastAttempt
3578
     * @param bool   $updateResults
3579
     * @param bool   $showHotSpotDelineationTable
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
    ) {
3600
        $debug = false;
3601
        //needed in order to use in the exercise_attempt() for the time
3602
        global $learnpath_id, $learnpath_item_id;
3603
        require_once api_get_path(LIBRARY_PATH).'geometry.lib.php';
3604
        $em = Database::getManager();
3605
        $feedback_type = $this->getFeedbackType();
3606
        $results_disabled = $this->selectResultsDisabled();
3607
3608
        if ($debug) {
3609
            error_log("<------ manage_answer ------> ");
3610
            error_log('exe_id: '.$exeId);
3611
            error_log('$from:  '.$from);
3612
            error_log('$save_results: '.intval($save_results));
3613
            error_log('$from_database: '.intval($from_database));
3614
            error_log('$show_result: '.intval($show_result));
3615
            error_log('$propagate_neg: '.$propagate_neg);
3616
            error_log('$exerciseResultCoordinates: '.print_r($exerciseResultCoordinates, 1));
3617
            error_log('$hotspot_delineation_result: '.print_r($hotspot_delineation_result, 1));
3618
            error_log('$learnpath_id: '.$learnpath_id);
3619
            error_log('$learnpath_item_id: '.$learnpath_item_id);
3620
            error_log('$choice: '.print_r($choice, 1));
3621
        }
3622
3623
        $final_overlap = 0;
3624
        $final_missing = 0;
3625
        $final_excess = 0;
3626
        $overlap_color = 0;
3627
        $missing_color = 0;
3628
        $excess_color = 0;
3629
        $threadhold1 = 0;
3630
        $threadhold2 = 0;
3631
        $threadhold3 = 0;
3632
        $arrques = null;
3633
        $arrans = null;
3634
        $studentChoice = null;
3635
        $expectedAnswer = '';
3636
        $calculatedChoice = '';
3637
        $calculatedStatus = '';
3638
        $questionId = (int) $questionId;
3639
        $exeId = (int) $exeId;
3640
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3641
        $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
3642
        $studentChoiceDegree = null;
3643
3644
        // Creates a temporary Question object
3645
        $course_id = $this->course_id;
3646
        $objQuestionTmp = Question::read($questionId, $this->course);
3647
3648
        if (false === $objQuestionTmp) {
3649
            return false;
3650
        }
3651
3652
        $questionName = $objQuestionTmp->selectTitle();
3653
        $questionWeighting = $objQuestionTmp->selectWeighting();
3654
        $answerType = $objQuestionTmp->selectType();
3655
        $quesId = $objQuestionTmp->selectId();
3656
        $extra = $objQuestionTmp->extra;
3657
        $next = 1; //not for now
3658
        $totalWeighting = 0;
3659
        $totalScore = 0;
3660
3661
        // Extra information of the question
3662
        if ((
3663
            $answerType == MULTIPLE_ANSWER_TRUE_FALSE ||
3664
            $answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY
3665
            )
3666
            && !empty($extra)
3667
        ) {
3668
            $extra = explode(':', $extra);
3669
            // Fixes problems with negatives values using intval
3670
            $true_score = (float) trim($extra[0]);
3671
            $false_score = (float) trim($extra[1]);
3672
            $doubt_score = (float) trim($extra[2]);
3673
        }
3674
3675
        // Construction of the Answer object
3676
        $objAnswerTmp = new Answer($questionId, $course_id);
3677
        $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
3678
3679
        if ($debug) {
3680
            error_log('Count of possible answers: '.$nbrAnswers);
3681
            error_log('$answerType: '.$answerType);
3682
        }
3683
3684
        if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
3685
            $choiceTmp = $choice;
3686
            $choice = isset($choiceTmp['choice']) ? $choiceTmp['choice'] : '';
3687
            $choiceDegreeCertainty = isset($choiceTmp['choiceDegreeCertainty']) ? $choiceTmp['choiceDegreeCertainty'] : '';
3688
        }
3689
3690
        if ($answerType == FREE_ANSWER ||
3691
            $answerType == ORAL_EXPRESSION ||
3692
            $answerType == CALCULATED_ANSWER ||
3693
            $answerType == ANNOTATION
3694
        ) {
3695
            $nbrAnswers = 1;
3696
        }
3697
3698
        $generatedFile = '';
3699
        if ($answerType == ORAL_EXPRESSION) {
3700
            $exe_info = Event::get_exercise_results_by_attempt($exeId);
3701
            $exe_info = isset($exe_info[$exeId]) ? $exe_info[$exeId] : null;
3702
3703
            $objQuestionTmp->initFile(
3704
                api_get_session_id(),
3705
                isset($exe_info['exe_user_id']) ? $exe_info['exe_user_id'] : api_get_user_id(),
3706
                isset($exe_info['exe_exo_id']) ? $exe_info['exe_exo_id'] : $this->id,
3707
                isset($exe_info['exe_id']) ? $exe_info['exe_id'] : $exeId
3708
            );
3709
3710
            // Probably this attempt came in an exercise all question by page
3711
            if ($feedback_type == 0) {
3712
                $objQuestionTmp->replaceWithRealExe($exeId);
3713
            }
3714
            $generatedFile = $objQuestionTmp->getFileUrl();
3715
        }
3716
3717
        $user_answer = '';
3718
        // Get answer list for matching
3719
        $sql = "SELECT id_auto, id, answer
3720
                FROM $table_ans
3721
                WHERE c_id = $course_id AND question_id = $questionId";
3722
        $res_answer = Database::query($sql);
3723
3724
        $answerMatching = [];
3725
        while ($real_answer = Database::fetch_array($res_answer)) {
3726
            $answerMatching[$real_answer['id_auto']] = $real_answer['answer'];
3727
        }
3728
3729
        $real_answers = [];
3730
        $quiz_question_options = Question::readQuestionOption(
3731
            $questionId,
3732
            $course_id
3733
        );
3734
3735
        $organs_at_risk_hit = 0;
3736
        $questionScore = 0;
3737
        $orderedHotSpots = [];
3738
        if ($answerType == HOT_SPOT || $answerType == ANNOTATION) {
3739
            $orderedHotSpots = $em->getRepository('ChamiloCoreBundle:TrackEHotspot')->findBy(
3740
                [
3741
                    'hotspotQuestionId' => $questionId,
3742
                    'cId' => $course_id,
3743
                    'hotspotExeId' => $exeId,
3744
                ],
3745
                ['hotspotAnswerId' => 'ASC']
3746
            );
3747
        }
3748
3749
        if ($debug) {
3750
            error_log('-- Start answer loop --');
3751
        }
3752
3753
        $answerDestination = null;
3754
        $userAnsweredQuestion = false;
3755
        $correctAnswerId = [];
3756
        for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
3757
            $answer = $objAnswerTmp->selectAnswer($answerId);
3758
            $answerComment = $objAnswerTmp->selectComment($answerId);
3759
            $answerCorrect = $objAnswerTmp->isCorrect($answerId);
3760
            $answerWeighting = (float) $objAnswerTmp->selectWeighting($answerId);
3761
            $answerAutoId = $objAnswerTmp->selectAutoId($answerId);
3762
            $answerIid = isset($objAnswerTmp->iid[$answerId]) ? (int) $objAnswerTmp->iid[$answerId] : 0;
3763
3764
            if ($debug) {
3765
                error_log("c_quiz_answer.id_auto: $answerAutoId ");
3766
                error_log("Answer marked as correct in db (0/1)?: $answerCorrect ");
3767
            }
3768
3769
            // Delineation
3770
            $delineation_cord = $objAnswerTmp->selectHotspotCoordinates(1);
3771
            $answer_delineation_destination = $objAnswerTmp->selectDestination(1);
3772
3773
            switch ($answerType) {
3774
                case UNIQUE_ANSWER:
3775
                case UNIQUE_ANSWER_IMAGE:
3776
                case UNIQUE_ANSWER_NO_OPTION:
3777
                case READING_COMPREHENSION:
3778
                    if ($from_database) {
3779
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3780
                                WHERE
3781
                                    exe_id = $exeId AND
3782
                                    question_id = $questionId";
3783
                        $result = Database::query($sql);
3784
                        $choice = Database::result($result, 0, 'answer');
3785
3786
                        if (false === $userAnsweredQuestion) {
3787
                            $userAnsweredQuestion = !empty($choice);
3788
                        }
3789
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3790
                        if ($studentChoice) {
3791
                            $questionScore += $answerWeighting;
3792
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3793
                            $correctAnswerId[] = $answerId;
3794
                        }
3795
                    } else {
3796
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3797
                        if ($studentChoice) {
3798
                            $questionScore += $answerWeighting;
3799
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3800
                            $correctAnswerId[] = $answerId;
3801
                        }
3802
                    }
3803
                    break;
3804
                case MULTIPLE_ANSWER_TRUE_FALSE:
3805
                    if ($from_database) {
3806
                        $choice = [];
3807
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3808
                                WHERE
3809
                                    exe_id = $exeId AND
3810
                                    question_id = ".$questionId;
3811
3812
                        $result = Database::query($sql);
3813
                        while ($row = Database::fetch_array($result)) {
3814
                            $values = explode(':', $row['answer']);
3815
                            $my_answer_id = isset($values[0]) ? $values[0] : '';
3816
                            $option = isset($values[1]) ? $values[1] : '';
3817
                            $choice[$my_answer_id] = $option;
3818
                        }
3819
3820
                        $userAnsweredQuestion = !empty($choice);
3821
                    }
3822
3823
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3824
                    if (!empty($studentChoice)) {
3825
                        $correctAnswerId[] = $answerAutoId;
3826
                        if ($studentChoice == $answerCorrect) {
3827
                            $questionScore += $true_score;
3828
                        } else {
3829
                            if ($quiz_question_options[$studentChoice]['name'] === "Don't know" ||
3830
                                $quiz_question_options[$studentChoice]['name'] === "DoubtScore"
3831
                            ) {
3832
                                $questionScore += $doubt_score;
3833
                            } else {
3834
                                $questionScore += $false_score;
3835
                            }
3836
                        }
3837
                    } else {
3838
                        // If no result then the user just hit don't know
3839
                        $studentChoice = 3;
3840
                        $questionScore += $doubt_score;
3841
                    }
3842
                    $totalScore = $questionScore;
3843
                    break;
3844
                case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
3845
                    if ($from_database) {
3846
                        $choice = [];
3847
                        $choiceDegreeCertainty = [];
3848
                        $sql = "SELECT answer
3849
                            FROM $TBL_TRACK_ATTEMPT
3850
                            WHERE
3851
                            exe_id = $exeId AND question_id = $questionId";
3852
3853
                        $result = Database::query($sql);
3854
                        while ($row = Database::fetch_array($result)) {
3855
                            $ind = $row['answer'];
3856
                            $values = explode(':', $ind);
3857
                            $myAnswerId = $values[0];
3858
                            $option = $values[1];
3859
                            $percent = $values[2];
3860
                            $choice[$myAnswerId] = $option;
3861
                            $choiceDegreeCertainty[$myAnswerId] = $percent;
3862
                        }
3863
                    }
3864
3865
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3866
                    $studentChoiceDegree = isset($choiceDegreeCertainty[$answerAutoId]) ? $choiceDegreeCertainty[$answerAutoId] : null;
3867
3868
                    // student score update
3869
                    if (!empty($studentChoice)) {
3870
                        if ($studentChoice == $answerCorrect) {
3871
                            // correct answer and student is Unsure or PrettySur
3872
                            if (isset($quiz_question_options[$studentChoiceDegree]) &&
3873
                                $quiz_question_options[$studentChoiceDegree]['position'] >= 3 &&
3874
                                $quiz_question_options[$studentChoiceDegree]['position'] < 9
3875
                            ) {
3876
                                $questionScore += $true_score;
3877
                            } else {
3878
                                // student ignore correct answer
3879
                                $questionScore += $doubt_score;
3880
                            }
3881
                        } else {
3882
                            // false answer and student is Unsure or PrettySur
3883
                            if ($quiz_question_options[$studentChoiceDegree]['position'] >= 3
3884
                                && $quiz_question_options[$studentChoiceDegree]['position'] < 9) {
3885
                                $questionScore += $false_score;
3886
                            } else {
3887
                                // student ignore correct answer
3888
                                $questionScore += $doubt_score;
3889
                            }
3890
                        }
3891
                    }
3892
                    $totalScore = $questionScore;
3893
                    break;
3894
                case MULTIPLE_ANSWER:
3895
                    if ($from_database) {
3896
                        $choice = [];
3897
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3898
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3899
                        $resultans = Database::query($sql);
3900
                        while ($row = Database::fetch_array($resultans)) {
3901
                            $choice[$row['answer']] = 1;
3902
                        }
3903
3904
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3905
                        $real_answers[$answerId] = (bool) $studentChoice;
3906
3907
                        if ($studentChoice) {
3908
                            $questionScore += $answerWeighting;
3909
                        }
3910
                    } else {
3911
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3912
                        $real_answers[$answerId] = (bool) $studentChoice;
3913
3914
                        if (isset($studentChoice)) {
3915
                            $correctAnswerId[] = $answerAutoId;
3916
                            $questionScore += $answerWeighting;
3917
                        }
3918
                    }
3919
                    $totalScore += $answerWeighting;
3920
                    break;
3921
                case GLOBAL_MULTIPLE_ANSWER:
3922
                    if ($from_database) {
3923
                        $choice = [];
3924
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3925
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3926
                        $resultans = Database::query($sql);
3927
                        while ($row = Database::fetch_array($resultans)) {
3928
                            $choice[$row['answer']] = 1;
3929
                        }
3930
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3931
                        $real_answers[$answerId] = (bool) $studentChoice;
3932
                        if ($studentChoice) {
3933
                            $questionScore += $answerWeighting;
3934
                        }
3935
                    } else {
3936
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3937
                        if (isset($studentChoice)) {
3938
                            $questionScore += $answerWeighting;
3939
                        }
3940
                        $real_answers[$answerId] = (bool) $studentChoice;
3941
                    }
3942
                    $totalScore += $answerWeighting;
3943
                    if ($debug) {
3944
                        error_log("studentChoice: $studentChoice");
3945
                    }
3946
                    break;
3947
                case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
3948
                    if ($from_database) {
3949
                        $choice = [];
3950
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3951
                                WHERE exe_id = $exeId AND question_id = $questionId";
3952
                        $resultans = Database::query($sql);
3953
                        while ($row = Database::fetch_array($resultans)) {
3954
                            $result = explode(':', $row['answer']);
3955
                            if (isset($result[0])) {
3956
                                $my_answer_id = isset($result[0]) ? $result[0] : '';
3957
                                $option = isset($result[1]) ? $result[1] : '';
3958
                                $choice[$my_answer_id] = $option;
3959
                            }
3960
                        }
3961
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3962
                        $real_answers[$answerId] = false;
3963
                        if ($answerCorrect == $studentChoice) {
3964
                            $real_answers[$answerId] = true;
3965
                        }
3966
                    } else {
3967
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3968
                        $real_answers[$answerId] = false;
3969
                        if ($answerCorrect == $studentChoice) {
3970
                            $real_answers[$answerId] = true;
3971
                        }
3972
                    }
3973
                    break;
3974
                case MULTIPLE_ANSWER_COMBINATION:
3975
                    if ($from_database) {
3976
                        $choice = [];
3977
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3978
                                WHERE exe_id = $exeId AND question_id = $questionId";
3979
                        $resultans = Database::query($sql);
3980
                        while ($row = Database::fetch_array($resultans)) {
3981
                            $choice[$row['answer']] = 1;
3982
                        }
3983
3984
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3985
                        if (1 == $answerCorrect) {
3986
                            $real_answers[$answerId] = false;
3987
                            if ($studentChoice) {
3988
                                $real_answers[$answerId] = true;
3989
                            }
3990
                        } else {
3991
                            $real_answers[$answerId] = true;
3992
                            if ($studentChoice) {
3993
                                $real_answers[$answerId] = false;
3994
                            }
3995
                        }
3996
                    } else {
3997
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3998
                        if (1 == $answerCorrect) {
3999
                            $real_answers[$answerId] = false;
4000
                            if ($studentChoice) {
4001
                                $real_answers[$answerId] = true;
4002
                            }
4003
                        } else {
4004
                            $real_answers[$answerId] = true;
4005
                            if ($studentChoice) {
4006
                                $real_answers[$answerId] = false;
4007
                            }
4008
                        }
4009
                    }
4010
                    break;
4011
                case FILL_IN_BLANKS:
4012
                    $str = '';
4013
                    $answerFromDatabase = '';
4014
                    if ($from_database) {
4015
                        $sql = "SELECT answer
4016
                                FROM $TBL_TRACK_ATTEMPT
4017
                                WHERE
4018
                                    exe_id = $exeId AND
4019
                                    question_id= $questionId ";
4020
                        $result = Database::query($sql);
4021
                        $str = $answerFromDatabase = Database::result($result, 0, 'answer');
4022
                    }
4023
4024
                    // if ($save_results == false && strpos($answerFromDatabase, 'font color') !== false) {
4025
                    if (false) {
4026
                        // the question is encoded like this
4027
                        // [A] B [C] D [E] F::10,10,10@1
4028
                        // number 1 before the "@" means that is a switchable fill in blank question
4029
                        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4030
                        // means that is a normal fill blank question
4031
                        // first we explode the "::"
4032
                        $pre_array = explode('::', $answer);
4033
4034
                        // is switchable fill blank or not
4035
                        $last = count($pre_array) - 1;
4036
                        $is_set_switchable = explode('@', $pre_array[$last]);
4037
                        $switchable_answer_set = false;
4038
                        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
4039
                            $switchable_answer_set = true;
4040
                        }
4041
                        $answer = '';
4042
                        for ($k = 0; $k < $last; $k++) {
4043
                            $answer .= $pre_array[$k];
4044
                        }
4045
                        // splits weightings that are joined with a comma
4046
                        $answerWeighting = explode(',', $is_set_switchable[0]);
4047
                        // we save the answer because it will be modified
4048
                        $temp = $answer;
4049
                        $answer = '';
4050
                        $j = 0;
4051
                        //initialise answer tags
4052
                        $user_tags = $correct_tags = $real_text = [];
4053
                        // the loop will stop at the end of the text
4054
                        while (1) {
4055
                            // quits the loop if there are no more blanks (detect '[')
4056
                            if ($temp == false || ($pos = api_strpos($temp, '[')) === false) {
4057
                                // adds the end of the text
4058
                                $answer = $temp;
4059
                                $real_text[] = $answer;
4060
                                break; //no more "blanks", quit the loop
4061
                            }
4062
                            // adds the piece of text that is before the blank
4063
                            //and ends with '[' into a general storage array
4064
                            $real_text[] = api_substr($temp, 0, $pos + 1);
4065
                            $answer .= api_substr($temp, 0, $pos + 1);
4066
                            //take the string remaining (after the last "[" we found)
4067
                            $temp = api_substr($temp, $pos + 1);
4068
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4069
                            if (($pos = api_strpos($temp, ']')) === false) {
4070
                                // adds the end of the text
4071
                                $answer .= $temp;
4072
                                break;
4073
                            }
4074
                            if ($from_database) {
4075
                                $str = $answerFromDatabase;
4076
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4077
                                $str = str_replace('\r\n', '', $str);
4078
4079
                                $choice = $arr[1];
4080
                                if (isset($choice[$j])) {
4081
                                    $tmp = api_strrpos($choice[$j], ' / ');
4082
                                    $choice[$j] = api_substr($choice[$j], 0, $tmp);
4083
                                    $choice[$j] = trim($choice[$j]);
4084
                                    // Needed to let characters ' and " to work as part of an answer
4085
                                    $choice[$j] = stripslashes($choice[$j]);
4086
                                } else {
4087
                                    $choice[$j] = null;
4088
                                }
4089
                            } else {
4090
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4091
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4092
                            }
4093
4094
                            $user_tags[] = $choice[$j];
4095
                            // Put the contents of the [] answer tag into correct_tags[]
4096
                            $correct_tags[] = api_substr($temp, 0, $pos);
4097
                            $j++;
4098
                            $temp = api_substr($temp, $pos + 1);
4099
                        }
4100
                        $answer = '';
4101
                        $real_correct_tags = $correct_tags;
4102
                        $chosen_list = [];
4103
4104
                        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...
4105
                            if (0 == $i) {
4106
                                $answer .= $real_text[0];
4107
                            }
4108
                            if (!$switchable_answer_set) {
4109
                                // Needed to parse ' and " characters
4110
                                $user_tags[$i] = stripslashes($user_tags[$i]);
4111
                                if ($correct_tags[$i] == $user_tags[$i]) {
4112
                                    // gives the related weighting to the student
4113
                                    $questionScore += $answerWeighting[$i];
4114
                                    // increments total score
4115
                                    $totalScore += $answerWeighting[$i];
4116
                                    // adds the word in green at the end of the string
4117
                                    $answer .= $correct_tags[$i];
4118
                                } elseif (!empty($user_tags[$i])) {
4119
                                    // else if the word entered by the student IS NOT the same as
4120
                                    // the one defined by the professor
4121
                                    // adds the word in red at the end of the string, and strikes it
4122
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4123
                                } else {
4124
                                    // adds a tabulation if no word has been typed by the student
4125
                                    $answer .= ''; // remove &nbsp; that causes issue
4126
                                }
4127
                            } else {
4128
                                // switchable fill in the blanks
4129
                                if (in_array($user_tags[$i], $correct_tags)) {
4130
                                    $chosen_list[] = $user_tags[$i];
4131
                                    $correct_tags = array_diff($correct_tags, $chosen_list);
4132
                                    // gives the related weighting to the student
4133
                                    $questionScore += $answerWeighting[$i];
4134
                                    // increments total score
4135
                                    $totalScore += $answerWeighting[$i];
4136
                                    // adds the word in green at the end of the string
4137
                                    $answer .= $user_tags[$i];
4138
                                } elseif (!empty($user_tags[$i])) {
4139
                                    // else if the word entered by the student IS NOT the same
4140
                                    // as the one defined by the professor
4141
                                    // adds the word in red at the end of the string, and strikes it
4142
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4143
                                } else {
4144
                                    // adds a tabulation if no word has been typed by the student
4145
                                    $answer .= ''; // remove &nbsp; that causes issue
4146
                                }
4147
                            }
4148
4149
                            // adds the correct word, followed by ] to close the blank
4150
                            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4151
                            if (isset($real_text[$i + 1])) {
4152
                                $answer .= $real_text[$i + 1];
4153
                            }
4154
                        }
4155
                    } else {
4156
                        // insert the student result in the track_e_attempt table, field answer
4157
                        // $answer is the answer like in the c_quiz_answer table for the question
4158
                        // student data are choice[]
4159
                        $listCorrectAnswers = FillBlanks::getAnswerInfo($answer);
4160
                        $switchableAnswerSet = $listCorrectAnswers['switchable'];
4161
                        $answerWeighting = $listCorrectAnswers['weighting'];
4162
                        // user choices is an array $choice
4163
4164
                        // get existing user data in n the BDD
4165
                        if ($from_database) {
4166
                            $listStudentResults = FillBlanks::getAnswerInfo(
4167
                                $answerFromDatabase,
4168
                                true
4169
                            );
4170
                            $choice = $listStudentResults['student_answer'];
4171
                        }
4172
4173
                        // loop other all blanks words
4174
                        if (!$switchableAnswerSet) {
4175
                            // not switchable answer, must be in the same place than teacher order
4176
                            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...
4177
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
4178
                                $correctAnswer = $listCorrectAnswers['words'][$i];
4179
4180
                                if ($debug) {
4181
                                    error_log("Student answer: $i");
4182
                                    error_log($studentAnswer);
4183
                                }
4184
4185
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4186
                                // Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
4187
                                // ENT_QUOTES is used in order to transform ' to &#039;
4188
                                if (!$from_database) {
4189
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4190
                                    if ($debug) {
4191
                                        error_log('Student answer cleaned:');
4192
                                        error_log($studentAnswer);
4193
                                    }
4194
                                }
4195
4196
                                $isAnswerCorrect = 0;
4197
                                if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
4198
                                    // gives the related weighting to the student
4199
                                    $questionScore += $answerWeighting[$i];
4200
                                    // increments total score
4201
                                    $totalScore += $answerWeighting[$i];
4202
                                    $isAnswerCorrect = 1;
4203
                                }
4204
                                if ($debug) {
4205
                                    error_log("isAnswerCorrect $i: $isAnswerCorrect");
4206
                                }
4207
4208
                                $studentAnswerToShow = $studentAnswer;
4209
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4210
                                if ($debug) {
4211
                                    error_log("Fill in blank type: $type");
4212
                                }
4213
                                if ($type == FillBlanks::FILL_THE_BLANK_MENU) {
4214
                                    $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4215
                                    if ($studentAnswer != '') {
4216
                                        foreach ($listMenu as $item) {
4217
                                            if (sha1($item) == $studentAnswer) {
4218
                                                $studentAnswerToShow = $item;
4219
                                            }
4220
                                        }
4221
                                    }
4222
                                }
4223
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4224
                                $listCorrectAnswers['student_score'][$i] = $isAnswerCorrect;
4225
                            }
4226
                        } else {
4227
                            // switchable answer
4228
                            $listStudentAnswerTemp = $choice;
4229
                            $listTeacherAnswerTemp = $listCorrectAnswers['words'];
4230
4231
                            // for every teacher answer, check if there is a student answer
4232
                            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...
4233
                                $studentAnswer = trim($listStudentAnswerTemp[$i]);
4234
                                $studentAnswerToShow = $studentAnswer;
4235
4236
                                if (empty($studentAnswer)) {
4237
                                    continue;
4238
                                }
4239
4240
                                if ($debug) {
4241
                                    error_log("Student answer: $i");
4242
                                    error_log($studentAnswer);
4243
                                }
4244
4245
                                if (!$from_database) {
4246
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4247
                                    if ($debug) {
4248
                                        error_log("Student answer cleaned:");
4249
                                        error_log($studentAnswer);
4250
                                    }
4251
                                }
4252
4253
                                $found = false;
4254
                                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...
4255
                                    $correctAnswer = $listTeacherAnswerTemp[$j];
4256
4257
                                    if (!$found) {
4258
                                        if (FillBlanks::isStudentAnswerGood(
4259
                                            $studentAnswer,
4260
                                            $correctAnswer,
4261
                                            $from_database
4262
                                        )) {
4263
                                            $questionScore += $answerWeighting[$i];
4264
                                            $totalScore += $answerWeighting[$i];
4265
                                            $listTeacherAnswerTemp[$j] = '';
4266
                                            $found = true;
4267
                                        }
4268
                                    }
4269
4270
                                    $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4271
                                    if ($type == FillBlanks::FILL_THE_BLANK_MENU) {
4272
                                        $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4273
                                        if (!empty($studentAnswer)) {
4274
                                            foreach ($listMenu as $key => $item) {
4275
                                                if ($key == $correctAnswer) {
4276
                                                    $studentAnswerToShow = $item;
4277
                                                    break;
4278
                                                }
4279
                                            }
4280
                                        }
4281
                                    }
4282
                                }
4283
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4284
                                $listCorrectAnswers['student_score'][$i] = $found ? 1 : 0;
4285
                            }
4286
                        }
4287
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
4288
                    }
4289
                    break;
4290
                case CALCULATED_ANSWER:
4291
                    $calculatedAnswerList = Session::read('calculatedAnswerId');
4292
                    if (!empty($calculatedAnswerList)) {
4293
                        $answer = $objAnswerTmp->selectAnswer($calculatedAnswerList[$questionId]);
4294
                        $preArray = explode('@@', $answer);
4295
                        $last = count($preArray) - 1;
4296
                        $answer = '';
4297
                        for ($k = 0; $k < $last; $k++) {
4298
                            $answer .= $preArray[$k];
4299
                        }
4300
                        $answerWeighting = [$answerWeighting];
4301
                        // we save the answer because it will be modified
4302
                        $temp = $answer;
4303
                        $answer = '';
4304
                        $j = 0;
4305
                        // initialise answer tags
4306
                        $userTags = $correctTags = $realText = [];
4307
                        // the loop will stop at the end of the text
4308
                        while (1) {
4309
                            // quits the loop if there are no more blanks (detect '[')
4310
                            if ($temp == false || ($pos = api_strpos($temp, '[')) === false) {
4311
                                // adds the end of the text
4312
                                $answer = $temp;
4313
                                $realText[] = $answer;
4314
                                break; //no more "blanks", quit the loop
4315
                            }
4316
                            // adds the piece of text that is before the blank
4317
                            // and ends with '[' into a general storage array
4318
                            $realText[] = api_substr($temp, 0, $pos + 1);
4319
                            $answer .= api_substr($temp, 0, $pos + 1);
4320
                            // take the string remaining (after the last "[" we found)
4321
                            $temp = api_substr($temp, $pos + 1);
4322
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4323
                            if (false === ($pos = api_strpos($temp, ']'))) {
4324
                                // adds the end of the text
4325
                                $answer .= $temp;
4326
                                break;
4327
                            }
4328
4329
                            if ($from_database) {
4330
                                $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4331
                                        WHERE
4332
                                            exe_id = $exeId AND
4333
                                            question_id = $questionId ";
4334
                                $result = Database::query($sql);
4335
                                $str = Database::result($result, 0, 'answer');
4336
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4337
                                $str = str_replace('\r\n', '', $str);
4338
                                $choice = $arr[1];
4339
                                if (isset($choice[$j])) {
4340
                                    $tmp = api_strrpos($choice[$j], ' / ');
4341
                                    if ($tmp) {
4342
                                        $choice[$j] = api_substr($choice[$j], 0, $tmp);
4343
                                    } else {
4344
                                        $tmp = ltrim($tmp, '[');
4345
                                        $tmp = rtrim($tmp, ']');
4346
                                    }
4347
                                    $choice[$j] = trim($choice[$j]);
4348
                                    // Needed to let characters ' and " to work as part of an answer
4349
                                    $choice[$j] = stripslashes($choice[$j]);
4350
                                } else {
4351
                                    $choice[$j] = null;
4352
                                }
4353
                            } else {
4354
                                // This value is the user input not escaped while correct answer is escaped by ckeditor
4355
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4356
                            }
4357
                            $userTags[] = $choice[$j];
4358
                            // put the contents of the [] answer tag into correct_tags[]
4359
                            $correctTags[] = api_substr($temp, 0, $pos);
4360
                            $j++;
4361
                            $temp = api_substr($temp, $pos + 1);
4362
                        }
4363
                        $answer = '';
4364
                        $realCorrectTags = $correctTags;
4365
                        $calculatedStatus = Display::label(get_lang('Incorrect'), 'danger');
4366
                        $expectedAnswer = '';
4367
                        $calculatedChoice = '';
4368
4369
                        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...
4370
                            if (0 == $i) {
4371
                                $answer .= $realText[0];
4372
                            }
4373
                            // Needed to parse ' and " characters
4374
                            $userTags[$i] = stripslashes($userTags[$i]);
4375
                            if ($correctTags[$i] == $userTags[$i]) {
4376
                                // gives the related weighting to the student
4377
                                $questionScore += $answerWeighting[$i];
4378
                                // increments total score
4379
                                $totalScore += $answerWeighting[$i];
4380
                                // adds the word in green at the end of the string
4381
                                $answer .= $correctTags[$i];
4382
                                $calculatedChoice = $correctTags[$i];
4383
                            } elseif (!empty($userTags[$i])) {
4384
                                // else if the word entered by the student IS NOT the same as
4385
                                // the one defined by the professor
4386
                                // adds the word in red at the end of the string, and strikes it
4387
                                $answer .= '<font color="red"><s>'.$userTags[$i].'</s></font>';
4388
                                $calculatedChoice = $userTags[$i];
4389
                            } else {
4390
                                // adds a tabulation if no word has been typed by the student
4391
                                $answer .= ''; // remove &nbsp; that causes issue
4392
                            }
4393
                            // adds the correct word, followed by ] to close the blank
4394
                            if (EXERCISE_FEEDBACK_TYPE_EXAM != $this->results_disabled) {
4395
                                $answer .= ' / <font color="green"><b>'.$realCorrectTags[$i].'</b></font>';
4396
                                $calculatedStatus = Display::label(get_lang('Correct'), 'success');
4397
                                $expectedAnswer = $realCorrectTags[$i];
4398
                            }
4399
                            $answer .= ']';
4400
                            if (isset($realText[$i + 1])) {
4401
                                $answer .= $realText[$i + 1];
4402
                            }
4403
                        }
4404
                    } else {
4405
                        if ($from_database) {
4406
                            $sql = "SELECT *
4407
                                    FROM $TBL_TRACK_ATTEMPT
4408
                                    WHERE
4409
                                        exe_id = $exeId AND
4410
                                        question_id = $questionId ";
4411
                            $result = Database::query($sql);
4412
                            $resultData = Database::fetch_array($result, 'ASSOC');
4413
                            $answer = $resultData['answer'];
4414
                            $questionScore = $resultData['marks'];
4415
                        }
4416
                    }
4417
                    break;
4418
                case FREE_ANSWER:
4419
                    if ($from_database) {
4420
                        $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
4421
                                 WHERE
4422
                                    exe_id = $exeId AND
4423
                                    question_id= ".$questionId;
4424
                        $result = Database::query($sql);
4425
                        $data = Database::fetch_array($result);
4426
4427
                        $choice = $data['answer'];
4428
                        $choice = str_replace('\r\n', '', $choice);
4429
                        $choice = stripslashes($choice);
4430
                        $questionScore = $data['marks'];
4431
4432
                        if (-1 == $questionScore) {
4433
                            $totalScore += 0;
4434
                        } else {
4435
                            $totalScore += $questionScore;
4436
                        }
4437
                        if ('' == $questionScore) {
4438
                            $questionScore = 0;
4439
                        }
4440
                        $arrques = $questionName;
4441
                        $arrans = $choice;
4442
                    } else {
4443
                        $studentChoice = $choice;
4444
                        if ($studentChoice) {
4445
                            //Fixing negative puntation see #2193
4446
                            $questionScore = 0;
4447
                            $totalScore += 0;
4448
                        }
4449
                    }
4450
                    break;
4451
                case ORAL_EXPRESSION:
4452
                    if ($from_database) {
4453
                        $query = "SELECT answer, marks
4454
                                  FROM $TBL_TRACK_ATTEMPT
4455
                                  WHERE
4456
                                        exe_id = $exeId AND
4457
                                        question_id = $questionId
4458
                                 ";
4459
                        $resq = Database::query($query);
4460
                        $row = Database::fetch_assoc($resq);
4461
                        $choice = $row['answer'];
4462
                        $choice = str_replace('\r\n', '', $choice);
4463
                        $choice = stripslashes($choice);
4464
                        $questionScore = $row['marks'];
4465
                        if ($questionScore == -1) {
4466
                            $totalScore += 0;
4467
                        } else {
4468
                            $totalScore += $questionScore;
4469
                        }
4470
                        $arrques = $questionName;
4471
                        $arrans = $choice;
4472
                    } else {
4473
                        $studentChoice = $choice;
4474
                        if ($studentChoice) {
4475
                            //Fixing negative puntation see #2193
4476
                            $questionScore = 0;
4477
                            $totalScore += 0;
4478
                        }
4479
                    }
4480
                    break;
4481
                case DRAGGABLE:
4482
                case MATCHING_DRAGGABLE:
4483
                case MATCHING:
4484
                    if ($from_database) {
4485
                        $sql = "SELECT id, answer, id_auto
4486
                                FROM $table_ans
4487
                                WHERE
4488
                                    c_id = $course_id AND
4489
                                    question_id = $questionId AND
4490
                                    correct = 0
4491
                                ";
4492
                        $result = Database::query($sql);
4493
                        // Getting the real answer
4494
                        $real_list = [];
4495
                        while ($realAnswer = Database::fetch_array($result)) {
4496
                            $real_list[$realAnswer['id_auto']] = $realAnswer['answer'];
4497
                        }
4498
4499
                        $sql = "SELECT id, answer, correct, id_auto, ponderation
4500
                                FROM $table_ans
4501
                                WHERE
4502
                                    c_id = $course_id AND
4503
                                    question_id = $questionId AND
4504
                                    correct <> 0
4505
                                ORDER BY id_auto";
4506
                        $result = Database::query($sql);
4507
                        $options = [];
4508
                        while ($row = Database::fetch_array($result, 'ASSOC')) {
4509
                            $options[] = $row;
4510
                        }
4511
4512
                        $questionScore = 0;
4513
                        $counterAnswer = 1;
4514
                        foreach ($options as $a_answers) {
4515
                            $i_answer_id = $a_answers['id']; //3
4516
                            $s_answer_label = $a_answers['answer']; // your daddy - your mother
4517
                            $i_answer_correct_answer = $a_answers['correct']; //1 - 2
4518
                            $i_answer_id_auto = $a_answers['id_auto']; // 3 - 4
4519
4520
                            $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4521
                                    WHERE
4522
                                        exe_id = '$exeId' AND
4523
                                        question_id = '$questionId' AND
4524
                                        position = '$i_answer_id_auto'";
4525
                            $result = Database::query($sql);
4526
                            $s_user_answer = 0;
4527
                            if (Database::num_rows($result) > 0) {
4528
                                //  rich - good looking
4529
                                $s_user_answer = Database::result($result, 0, 0);
4530
                            }
4531
                            $i_answerWeighting = $a_answers['ponderation'];
4532
                            $user_answer = '';
4533
                            $status = Display::label(get_lang('Incorrect'), 'danger');
4534
4535
                            if (!empty($s_user_answer)) {
4536
                                if (DRAGGABLE == $answerType) {
4537
                                    if ($s_user_answer == $i_answer_correct_answer) {
4538
                                        $questionScore += $i_answerWeighting;
4539
                                        $totalScore += $i_answerWeighting;
4540
                                        $user_answer = Display::label(get_lang('Correct'), 'success');
4541
                                        if ($this->showExpectedChoice()) {
4542
                                            $user_answer = $answerMatching[$i_answer_id_auto];
4543
                                        }
4544
                                        $status = Display::label(get_lang('Correct'), 'success');
4545
                                    } else {
4546
                                        $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4547
                                        if ($this->showExpectedChoice()) {
4548
                                            $data = $options[$real_list[$s_user_answer] - 1];
4549
                                            $user_answer = $data['answer'];
4550
                                        }
4551
                                    }
4552
                                } else {
4553
                                    if ($s_user_answer == $i_answer_correct_answer) {
4554
                                        $questionScore += $i_answerWeighting;
4555
                                        $totalScore += $i_answerWeighting;
4556
                                        $status = Display::label(get_lang('Correct'), 'success');
4557
4558
                                        // Try with id
4559
                                        if (isset($real_list[$i_answer_id])) {
4560
                                            $user_answer = Display::span(
4561
                                                $real_list[$i_answer_id],
4562
                                                ['style' => 'color: #008000; font-weight: bold;']
4563
                                            );
4564
                                        }
4565
4566
                                        // Try with $i_answer_id_auto
4567
                                        if (empty($user_answer)) {
4568
                                            if (isset($real_list[$i_answer_id_auto])) {
4569
                                                $user_answer = Display::span(
4570
                                                    $real_list[$i_answer_id_auto],
4571
                                                    ['style' => 'color: #008000; font-weight: bold;']
4572
                                                );
4573
                                            }
4574
                                        }
4575
4576
                                        if (isset($real_list[$i_answer_correct_answer])) {
4577
                                            $user_answer = Display::span(
4578
                                                $real_list[$i_answer_correct_answer],
4579
                                                ['style' => 'color: #008000; font-weight: bold;']
4580
                                            );
4581
                                        }
4582
                                    } else {
4583
                                        $user_answer = Display::span(
4584
                                            $real_list[$s_user_answer],
4585
                                            ['style' => 'color: #FF0000; text-decoration: line-through;']
4586
                                        );
4587
                                        if ($this->showExpectedChoice()) {
4588
                                            if (isset($real_list[$s_user_answer])) {
4589
                                                $user_answer = Display::span($real_list[$s_user_answer]);
4590
                                            }
4591
                                        }
4592
                                    }
4593
                                }
4594
                            } elseif (DRAGGABLE == $answerType) {
4595
                                $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4596
                                if ($this->showExpectedChoice()) {
4597
                                    $user_answer = '';
4598
                                }
4599
                            } else {
4600
                                $user_answer = Display::span(
4601
                                    get_lang('Incorrect').' &nbsp;',
4602
                                    ['style' => 'color: #FF0000; text-decoration: line-through;']
4603
                                );
4604
                                if ($this->showExpectedChoice()) {
4605
                                    $user_answer = '';
4606
                                }
4607
                            }
4608
4609
                            if ($show_result) {
4610
                                if (false === $this->showExpectedChoice() &&
4611
                                    false === $showTotalScoreAndUserChoicesInLastAttempt
4612
                                ) {
4613
                                    $user_answer = '';
4614
                                }
4615
                                switch ($answerType) {
4616
                                    case MATCHING:
4617
                                    case MATCHING_DRAGGABLE:
4618
                                        echo '<tr>';
4619
                                        if (!in_array(
4620
                                            $this->results_disabled,
4621
                                            [
4622
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4623
                                            ]
4624
                                        )
4625
                                        ) {
4626
                                            echo '<td>'.$s_answer_label.'</td>';
4627
                                            echo '<td>'.$user_answer.'</td>';
4628
                                        } else {
4629
                                            echo '<td>'.$s_answer_label.'</td>';
4630
                                            $status = Display::label(get_lang('Correct'), 'success');
4631
                                        }
4632
4633
                                        if ($this->showExpectedChoice()) {
4634
                                            if ($this->showExpectedChoiceColumn()) {
4635
                                                echo '<td>';
4636
                                                if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4637
                                                    if (isset($real_list[$i_answer_correct_answer]) &&
4638
                                                        $showTotalScoreAndUserChoicesInLastAttempt == true
4639
                                                    ) {
4640
                                                        echo Display::span(
4641
                                                            $real_list[$i_answer_correct_answer]
4642
                                                        );
4643
                                                    }
4644
                                                }
4645
                                                echo '</td>';
4646
                                            }
4647
                                            echo '<td>'.$status.'</td>';
4648
                                        } else {
4649
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4650
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4651
                                                    $showTotalScoreAndUserChoicesInLastAttempt === true
4652
                                                ) {
4653
                                                    if ($this->showExpectedChoiceColumn()) {
4654
                                                        echo '<td>';
4655
                                                        echo Display::span(
4656
                                                            $real_list[$i_answer_correct_answer],
4657
                                                            ['style' => 'color: #008000; font-weight: bold;']
4658
                                                        );
4659
                                                        echo '</td>';
4660
                                                    }
4661
                                                }
4662
                                            }
4663
                                        }
4664
                                        echo '</tr>';
4665
                                        break;
4666
                                    case DRAGGABLE:
4667
                                        if (false == $showTotalScoreAndUserChoicesInLastAttempt) {
4668
                                            $s_answer_label = '';
4669
                                        }
4670
                                        echo '<tr>';
4671
                                        if ($this->showExpectedChoice()) {
4672
                                            if (!in_array($this->results_disabled, [
4673
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4674
                                                //RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4675
                                            ])
4676
                                            ) {
4677
                                                echo '<td>'.$user_answer.'</td>';
4678
                                            } else {
4679
                                                $status = Display::label(get_lang('Correct'), 'success');
4680
                                            }
4681
                                            echo '<td>'.$s_answer_label.'</td>';
4682
                                            echo '<td>'.$status.'</td>';
4683
                                        } else {
4684
                                            echo '<td>'.$s_answer_label.'</td>';
4685
                                            echo '<td>'.$user_answer.'</td>';
4686
                                            echo '<td>';
4687
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4688
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4689
                                                    $showTotalScoreAndUserChoicesInLastAttempt === true
4690
                                                ) {
4691
                                                    echo Display::span(
4692
                                                        $real_list[$i_answer_correct_answer],
4693
                                                        ['style' => 'color: #008000; font-weight: bold;']
4694
                                                    );
4695
                                                }
4696
                                            }
4697
                                            echo '</td>';
4698
                                        }
4699
                                        echo '</tr>';
4700
                                        break;
4701
                                }
4702
                            }
4703
                            $counterAnswer++;
4704
                        }
4705
                        break 2; // break the switch and the "for" condition
4706
                    } else {
4707
                        if ($answerCorrect) {
4708
                            if (isset($choice[$answerAutoId]) &&
4709
                                $answerCorrect == $choice[$answerAutoId]
4710
                            ) {
4711
                                $correctAnswerId[] = $answerAutoId;
4712
                                $questionScore += $answerWeighting;
4713
                                $totalScore += $answerWeighting;
4714
                                $user_answer = Display::span($answerMatching[$choice[$answerAutoId]]);
4715
                            } else {
4716
                                if (isset($answerMatching[$choice[$answerAutoId]])) {
4717
                                    $user_answer = Display::span(
4718
                                        $answerMatching[$choice[$answerAutoId]],
4719
                                        ['style' => 'color: #FF0000; text-decoration: line-through;']
4720
                                    );
4721
                                }
4722
                            }
4723
                            $matching[$answerAutoId] = $choice[$answerAutoId];
4724
                        }
4725
                    }
4726
                    break;
4727
                case HOT_SPOT:
4728
                    if ($from_database) {
4729
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4730
                        // Check auto id
4731
                        $foundAnswerId = $answerAutoId;
4732
                        $sql = "SELECT hotspot_correct
4733
                                FROM $TBL_TRACK_HOTSPOT
4734
                                WHERE
4735
                                    hotspot_exe_id = $exeId AND
4736
                                    hotspot_question_id= $questionId AND
4737
                                    hotspot_answer_id = $answerAutoId
4738
                                ORDER BY hotspot_id ASC";
4739
                        $result = Database::query($sql);
4740
                        if (Database::num_rows($result)) {
4741
                            $studentChoice = Database::result(
4742
                                $result,
4743
                                0,
4744
                                'hotspot_correct'
4745
                            );
4746
4747
                            if ($studentChoice) {
4748
                                $questionScore += $answerWeighting;
4749
                                $totalScore += $answerWeighting;
4750
                            }
4751
                        } else {
4752
                            // If answer.id is different:
4753
                            $sql = "SELECT hotspot_correct
4754
                                FROM $TBL_TRACK_HOTSPOT
4755
                                WHERE
4756
                                    hotspot_exe_id = $exeId AND
4757
                                    hotspot_question_id= $questionId AND
4758
                                    hotspot_answer_id = ".intval($answerId)."
4759
                                ORDER BY hotspot_id ASC";
4760
                            $result = Database::query($sql);
4761
                            $foundAnswerId = $answerId;
4762
                            if (Database::num_rows($result)) {
4763
                                $studentChoice = Database::result(
4764
                                    $result,
4765
                                    0,
4766
                                    'hotspot_correct'
4767
                                );
4768
4769
                                if ($studentChoice) {
4770
                                    $questionScore += $answerWeighting;
4771
                                    $totalScore += $answerWeighting;
4772
                                }
4773
                            } else {
4774
                                // check answer.iid
4775
                                if (!empty($answerIid)) {
4776
                                    $sql = "SELECT hotspot_correct
4777
                                            FROM $TBL_TRACK_HOTSPOT
4778
                                            WHERE
4779
                                                hotspot_exe_id = $exeId AND
4780
                                                hotspot_question_id= $questionId AND
4781
                                                hotspot_answer_id = $answerIid
4782
                                            ORDER BY hotspot_id ASC";
4783
                                    $result = Database::query($sql);
4784
                                    $foundAnswerId = $answerIid;
4785
                                    $studentChoice = Database::result(
4786
                                        $result,
4787
                                        0,
4788
                                        'hotspot_correct'
4789
                                    );
4790
4791
                                    if ($studentChoice) {
4792
                                        $questionScore += $answerWeighting;
4793
                                        $totalScore += $answerWeighting;
4794
                                    }
4795
                                }
4796
                            }
4797
                        }
4798
                    } else {
4799
                        if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
4800
                            $choice[$answerAutoId] = 0;
4801
                            $choice[$answerIid] = 0;
4802
                        } else {
4803
                            $studentChoice = $choice[$answerAutoId];
4804
                            if (empty($studentChoice)) {
4805
                                $studentChoice = $choice[$answerIid];
4806
                            }
4807
                            $choiceIsValid = false;
4808
                            if (!empty($studentChoice)) {
4809
                                $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
4810
                                $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
4811
                                $choicePoint = Geometry::decodePoint($studentChoice);
4812
4813
                                switch ($hotspotType) {
4814
                                    case 'square':
4815
                                        $hotspotProperties = Geometry::decodeSquare($hotspotCoordinates);
4816
                                        $choiceIsValid = Geometry::pointIsInSquare($hotspotProperties, $choicePoint);
4817
                                        break;
4818
                                    case 'circle':
4819
                                        $hotspotProperties = Geometry::decodeEllipse($hotspotCoordinates);
4820
                                        $choiceIsValid = Geometry::pointIsInEllipse($hotspotProperties, $choicePoint);
4821
                                        break;
4822
                                    case 'poly':
4823
                                        $hotspotProperties = Geometry::decodePolygon($hotspotCoordinates);
4824
                                        $choiceIsValid = Geometry::pointIsInPolygon($hotspotProperties, $choicePoint);
4825
                                        break;
4826
                                }
4827
                            }
4828
4829
                            $choice[$answerAutoId] = 0;
4830
                            if ($choiceIsValid) {
4831
                                $questionScore += $answerWeighting;
4832
                                $totalScore += $answerWeighting;
4833
                                $choice[$answerAutoId] = 1;
4834
                                $choice[$answerIid] = 1;
4835
                            }
4836
                        }
4837
                    }
4838
                    break;
4839
                case HOT_SPOT_ORDER:
4840
                    // @todo never added to chamilo
4841
                    // for hotspot with fixed order
4842
                    $studentChoice = $choice['order'][$answerId];
4843
                    if ($studentChoice == $answerId) {
4844
                        $questionScore += $answerWeighting;
4845
                        $totalScore += $answerWeighting;
4846
                        $studentChoice = true;
4847
                    } else {
4848
                        $studentChoice = false;
4849
                    }
4850
                    break;
4851
                case HOT_SPOT_DELINEATION:
4852
                    // for hotspot with delineation
4853
                    if ($from_database) {
4854
                        // getting the user answer
4855
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4856
                        $query = "SELECT hotspot_correct, hotspot_coordinate
4857
                                    FROM $TBL_TRACK_HOTSPOT
4858
                                    WHERE
4859
                                        hotspot_exe_id = $exeId AND
4860
                                        hotspot_question_id= $questionId AND
4861
                                        hotspot_answer_id = '1'";
4862
                        // By default we take 1 because it's a delineation
4863
                        $resq = Database::query($query);
4864
                        $row = Database::fetch_array($resq, 'ASSOC');
4865
4866
                        $choice = $row['hotspot_correct'];
4867
                        $user_answer = $row['hotspot_coordinate'];
4868
4869
                        // THIS is very important otherwise the poly_compile will throw an error!!
4870
                        // round-up the coordinates
4871
                        $coords = explode('/', $user_answer);
4872
                        $coords = array_filter($coords);
4873
                        $user_array = '';
4874
                        foreach ($coords as $coord) {
4875
                            list($x, $y) = explode(';', $coord);
4876
                            $user_array .= round($x).';'.round($y).'/';
4877
                        }
4878
                        $user_array = substr($user_array, 0, -1) ?: '';
4879
                    } else {
4880
                        if (!empty($studentChoice)) {
4881
                            $correctAnswerId[] = $answerAutoId;
4882
                            $newquestionList[] = $questionId;
4883
                        }
4884
4885
                        if (1 === $answerId) {
4886
                            $studentChoice = $choice[$answerId];
4887
                            $questionScore += $answerWeighting;
4888
                        }
4889
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
4890
                            $user_array = $_SESSION['exerciseResultCoordinates'][$questionId];
4891
                        }
4892
                    }
4893
                    $_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
4894
                    $_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
4895
                    break;
4896
                case ANNOTATION:
4897
                    if ($from_database) {
4898
                        $sql = "SELECT answer, marks
4899
                                FROM $TBL_TRACK_ATTEMPT
4900
                                WHERE
4901
                                  exe_id = $exeId AND
4902
                                  question_id = $questionId ";
4903
                        $resq = Database::query($sql);
4904
                        $data = Database::fetch_array($resq);
4905
4906
                        $questionScore = empty($data['marks']) ? 0 : $data['marks'];
4907
                        $arrques = $questionName;
4908
                        break;
4909
                    }
4910
                    $studentChoice = $choice;
4911
                    if ($studentChoice) {
4912
                        $questionScore = 0;
4913
                    }
4914
                    break;
4915
            }
4916
4917
            if ($show_result) {
4918
                if ('exercise_result' === $from) {
4919
                    // Display answers (if not matching type, or if the answer is correct)
4920
                    if (!in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE]) ||
4921
                        $answerCorrect
4922
                    ) {
4923
                        if (in_array(
4924
                            $answerType,
4925
                            [
4926
                                UNIQUE_ANSWER,
4927
                                UNIQUE_ANSWER_IMAGE,
4928
                                UNIQUE_ANSWER_NO_OPTION,
4929
                                MULTIPLE_ANSWER,
4930
                                MULTIPLE_ANSWER_COMBINATION,
4931
                                GLOBAL_MULTIPLE_ANSWER,
4932
                                READING_COMPREHENSION,
4933
                            ]
4934
                        )) {
4935
                            ExerciseShowFunctions::display_unique_or_multiple_answer(
4936
                                $this,
4937
                                $feedback_type,
4938
                                $answerType,
4939
                                $studentChoice,
4940
                                $answer,
4941
                                $answerComment,
4942
                                $answerCorrect,
4943
                                0,
4944
                                0,
4945
                                0,
4946
                                $results_disabled,
4947
                                $showTotalScoreAndUserChoicesInLastAttempt,
4948
                                $this->export
4949
                            );
4950
                        } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE) {
4951
                            ExerciseShowFunctions::display_multiple_answer_true_false(
4952
                                $this,
4953
                                $feedback_type,
4954
                                $answerType,
4955
                                $studentChoice,
4956
                                $answer,
4957
                                $answerComment,
4958
                                $answerCorrect,
4959
                                0,
4960
                                $questionId,
4961
                                0,
4962
                                $results_disabled,
4963
                                $showTotalScoreAndUserChoicesInLastAttempt
4964
                            );
4965
                        } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
4966
                            ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
4967
                                $this,
4968
                                $feedback_type,
4969
                                $studentChoice,
4970
                                $studentChoiceDegree,
4971
                                $answer,
4972
                                $answerComment,
4973
                                $answerCorrect,
4974
                                $questionId,
4975
                                $results_disabled
4976
                            );
4977
                        } elseif ($answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE) {
4978
                            ExerciseShowFunctions::display_multiple_answer_combination_true_false(
4979
                                $this,
4980
                                $feedback_type,
4981
                                $answerType,
4982
                                $studentChoice,
4983
                                $answer,
4984
                                $answerComment,
4985
                                $answerCorrect,
4986
                                0,
4987
                                0,
4988
                                0,
4989
                                $results_disabled,
4990
                                $showTotalScoreAndUserChoicesInLastAttempt
4991
                            );
4992
                        } elseif ($answerType == FILL_IN_BLANKS) {
4993
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
4994
                                $this,
4995
                                $feedback_type,
4996
                                $answer,
4997
                                0,
4998
                                0,
4999
                                $results_disabled,
5000
                                '',
5001
                                $showTotalScoreAndUserChoicesInLastAttempt
5002
                            );
5003
                        } elseif ($answerType == CALCULATED_ANSWER) {
5004
                            ExerciseShowFunctions::display_calculated_answer(
5005
                                $this,
5006
                                $feedback_type,
5007
                                $answer,
5008
                                0,
5009
                                0,
5010
                                $results_disabled,
5011
                                $showTotalScoreAndUserChoicesInLastAttempt,
5012
                                $expectedAnswer,
5013
                                $calculatedChoice,
5014
                                $calculatedStatus
5015
                            );
5016
                        } elseif ($answerType == FREE_ANSWER) {
5017
                            ExerciseShowFunctions::display_free_answer(
5018
                                $feedback_type,
5019
                                $choice,
5020
                                $exeId,
5021
                                $questionId,
5022
                                $questionScore,
5023
                                $results_disabled
5024
                            );
5025
                        } elseif ($answerType == ORAL_EXPRESSION) {
5026
                            // to store the details of open questions in an array to be used in mail
5027
                            /** @var OralExpression $objQuestionTmp */
5028
                            ExerciseShowFunctions::display_oral_expression_answer(
5029
                                $feedback_type,
5030
                                $choice,
5031
                                0,
5032
                                0,
5033
                                $objQuestionTmp->getFileUrl(true),
5034
                                $results_disabled,
5035
                                $questionScore
5036
                            );
5037
                        } elseif ($answerType == HOT_SPOT) {
5038
                            $correctAnswerId = 0;
5039
                            /** @var TrackEHotspot $hotspot */
5040
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5041
                                if ($hotspot->getHotspotAnswerId() == $answerIid) {
5042
                                    break;
5043
                                }
5044
                            }
5045
5046
                            // force to show whether the choice is correct or not
5047
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
5048
                            ExerciseShowFunctions::display_hotspot_answer(
5049
                                $this,
5050
                                $feedback_type,
5051
                                ++$correctAnswerId,
5052
                                $answer,
5053
                                $studentChoice,
5054
                                $answerComment,
5055
                                $results_disabled,
5056
                                $correctAnswerId,
5057
                                $showTotalScoreAndUserChoicesInLastAttempt
5058
                            );
5059
                        } elseif ($answerType == HOT_SPOT_ORDER) {
5060
                            ExerciseShowFunctions::display_hotspot_order_answer(
5061
                                $feedback_type,
5062
                                $answerId,
5063
                                $answer,
5064
                                $studentChoice,
5065
                                $answerComment
5066
                            );
5067
                        } elseif ($answerType == HOT_SPOT_DELINEATION) {
5068
                            $user_answer = $_SESSION['exerciseResultCoordinates'][$questionId];
5069
5070
                            // Round-up the coordinates
5071
                            $coords = explode('/', $user_answer);
5072
                            $coords = array_filter($coords);
5073
                            $user_array = '';
5074
                            foreach ($coords as $coord) {
5075
                                if (!empty($coord)) {
5076
                                    $parts = explode(';', $coord);
5077
                                    if (!empty($parts)) {
5078
                                        $user_array .= round($parts[0]).';'.round($parts[1]).'/';
5079
                                    }
5080
                                }
5081
                            }
5082
                            $user_array = substr($user_array, 0, -1) ?: '';
5083
                            if ($next) {
5084
                                $user_answer = $user_array;
5085
                                // We compare only the delineation not the other points
5086
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5087
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5088
5089
                                // Calculating the area
5090
                                $poly_user = convert_coordinates($user_answer, '/');
5091
                                $poly_answer = convert_coordinates($answer_question, '|');
5092
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5093
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5094
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5095
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5096
5097
                                $overlap = $poly_results['both'];
5098
                                $poly_answer_area = $poly_results['s1'];
5099
                                $poly_user_area = $poly_results['s2'];
5100
                                $missing = $poly_results['s1Only'];
5101
                                $excess = $poly_results['s2Only'];
5102
5103
                                // //this is an area in pixels
5104
                                if ($debug > 0) {
5105
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5106
                                }
5107
5108
                                if ($overlap < 1) {
5109
                                    // Shortcut to avoid complicated calculations
5110
                                    $final_overlap = 0;
5111
                                    $final_missing = 100;
5112
                                    $final_excess = 100;
5113
                                } else {
5114
                                    // the final overlap is the percentage of the initial polygon
5115
                                    // that is overlapped by the user's polygon
5116
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5117
                                    if ($debug > 1) {
5118
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap, 0);
5119
                                    }
5120
                                    // the final missing area is the percentage of the initial polygon
5121
                                    // that is not overlapped by the user's polygon
5122
                                    $final_missing = 100 - $final_overlap;
5123
                                    if ($debug > 1) {
5124
                                        error_log(__LINE__.' - Final missing is '.$final_missing, 0);
5125
                                    }
5126
                                    // the final excess area is the percentage of the initial polygon's size
5127
                                    // that is covered by the user's polygon outside of the initial polygon
5128
                                    $final_excess = round((((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100);
5129
                                    if ($debug > 1) {
5130
                                        error_log(__LINE__.' - Final excess is '.$final_excess, 0);
5131
                                    }
5132
                                }
5133
5134
                                // Checking the destination parameters parsing the "@@"
5135
                                $destination_items = explode('@@', $answerDestination);
5136
                                $threadhold_total = $destination_items[0];
5137
                                $threadhold_items = explode(';', $threadhold_total);
5138
                                $threadhold1 = $threadhold_items[0]; // overlap
5139
                                $threadhold2 = $threadhold_items[1]; // excess
5140
                                $threadhold3 = $threadhold_items[2]; // missing
5141
5142
                                // if is delineation
5143
                                if ($answerId === 1) {
5144
                                    //setting colors
5145
                                    if ($final_overlap >= $threadhold1) {
5146
                                        $overlap_color = true;
5147
                                    }
5148
                                    if ($final_excess <= $threadhold2) {
5149
                                        $excess_color = true;
5150
                                    }
5151
                                    if ($final_missing <= $threadhold3) {
5152
                                        $missing_color = true;
5153
                                    }
5154
5155
                                    // if pass
5156
                                    if ($final_overlap >= $threadhold1 &&
5157
                                        $final_missing <= $threadhold3 &&
5158
                                        $final_excess <= $threadhold2
5159
                                    ) {
5160
                                        $next = 1; //go to the oars
5161
                                        $result_comment = get_lang('Acceptable');
5162
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5163
                                    } else {
5164
                                        $next = 0;
5165
                                        $result_comment = get_lang('Unacceptable');
5166
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5167
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5168
                                        // checking the destination parameters parsing the "@@"
5169
                                        $destination_items = explode('@@', $answerDestination);
5170
                                    }
5171
                                } elseif ($answerId > 1) {
5172
                                    if ($objAnswerTmp->selectHotspotType($answerId) == 'noerror') {
5173
                                        if ($debug > 0) {
5174
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5175
                                        }
5176
                                        //type no error shouldn't be treated
5177
                                        $next = 1;
5178
                                        continue;
5179
                                    }
5180
                                    if ($debug > 0) {
5181
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5182
                                    }
5183
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5184
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5185
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5186
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5187
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5188
5189
                                    if ($overlap == false) {
5190
                                        //all good, no overlap
5191
                                        $next = 1;
5192
                                        continue;
5193
                                    } else {
5194
                                        if ($debug > 0) {
5195
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5196
                                        }
5197
                                        $organs_at_risk_hit++;
5198
                                        //show the feedback
5199
                                        $next = 0;
5200
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5201
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5202
5203
                                        $destination_items = explode('@@', $answerDestination);
5204
                                        $try_hotspot = $destination_items[1];
5205
                                        $lp_hotspot = $destination_items[2];
5206
                                        $select_question_hotspot = $destination_items[3];
5207
                                        $url_hotspot = $destination_items[4];
5208
                                    }
5209
                                }
5210
                            } else {
5211
                                // the first delineation feedback
5212
                                if ($debug > 0) {
5213
                                    error_log(__LINE__.' first', 0);
5214
                                }
5215
                            }
5216
                        } elseif (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
5217
                            echo '<tr>';
5218
                            echo Display::tag('td', $answerMatching[$answerId]);
5219
                            echo Display::tag(
5220
                                'td',
5221
                                "$user_answer / ".Display::tag(
5222
                                    'strong',
5223
                                    $answerMatching[$answerCorrect],
5224
                                    ['style' => 'color: #008000; font-weight: bold;']
5225
                                )
5226
                            );
5227
                            echo '</tr>';
5228
                        } elseif ($answerType == ANNOTATION) {
5229
                            ExerciseShowFunctions::displayAnnotationAnswer(
5230
                                $feedback_type,
5231
                                $exeId,
5232
                                $questionId,
5233
                                $questionScore,
5234
                                $results_disabled
5235
                            );
5236
                        }
5237
                    }
5238
                } else {
5239
                    if ($debug) {
5240
                        error_log('Showing questions $from '.$from);
5241
                    }
5242
5243
                    switch ($answerType) {
5244
                        case UNIQUE_ANSWER:
5245
                        case UNIQUE_ANSWER_IMAGE:
5246
                        case UNIQUE_ANSWER_NO_OPTION:
5247
                        case MULTIPLE_ANSWER:
5248
                        case GLOBAL_MULTIPLE_ANSWER:
5249
                        case MULTIPLE_ANSWER_COMBINATION:
5250
                        case READING_COMPREHENSION:
5251
                            if ($answerId == 1) {
5252
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5253
                                    $this,
5254
                                    $feedback_type,
5255
                                    $answerType,
5256
                                    $studentChoice,
5257
                                    $answer,
5258
                                    $answerComment,
5259
                                    $answerCorrect,
5260
                                    $exeId,
5261
                                    $questionId,
5262
                                    $answerId,
5263
                                    $results_disabled,
5264
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5265
                                    $this->export
5266
                                );
5267
                            } else {
5268
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5269
                                    $this,
5270
                                    $feedback_type,
5271
                                    $answerType,
5272
                                    $studentChoice,
5273
                                    $answer,
5274
                                    $answerComment,
5275
                                    $answerCorrect,
5276
                                    $exeId,
5277
                                    $questionId,
5278
                                    '',
5279
                                    $results_disabled,
5280
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5281
                                    $this->export
5282
                                );
5283
                            }
5284
                            break;
5285
                        case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
5286
                            if ($answerId == 1) {
5287
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5288
                                    $this,
5289
                                    $feedback_type,
5290
                                    $answerType,
5291
                                    $studentChoice,
5292
                                    $answer,
5293
                                    $answerComment,
5294
                                    $answerCorrect,
5295
                                    $exeId,
5296
                                    $questionId,
5297
                                    $answerId,
5298
                                    $results_disabled,
5299
                                    $showTotalScoreAndUserChoicesInLastAttempt
5300
                                );
5301
                            } else {
5302
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5303
                                    $this,
5304
                                    $feedback_type,
5305
                                    $answerType,
5306
                                    $studentChoice,
5307
                                    $answer,
5308
                                    $answerComment,
5309
                                    $answerCorrect,
5310
                                    $exeId,
5311
                                    $questionId,
5312
                                    '',
5313
                                    $results_disabled,
5314
                                    $showTotalScoreAndUserChoicesInLastAttempt
5315
                                );
5316
                            }
5317
                            break;
5318
                        case MULTIPLE_ANSWER_TRUE_FALSE:
5319
                            if ($answerId == 1) {
5320
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5321
                                    $this,
5322
                                    $feedback_type,
5323
                                    $answerType,
5324
                                    $studentChoice,
5325
                                    $answer,
5326
                                    $answerComment,
5327
                                    $answerCorrect,
5328
                                    $exeId,
5329
                                    $questionId,
5330
                                    $answerId,
5331
                                    $results_disabled,
5332
                                    $showTotalScoreAndUserChoicesInLastAttempt
5333
                                );
5334
                            } else {
5335
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5336
                                    $this,
5337
                                    $feedback_type,
5338
                                    $answerType,
5339
                                    $studentChoice,
5340
                                    $answer,
5341
                                    $answerComment,
5342
                                    $answerCorrect,
5343
                                    $exeId,
5344
                                    $questionId,
5345
                                    '',
5346
                                    $results_disabled,
5347
                                    $showTotalScoreAndUserChoicesInLastAttempt
5348
                                );
5349
                            }
5350
                            break;
5351
                        case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
5352
                            if ($answerId == 1) {
5353
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5354
                                    $this,
5355
                                    $feedback_type,
5356
                                    $studentChoice,
5357
                                    $studentChoiceDegree,
5358
                                    $answer,
5359
                                    $answerComment,
5360
                                    $answerCorrect,
5361
                                    $questionId,
5362
                                    $results_disabled
5363
                                );
5364
                            } else {
5365
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5366
                                    $this,
5367
                                    $feedback_type,
5368
                                    $studentChoice,
5369
                                    $studentChoiceDegree,
5370
                                    $answer,
5371
                                    $answerComment,
5372
                                    $answerCorrect,
5373
                                    $questionId,
5374
                                    $results_disabled
5375
                                );
5376
                            }
5377
                            break;
5378
                        case FILL_IN_BLANKS:
5379
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5380
                                $this,
5381
                                $feedback_type,
5382
                                $answer,
5383
                                $exeId,
5384
                                $questionId,
5385
                                $results_disabled,
5386
                                $str,
5387
                                $showTotalScoreAndUserChoicesInLastAttempt
5388
                            );
5389
                            break;
5390
                        case CALCULATED_ANSWER:
5391
                            ExerciseShowFunctions::display_calculated_answer(
5392
                                $this,
5393
                                $feedback_type,
5394
                                $answer,
5395
                                $exeId,
5396
                                $questionId,
5397
                                $results_disabled,
5398
                                '',
5399
                                $showTotalScoreAndUserChoicesInLastAttempt
5400
                            );
5401
                            break;
5402
                        case FREE_ANSWER:
5403
                            echo ExerciseShowFunctions::display_free_answer(
5404
                                $feedback_type,
5405
                                $choice,
5406
                                $exeId,
5407
                                $questionId,
5408
                                $questionScore,
5409
                                $results_disabled
5410
                            );
5411
                            break;
5412
                        case ORAL_EXPRESSION:
5413
                            echo '<tr>
5414
                                <td valign="top">'.
5415
                                ExerciseShowFunctions::display_oral_expression_answer(
5416
                                    $feedback_type,
5417
                                    $choice,
5418
                                    $exeId,
5419
                                    $questionId,
5420
                                    $objQuestionTmp->getFileUrl(),
5421
                                    $results_disabled,
5422
                                    $questionScore
5423
                                ).'</td>
5424
                                </tr>
5425
                                </table>';
5426
                            break;
5427
                        case HOT_SPOT:
5428
                            $correctAnswerId = 0;
5429
                            /** @var TrackEHotspot $hotspot */
5430
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5431
                                if ($hotspot->getHotspotAnswerId() == $foundAnswerId) {
5432
                                    break;
5433
                                }
5434
                            }
5435
5436
                            ExerciseShowFunctions::display_hotspot_answer(
5437
                                $this,
5438
                                $feedback_type,
5439
                                ++$correctAnswerId,
5440
                                $answer,
5441
                                $studentChoice,
5442
                                $answerComment,
5443
                                $results_disabled,
5444
                                $correctAnswerId,
5445
                                $showTotalScoreAndUserChoicesInLastAttempt
5446
                            );
5447
                            break;
5448
                        case HOT_SPOT_DELINEATION:
5449
                            $user_answer = $user_array;
5450
                            if ($next) {
5451
                                $user_answer = $user_array;
5452
                                // we compare only the delineation not the other points
5453
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5454
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5455
5456
                                // calculating the area
5457
                                $poly_user = convert_coordinates($user_answer, '/');
5458
                                $poly_answer = convert_coordinates($answer_question, '|');
5459
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5460
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5461
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5462
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5463
5464
                                $overlap = $poly_results['both'];
5465
                                $poly_answer_area = $poly_results['s1'];
5466
                                $poly_user_area = $poly_results['s2'];
5467
                                $missing = $poly_results['s1Only'];
5468
                                $excess = $poly_results['s2Only'];
5469
                                if ($debug > 0) {
5470
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5471
                                }
5472
                                if ($overlap < 1) {
5473
                                    //shortcut to avoid complicated calculations
5474
                                    $final_overlap = 0;
5475
                                    $final_missing = 100;
5476
                                    $final_excess = 100;
5477
                                } else {
5478
                                    // the final overlap is the percentage of the initial polygon
5479
                                    // that is overlapped by the user's polygon
5480
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5481
5482
                                    // the final missing area is the percentage of the initial polygon that
5483
                                    // is not overlapped by the user's polygon
5484
                                    $final_missing = 100 - $final_overlap;
5485
                                    // the final excess area is the percentage of the initial polygon's size that is
5486
                                    // covered by the user's polygon outside of the initial polygon
5487
                                    $final_excess = round((((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100);
5488
5489
                                    if ($debug > 1) {
5490
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap);
5491
                                        error_log(__LINE__.' - Final excess is '.$final_excess);
5492
                                        error_log(__LINE__.' - Final missing is '.$final_missing);
5493
                                    }
5494
                                }
5495
5496
                                // Checking the destination parameters parsing the "@@"
5497
                                $destination_items = explode('@@', $answerDestination);
5498
                                $threadhold_total = $destination_items[0];
5499
                                $threadhold_items = explode(';', $threadhold_total);
5500
                                $threadhold1 = $threadhold_items[0]; // overlap
5501
                                $threadhold2 = $threadhold_items[1]; // excess
5502
                                $threadhold3 = $threadhold_items[2]; //missing
5503
                                // if is delineation
5504
                                if ($answerId === 1) {
5505
                                    //setting colors
5506
                                    if ($final_overlap >= $threadhold1) {
5507
                                        $overlap_color = true;
5508
                                    }
5509
                                    if ($final_excess <= $threadhold2) {
5510
                                        $excess_color = true;
5511
                                    }
5512
                                    if ($final_missing <= $threadhold3) {
5513
                                        $missing_color = true;
5514
                                    }
5515
5516
                                    // if pass
5517
                                    if ($final_overlap >= $threadhold1 &&
5518
                                        $final_missing <= $threadhold3 &&
5519
                                        $final_excess <= $threadhold2
5520
                                    ) {
5521
                                        $next = 1; //go to the oars
5522
                                        $result_comment = get_lang('Acceptable');
5523
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5524
                                    } else {
5525
                                        $next = 0;
5526
                                        $result_comment = get_lang('Unacceptable');
5527
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5528
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5529
                                        //checking the destination parameters parsing the "@@"
5530
                                        $destination_items = explode('@@', $answerDestination);
5531
                                    }
5532
                                } elseif ($answerId > 1) {
5533
                                    if ($objAnswerTmp->selectHotspotType($answerId) === 'noerror') {
5534
                                        if ($debug > 0) {
5535
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5536
                                        }
5537
                                        //type no error shouldn't be treated
5538
                                        $next = 1;
5539
                                        break;
5540
                                    }
5541
                                    if ($debug > 0) {
5542
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5543
                                    }
5544
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5545
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5546
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5547
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5548
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5549
5550
                                    if ($overlap == false) {
5551
                                        //all good, no overlap
5552
                                        $next = 1;
5553
                                        break;
5554
                                    } else {
5555
                                        if ($debug > 0) {
5556
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5557
                                        }
5558
                                        $organs_at_risk_hit++;
5559
                                        //show the feedback
5560
                                        $next = 0;
5561
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5562
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5563
5564
                                        $destination_items = explode('@@', $answerDestination);
5565
                                        $try_hotspot = $destination_items[1];
5566
                                        $lp_hotspot = $destination_items[2];
5567
                                        $select_question_hotspot = $destination_items[3];
5568
                                        $url_hotspot = $destination_items[4];
5569
                                    }
5570
                                }
5571
                            }
5572
                            break;
5573
                        case HOT_SPOT_ORDER:
5574
                            ExerciseShowFunctions::display_hotspot_order_answer(
5575
                                $feedback_type,
5576
                                $answerId,
5577
                                $answer,
5578
                                $studentChoice,
5579
                                $answerComment
5580
                            );
5581
                            break;
5582
                        case DRAGGABLE:
5583
                        case MATCHING_DRAGGABLE:
5584
                        case MATCHING:
5585
                            echo '<tr>';
5586
                            echo Display::tag('td', $answerMatching[$answerId]);
5587
                            echo Display::tag(
5588
                                'td',
5589
                                "$user_answer / ".Display::tag(
5590
                                    'strong',
5591
                                    $answerMatching[$answerCorrect],
5592
                                    ['style' => 'color: #008000; font-weight: bold;']
5593
                                )
5594
                            );
5595
                            echo '</tr>';
5596
                            break;
5597
                        case ANNOTATION:
5598
                            ExerciseShowFunctions::displayAnnotationAnswer(
5599
                                $feedback_type,
5600
                                $exeId,
5601
                                $questionId,
5602
                                $questionScore,
5603
                                $results_disabled
5604
                            );
5605
                            break;
5606
                    }
5607
                }
5608
            }
5609
        } // end for that loops over all answers of the current question
5610
5611
        if ($debug) {
5612
            error_log('-- End answer loop --');
5613
        }
5614
5615
        $final_answer = true;
5616
5617
        foreach ($real_answers as $my_answer) {
5618
            if (!$my_answer) {
5619
                $final_answer = false;
5620
            }
5621
        }
5622
5623
        //we add the total score after dealing with the answers
5624
        if ($answerType == MULTIPLE_ANSWER_COMBINATION ||
5625
            $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE
5626
        ) {
5627
            if ($final_answer) {
5628
                //getting only the first score where we save the weight of all the question
5629
                $answerWeighting = $objAnswerTmp->selectWeighting(1);
5630
                $questionScore += $answerWeighting;
5631
            }
5632
        }
5633
5634
        $extra_data = [
5635
            'final_overlap' => $final_overlap,
5636
            'final_missing' => $final_missing,
5637
            'final_excess' => $final_excess,
5638
            'overlap_color' => $overlap_color,
5639
            'missing_color' => $missing_color,
5640
            'excess_color' => $excess_color,
5641
            'threadhold1' => $threadhold1,
5642
            'threadhold2' => $threadhold2,
5643
            'threadhold3' => $threadhold3,
5644
        ];
5645
5646
        if ($from === 'exercise_result') {
5647
            // if answer is hotspot. To the difference of exercise_show.php,
5648
            //  we use the results from the session (from_db=0)
5649
            // TODO Change this, because it is wrong to show the user
5650
            //  some results that haven't been stored in the database yet
5651
            if ($answerType == HOT_SPOT || $answerType == HOT_SPOT_ORDER || $answerType == HOT_SPOT_DELINEATION) {
5652
                if ($debug) {
5653
                    error_log('$from AND this is a hotspot kind of question ');
5654
                }
5655
                if ($answerType === HOT_SPOT_DELINEATION) {
5656
                    if ($showHotSpotDelineationTable) {
5657
                        if (!is_numeric($final_overlap)) {
5658
                            $final_overlap = 0;
5659
                        }
5660
                        if (!is_numeric($final_missing)) {
5661
                            $final_missing = 0;
5662
                        }
5663
                        if (!is_numeric($final_excess)) {
5664
                            $final_excess = 0;
5665
                        }
5666
5667
                        if ($final_overlap > 100) {
5668
                            $final_overlap = 100;
5669
                        }
5670
5671
                        $table_resume = '<table class="table table-hover table-striped data_table">
5672
                                <tr class="row_odd" >
5673
                                    <td></td>
5674
                                    <td ><b>'.get_lang('Requirements').'</b></td>
5675
                                    <td><b>'.get_lang('YourAnswer').'</b></td>
5676
                                </tr>
5677
                                <tr class="row_even">
5678
                                    <td><b>'.get_lang('Overlap').'</b></td>
5679
                                    <td>'.get_lang('Min').' '.$threadhold1.'</td>
5680
                                    <td class="text-right '.($overlap_color ? 'text-success' : 'text-danger').'">'
5681
                            .(($final_overlap < 0) ? 0 : intval($final_overlap)).'</td>
5682
                                </tr>
5683
                                <tr>
5684
                                    <td><b>'.get_lang('Excess').'</b></td>
5685
                                    <td>'.get_lang('Max').' '.$threadhold2.'</td>
5686
                                    <td class="text-right '.($excess_color ? 'text-success' : 'text-danger').'">'
5687
                            .(($final_excess < 0) ? 0 : intval($final_excess)).'</td>
5688
                                </tr>
5689
                                <tr class="row_even">
5690
                                    <td><b>'.get_lang('Missing').'</b></td>
5691
                                    <td>'.get_lang('Max').' '.$threadhold3.'</td>
5692
                                    <td class="text-right '.($missing_color ? 'text-success' : 'text-danger').'">'
5693
                            .(($final_missing < 0) ? 0 : intval($final_missing)).'</td>
5694
                                </tr>
5695
                            </table>';
5696
                        if ($next == 0) {
5697
                            /*$try = $try_hotspot;
5698
                            $lp = $lp_hotspot;
5699
                            $destinationid = $select_question_hotspot;
5700
                            $url = $url_hotspot;*/
5701
                        } else {
5702
                            $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
5703
                            $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
5704
                        }
5705
5706
                        $message = '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>
5707
                                    <p style="text-align:center">';
5708
                        $message .= '<p>'.get_lang('YourDelineation').'</p>';
5709
                        $message .= $table_resume;
5710
                        $message .= '<br />'.get_lang('ResultIs').' '.$result_comment.'<br />';
5711
                        if ($organs_at_risk_hit > 0) {
5712
                            $message .= '<p><b>'.get_lang('OARHit').'</b></p>';
5713
                        }
5714
                        $message .= '<p>'.$comment.'</p>';
5715
                        echo $message;
5716
5717
                        $_SESSION['hotspot_delineation_result'][$this->selectId()][$questionId][0] = $message;
5718
                        $_SESSION['hotspot_delineation_result'][$this->selectId()][$questionId][1] = $_SESSION['exerciseResultCoordinates'][$questionId];
5719
                    } else {
5720
                        echo $hotspot_delineation_result[0];
5721
                    }
5722
5723
                    // Save the score attempts
5724
                    if (1) {
5725
                        //getting the answer 1 or 0 comes from exercise_submit_modal.php
5726
                        $final_answer = isset($hotspot_delineation_result[1]) ? $hotspot_delineation_result[1] : '';
5727
                        if ($final_answer == 0) {
5728
                            $questionScore = 0;
5729
                        }
5730
                        // we always insert the answer_id 1 = delineation
5731
                        Event::saveQuestionAttempt($questionScore, 1, $quesId, $exeId, 0);
5732
                        //in delineation mode, get the answer from $hotspot_delineation_result[1]
5733
                        $hotspotValue = isset($hotspot_delineation_result[1]) ? (int) $hotspot_delineation_result[1] === 1 ? 1 : 0 : 0;
5734
                        Event::saveExerciseAttemptHotspot(
5735
                            $exeId,
5736
                            $quesId,
5737
                            1,
5738
                            $hotspotValue,
5739
                            $exerciseResultCoordinates[$quesId]
5740
                        );
5741
                    } else {
5742
                        if ($final_answer == 0) {
5743
                            $questionScore = 0;
5744
                            $answer = 0;
5745
                            Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0);
5746
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5747
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5748
                                    Event::saveExerciseAttemptHotspot(
5749
                                        $exeId,
5750
                                        $quesId,
5751
                                        $idx,
5752
                                        0,
5753
                                        $val
5754
                                    );
5755
                                }
5756
                            }
5757
                        } else {
5758
                            Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0);
5759
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5760
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5761
                                    $hotspotValue = (int) $choice[$idx] === 1 ? 1 : 0;
5762
                                    Event::saveExerciseAttemptHotspot(
5763
                                        $exeId,
5764
                                        $quesId,
5765
                                        $idx,
5766
                                        $hotspotValue,
5767
                                        $val
5768
                                    );
5769
                                }
5770
                            }
5771
                        }
5772
                    }
5773
                }
5774
            }
5775
5776
            $relPath = api_get_path(WEB_CODE_PATH);
5777
5778
            if ($answerType == HOT_SPOT || $answerType == HOT_SPOT_ORDER) {
5779
                // We made an extra table for the answers
5780
                if ($show_result) {
5781
                    echo '</table></td></tr>';
5782
                    echo "
5783
                        <tr>
5784
                            <td>
5785
                                <p><em>".get_lang('HotSpot')."</em></p>
5786
                                <div id=\"hotspot-solution-$questionId\"></div>
5787
                                <script>
5788
                                    $(function() {
5789
                                        new HotspotQuestion({
5790
                                            questionId: $questionId,
5791
                                            exerciseId: {$this->id},
5792
                                            exeId: $exeId,
5793
                                            selector: '#hotspot-solution-$questionId',
5794
                                            for: 'solution',
5795
                                            relPath: '$relPath'
5796
                                        });
5797
                                    });
5798
                                </script>
5799
                            </td>
5800
                        </tr>
5801
                    ";
5802
                }
5803
            } elseif ($answerType == ANNOTATION) {
5804
                if ($show_result) {
5805
                    echo '
5806
                        <p><em>'.get_lang('Annotation').'</em></p>
5807
                        <div id="annotation-canvas-'.$questionId.'"></div>
5808
                        <script>
5809
                            AnnotationQuestion({
5810
                                questionId: parseInt('.$questionId.'),
5811
                                exerciseId: parseInt('.$exeId.'),
5812
                                relPath: \''.$relPath.'\',
5813
                                courseId: parseInt('.$course_id.')
5814
                            });
5815
                        </script>
5816
                    ';
5817
                }
5818
            }
5819
5820
            if ($show_result && $answerType != ANNOTATION) {
5821
                echo '</table>';
5822
            }
5823
        }
5824
        unset($objAnswerTmp);
5825
5826
        $totalWeighting += $questionWeighting;
5827
        // Store results directly in the database
5828
        // For all in one page exercises, the results will be
5829
        // stored by exercise_results.php (using the session)
5830
        if ($save_results) {
5831
            if ($debug) {
5832
                error_log("Save question results $save_results");
5833
                error_log('choice: ');
5834
                error_log(print_r($choice, 1));
5835
            }
5836
5837
            if (empty($choice)) {
5838
                $choice = 0;
5839
            }
5840
            // with certainty degree
5841
            if (empty($choiceDegreeCertainty)) {
5842
                $choiceDegreeCertainty = 0;
5843
            }
5844
            if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE ||
5845
                $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE ||
5846
                $answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY
5847
            ) {
5848
                if ($choice != 0) {
5849
                    $reply = array_keys($choice);
5850
                    $countReply = count($reply);
5851
                    for ($i = 0; $i < $countReply; $i++) {
5852
                        $chosenAnswer = $reply[$i];
5853
                        if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
5854
                            if ($choiceDegreeCertainty != 0) {
5855
                                $replyDegreeCertainty = array_keys($choiceDegreeCertainty);
5856
                                $answerDegreeCertainty = isset($replyDegreeCertainty[$i]) ? $replyDegreeCertainty[$i] : '';
5857
                                $answerValue = isset($choiceDegreeCertainty[$answerDegreeCertainty]) ? $choiceDegreeCertainty[$answerDegreeCertainty] : '';
5858
                                Event::saveQuestionAttempt(
5859
                                    $questionScore,
5860
                                    $chosenAnswer.':'.$choice[$chosenAnswer].':'.$answerValue,
5861
                                    $quesId,
5862
                                    $exeId,
5863
                                    $i,
5864
                                    $this->id,
5865
                                    $updateResults
5866
                                );
5867
                            }
5868
                        } else {
5869
                            Event::saveQuestionAttempt(
5870
                                $questionScore,
5871
                                $chosenAnswer.':'.$choice[$chosenAnswer],
5872
                                $quesId,
5873
                                $exeId,
5874
                                $i,
5875
                                $this->id,
5876
                                $updateResults
5877
                            );
5878
                        }
5879
                        if ($debug) {
5880
                            error_log('result =>'.$questionScore.' '.$chosenAnswer.':'.$choice[$chosenAnswer]);
5881
                        }
5882
                    }
5883
                } else {
5884
                    Event::saveQuestionAttempt(
5885
                        $questionScore,
5886
                        0,
5887
                        $quesId,
5888
                        $exeId,
5889
                        0,
5890
                        $this->id
5891
                    );
5892
                }
5893
            } elseif ($answerType == MULTIPLE_ANSWER || $answerType == GLOBAL_MULTIPLE_ANSWER) {
5894
                if ($choice != 0) {
5895
                    $reply = array_keys($choice);
5896
                    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...
5897
                        $ans = $reply[$i];
5898
                        Event::saveQuestionAttempt($questionScore, $ans, $quesId, $exeId, $i, $this->id);
5899
                    }
5900
                } else {
5901
                    Event::saveQuestionAttempt($questionScore, 0, $quesId, $exeId, 0, $this->id);
5902
                }
5903
            } elseif ($answerType == MULTIPLE_ANSWER_COMBINATION) {
5904
                if ($choice != 0) {
5905
                    $reply = array_keys($choice);
5906
                    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...
5907
                        $ans = $reply[$i];
5908
                        Event::saveQuestionAttempt($questionScore, $ans, $quesId, $exeId, $i, $this->id);
5909
                    }
5910
                } else {
5911
                    Event::saveQuestionAttempt(
5912
                        $questionScore,
5913
                        0,
5914
                        $quesId,
5915
                        $exeId,
5916
                        0,
5917
                        $this->id
5918
                    );
5919
                }
5920
            } elseif (in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE])) {
5921
                if (isset($matching)) {
5922
                    foreach ($matching as $j => $val) {
5923
                        Event::saveQuestionAttempt(
5924
                            $questionScore,
5925
                            $val,
5926
                            $quesId,
5927
                            $exeId,
5928
                            $j,
5929
                            $this->id
5930
                        );
5931
                    }
5932
                }
5933
            } elseif ($answerType == FREE_ANSWER) {
5934
                $answer = $choice;
5935
                Event::saveQuestionAttempt(
5936
                    $questionScore,
5937
                    $answer,
5938
                    $quesId,
5939
                    $exeId,
5940
                    0,
5941
                    $this->id
5942
                );
5943
            } elseif ($answerType == ORAL_EXPRESSION) {
5944
                $answer = $choice;
5945
                Event::saveQuestionAttempt(
5946
                    $questionScore,
5947
                    $answer,
5948
                    $quesId,
5949
                    $exeId,
5950
                    0,
5951
                    $this->id,
5952
                    false,
5953
                    $objQuestionTmp->getAbsoluteFilePath()
5954
                );
5955
            } elseif (
5956
                in_array(
5957
                    $answerType,
5958
                    [UNIQUE_ANSWER, UNIQUE_ANSWER_IMAGE, UNIQUE_ANSWER_NO_OPTION, READING_COMPREHENSION]
5959
                )
5960
            ) {
5961
                $answer = $choice;
5962
                Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0, $this->id);
5963
            } elseif ($answerType == HOT_SPOT || $answerType == ANNOTATION) {
5964
                $answer = [];
5965
                if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) {
5966
                    if ($debug) {
5967
                        error_log('Checking result coordinates');
5968
                    }
5969
                    Database::delete(
5970
                        Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
5971
                        [
5972
                            'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
5973
                                $exeId,
5974
                                $questionId,
5975
                                api_get_course_int_id(),
5976
                            ],
5977
                        ]
5978
                    );
5979
5980
                    foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
5981
                        $answer[] = $val;
5982
                        $hotspotValue = (int) $choice[$idx] === 1 ? 1 : 0;
5983
                        if ($debug) {
5984
                            error_log('Hotspot value: '.$hotspotValue);
5985
                        }
5986
                        Event::saveExerciseAttemptHotspot(
5987
                            $exeId,
5988
                            $quesId,
5989
                            $idx,
5990
                            $hotspotValue,
5991
                            $val,
5992
                            false,
5993
                            $this->id
5994
                        );
5995
                    }
5996
                } else {
5997
                    if ($debug) {
5998
                        error_log('Empty: exerciseResultCoordinates');
5999
                    }
6000
                }
6001
                Event::saveQuestionAttempt($questionScore, implode('|', $answer), $quesId, $exeId, 0, $this->id);
6002
            } else {
6003
                Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0, $this->id);
6004
            }
6005
        }
6006
6007
        if ($propagate_neg == 0 && $questionScore < 0) {
6008
            $questionScore = 0;
6009
        }
6010
6011
        if ($save_results) {
6012
            $statsTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6013
            $sql = "UPDATE $statsTable SET
6014
                        exe_result = exe_result + ".floatval($questionScore)."
6015
                    WHERE exe_id = $exeId";
6016
            Database::query($sql);
6017
        }
6018
6019
        $return = [
6020
            'score' => $questionScore,
6021
            'weight' => $questionWeighting,
6022
            'extra' => $extra_data,
6023
            'open_question' => $arrques,
6024
            'open_answer' => $arrans,
6025
            'answer_type' => $answerType,
6026
            'generated_oral_file' => $generatedFile,
6027
            'user_answered' => $userAnsweredQuestion,
6028
            'correct_answer_id' => $correctAnswerId,
6029
            'answer_destination' => $answerDestination,
6030
        ];
6031
6032
        return $return;
6033
    }
6034
6035
    /**
6036
     * Sends a notification when a user ends an examn.
6037
     *
6038
     * @param string $type                  'start' or 'end' of an exercise
6039
     * @param array  $question_list_answers
6040
     * @param string $origin
6041
     * @param int    $exe_id
6042
     * @param float  $score
6043
     * @param float  $weight
6044
     *
6045
     * @return bool
6046
     */
6047
    public function send_mail_notification_for_exam(
6048
        $type = 'end',
6049
        $question_list_answers,
6050
        $origin,
6051
        $exe_id,
6052
        $score = null,
6053
        $weight = null
6054
    ) {
6055
        $setting = api_get_course_setting('email_alert_manager_on_new_quiz');
6056
6057
        if (empty($setting) && empty($this->getNotifications())) {
6058
            return false;
6059
        }
6060
6061
        $settingFromExercise = $this->getNotifications();
6062
        if (!empty($settingFromExercise)) {
6063
            $setting = $settingFromExercise;
6064
        }
6065
6066
        // Email configuration settings
6067
        $courseCode = api_get_course_id();
6068
        $courseInfo = api_get_course_info($courseCode);
6069
6070
        if (empty($courseInfo)) {
6071
            return false;
6072
        }
6073
6074
        $sessionId = api_get_session_id();
6075
6076
        $sessionData = '';
6077
        if (!empty($sessionId)) {
6078
            $sessionInfo = api_get_session_info($sessionId);
6079
            if (!empty($sessionInfo)) {
6080
                $sessionData = '<tr>'
6081
                    .'<td>'.get_lang('SessionName').'</td>'
6082
                    .'<td>'.$sessionInfo['name'].'</td>'
6083
                    .'</tr>';
6084
            }
6085
        }
6086
6087
        $sendStart = false;
6088
        $sendEnd = false;
6089
        $sendEndOpenQuestion = false;
6090
        $sendEndOralQuestion = false;
6091
6092
        foreach ($setting as $option) {
6093
            switch ($option) {
6094
                case 0:
6095
                    return false;
6096
                    break;
6097
                case 1: // End
6098
                    if ($type === 'end') {
6099
                        $sendEnd = true;
6100
                    }
6101
                    break;
6102
                case 2: // start
6103
                    if ($type === 'start') {
6104
                        $sendStart = true;
6105
                    }
6106
                    break;
6107
                case 3: // end + open
6108
                    if ($type === 'end') {
6109
                        $sendEndOpenQuestion = true;
6110
                    }
6111
                    break;
6112
                case 4: // end + oral
6113
                    if ($type === 'end') {
6114
                        $sendEndOralQuestion = true;
6115
                    }
6116
                    break;
6117
            }
6118
        }
6119
6120
        $user_info = api_get_user_info(api_get_user_id());
6121
        $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_show.php?'.
6122
            api_get_cidreq(true, true, 'qualify').'&id='.$exe_id.'&action=qualify';
6123
6124
        if (!empty($sessionId)) {
6125
            $addGeneralCoach = true;
6126
            $setting = api_get_configuration_value('block_quiz_mail_notification_general_coach');
6127
            if ($setting === true) {
6128
                $addGeneralCoach = false;
6129
            }
6130
            $teachers = CourseManager::get_coach_list_from_course_code(
6131
                $courseCode,
6132
                $sessionId,
6133
                $addGeneralCoach
6134
            );
6135
        } else {
6136
            $teachers = CourseManager::get_teacher_list_from_course_code($courseCode);
6137
        }
6138
6139
        if ($sendEndOpenQuestion) {
6140
            $this->sendNotificationForOpenQuestions(
6141
                $question_list_answers,
6142
                $origin,
6143
                $user_info,
6144
                $url,
6145
                $teachers
6146
            );
6147
        }
6148
6149
        if ($sendEndOralQuestion) {
6150
            $this->sendNotificationForOralQuestions(
6151
                $question_list_answers,
6152
                $origin,
6153
                $exe_id,
6154
                $user_info,
6155
                $url,
6156
                $teachers
6157
            );
6158
        }
6159
6160
        if (!$sendEnd && !$sendStart) {
6161
            return false;
6162
        }
6163
6164
        $scoreLabel = '';
6165
        if ($sendEnd &&
6166
            api_get_configuration_value('send_score_in_exam_notification_mail_to_manager') == true
6167
        ) {
6168
            $notificationPercentage = api_get_configuration_value('send_notification_score_in_percentage');
6169
            $scoreLabel = ExerciseLib::show_score($score, $weight, $notificationPercentage, true);
6170
            $scoreLabel = "<tr>
6171
                            <td>".get_lang('Score')."</td>
6172
                            <td>&nbsp;$scoreLabel</td>
6173
                        </tr>";
6174
        }
6175
6176
        if ($sendEnd) {
6177
            $msg = get_lang('ExerciseAttempted').'<br /><br />';
6178
        } else {
6179
            $msg = get_lang('StudentStartExercise').'<br /><br />';
6180
        }
6181
6182
        $msg .= get_lang('AttemptDetails').' : <br /><br />
6183
                    <table>
6184
                        <tr>
6185
                            <td>'.get_lang('CourseName').'</td>
6186
                            <td>#course#</td>
6187
                        </tr>
6188
                        '.$sessionData.'
6189
                        <tr>
6190
                            <td>'.get_lang('Exercise').'</td>
6191
                            <td>&nbsp;#exercise#</td>
6192
                        </tr>
6193
                        <tr>
6194
                            <td>'.get_lang('StudentName').'</td>
6195
                            <td>&nbsp;#student_complete_name#</td>
6196
                        </tr>
6197
                        <tr>
6198
                            <td>'.get_lang('StudentEmail').'</td>
6199
                            <td>&nbsp;#email#</td>
6200
                        </tr>
6201
                        '.$scoreLabel.'
6202
                    </table>';
6203
6204
        $variables = [
6205
            '#email#' => $user_info['email'],
6206
            '#exercise#' => $this->exercise,
6207
            '#student_complete_name#' => $user_info['complete_name'],
6208
            '#course#' => Display::url(
6209
                $courseInfo['title'],
6210
                $courseInfo['course_public_url'].'?id_session='.$sessionId
6211
            ),
6212
        ];
6213
6214
        if ($sendEnd) {
6215
            $msg .= '<br /><a href="#url#">'.get_lang('ClickToCommentAndGiveFeedback').'</a>';
6216
            $variables['#url#'] = $url;
6217
        }
6218
6219
        $content = str_replace(array_keys($variables), array_values($variables), $msg);
6220
6221
        if ($sendEnd) {
6222
            $subject = get_lang('ExerciseAttempted');
6223
        } else {
6224
            $subject = get_lang('StudentStartExercise');
6225
        }
6226
6227
        if (!empty($teachers)) {
6228
            foreach ($teachers as $user_id => $teacher_data) {
6229
                MessageManager::send_message_simple(
6230
                    $user_id,
6231
                    $subject,
6232
                    $content
6233
                );
6234
            }
6235
        }
6236
    }
6237
6238
    /**
6239
     * @param array $user_data         result of api_get_user_info()
6240
     * @param array $trackExerciseInfo result of get_stat_track_exercise_info
6241
     * @param bool  $saveUserResult
6242
     * @param bool  $allowSignature
6243
     * @param bool  $allowExportPdf
6244
     *
6245
     * @return string
6246
     */
6247
    public function showExerciseResultHeader(
6248
        $user_data,
6249
        $trackExerciseInfo,
6250
        $saveUserResult,
6251
        $allowSignature = false,
6252
        $allowExportPdf = false
6253
    ) {
6254
        if (api_get_configuration_value('hide_user_info_in_quiz_result')) {
6255
            return '';
6256
        }
6257
6258
        $start_date = null;
6259
        if (isset($trackExerciseInfo['start_date'])) {
6260
            $start_date = api_convert_and_format_date($trackExerciseInfo['start_date']);
6261
        }
6262
        $duration = isset($trackExerciseInfo['duration_formatted']) ? $trackExerciseInfo['duration_formatted'] : null;
6263
        $ip = isset($trackExerciseInfo['user_ip']) ? $trackExerciseInfo['user_ip'] : null;
6264
6265
        if (!empty($user_data)) {
6266
            $userFullName = $user_data['complete_name'];
6267
            if (api_is_teacher() || api_is_platform_admin(true, true)) {
6268
                $userFullName = '<a href="'.$user_data['profile_url'].'" title="'.get_lang('GoToStudentDetails').'">'.
6269
                    $user_data['complete_name'].'</a>';
6270
            }
6271
6272
            $data = [
6273
                'name_url' => $userFullName,
6274
                'complete_name' => $user_data['complete_name'],
6275
                'username' => $user_data['username'],
6276
                'avatar' => $user_data['avatar_medium'],
6277
                'url' => $user_data['profile_url'],
6278
            ];
6279
6280
            if (!empty($user_data['official_code'])) {
6281
                $data['code'] = $user_data['official_code'];
6282
            }
6283
        }
6284
        // Description can be very long and is generally meant to explain
6285
        //   rules *before* the exam. Leaving here to make display easier if
6286
        //   necessary
6287
        /*
6288
        if (!empty($this->description)) {
6289
            $array[] = array('title' => get_lang("Description"), 'content' => $this->description);
6290
        }
6291
        */
6292
        if (!empty($start_date)) {
6293
            $data['start_date'] = $start_date;
6294
        }
6295
6296
        if (!empty($duration)) {
6297
            $data['duration'] = $duration;
6298
        }
6299
6300
        if (!empty($ip)) {
6301
            $data['ip'] = $ip;
6302
        }
6303
6304
        if (api_get_configuration_value('save_titles_as_html')) {
6305
            $data['title'] = $this->get_formated_title().get_lang('Result');
6306
        } else {
6307
            $data['title'] = PHP_EOL.$this->exercise.' : '.get_lang('Result');
6308
        }
6309
6310
        $questionsCount = count(explode(',', $trackExerciseInfo['data_tracking']));
6311
        $savedAnswersCount = $this->countUserAnswersSavedInExercise($trackExerciseInfo['exe_id']);
6312
6313
        $data['number_of_answers'] = $questionsCount;
6314
        $data['number_of_answers_saved'] = $savedAnswersCount;
6315
        $exeId = $trackExerciseInfo['exe_id'];
6316
6317
        if (false !== api_get_configuration_value('quiz_confirm_saved_answers')) {
6318
            $em = Database::getManager();
6319
6320
            if ($saveUserResult) {
6321
                $trackConfirmation = new TrackEExerciseConfirmation();
6322
                $trackConfirmation
6323
                    ->setUserId($trackExerciseInfo['exe_user_id'])
6324
                    ->setQuizId($trackExerciseInfo['exe_exo_id'])
6325
                    ->setAttemptId($trackExerciseInfo['exe_id'])
6326
                    ->setQuestionsCount($questionsCount)
6327
                    ->setSavedAnswersCount($savedAnswersCount)
6328
                    ->setCourseId($trackExerciseInfo['c_id'])
6329
                    ->setSessionId($trackExerciseInfo['session_id'])
6330
                    ->setCreatedAt(api_get_utc_datetime(null, false, true));
6331
6332
                $em->persist($trackConfirmation);
6333
                $em->flush();
6334
            } else {
6335
                $trackConfirmation = $em
6336
                    ->getRepository('ChamiloCoreBundle:TrackEExerciseConfirmation')
6337
                    ->findOneBy(
6338
                        [
6339
                            'attemptId' => $trackExerciseInfo['exe_id'],
6340
                            'quizId' => $trackExerciseInfo['exe_exo_id'],
6341
                            'courseId' => $trackExerciseInfo['c_id'],
6342
                            'sessionId' => $trackExerciseInfo['session_id'],
6343
                        ]
6344
                    );
6345
            }
6346
6347
            $data['track_confirmation'] = $trackConfirmation;
6348
        }
6349
6350
        $signature = '';
6351
        /*if (api_get_configuration_value('')) {
6352
            $extraFieldValue = new ExtraFieldValue('exercise');
6353
            $result = $extraFieldValue->get_values_by_handler_and_field_variable($objExercise->iId, 'signature_activated');
6354
            if ($result && isset($result['value']) && 1 === (int) $result['value']) {
6355
                $allowSignature = true;
6356
            }
6357
            $signature
6358
        }*/
6359
6360
        $tpl = new Template(null, false, false, false, false, false, false);
6361
        $tpl->assign('data', $data);
6362
        $tpl->assign('allow_signature', $allowSignature);
6363
        //$tpl->assign('signature', $signature);
6364
        $tpl->assign('allow_export_pdf', $allowExportPdf);
6365
        $tpl->assign('export_url', api_get_path(WEB_CODE_PATH).'exercise/result.php?action=export&id='.$exeId.'&'.api_get_cidreq());
6366
6367
        $layoutTemplate = $tpl->get_template('exercise/partials/result_exercise.tpl');
6368
6369
        return $tpl->fetch($layoutTemplate);
6370
    }
6371
6372
    /**
6373
     * Returns the exercise result.
6374
     *
6375
     * @param 	int		attempt id
6376
     *
6377
     * @return array
6378
     */
6379
    public function get_exercise_result($exe_id)
6380
    {
6381
        $result = [];
6382
        $track_exercise_info = ExerciseLib::get_exercise_track_exercise_info($exe_id);
6383
6384
        if (!empty($track_exercise_info)) {
6385
            $totalScore = 0;
6386
            $objExercise = new Exercise();
6387
            $objExercise->read($track_exercise_info['exe_exo_id']);
6388
            if (!empty($track_exercise_info['data_tracking'])) {
6389
                $question_list = explode(',', $track_exercise_info['data_tracking']);
6390
            }
6391
            foreach ($question_list as $questionId) {
6392
                $question_result = $objExercise->manage_answer(
6393
                    $exe_id,
6394
                    $questionId,
6395
                    '',
6396
                    'exercise_show',
6397
                    [],
6398
                    false,
6399
                    true,
6400
                    false,
6401
                    $objExercise->selectPropagateNeg()
6402
                );
6403
                $totalScore += $question_result['score'];
6404
            }
6405
6406
            if ($objExercise->selectPropagateNeg() == 0 && $totalScore < 0) {
6407
                $totalScore = 0;
6408
            }
6409
            $result = [
6410
                'score' => $totalScore,
6411
                'weight' => $track_exercise_info['exe_weighting'],
6412
            ];
6413
        }
6414
6415
        return $result;
6416
    }
6417
6418
    /**
6419
     * Checks if the exercise is visible due a lot of conditions
6420
     * visibility, time limits, student attempts
6421
     * Return associative array
6422
     * value : true if exercise visible
6423
     * message : HTML formatted message
6424
     * rawMessage : text message.
6425
     *
6426
     * @param int  $lpId
6427
     * @param int  $lpItemId
6428
     * @param int  $lpItemViewId
6429
     * @param bool $filterByAdmin
6430
     *
6431
     * @return array
6432
     */
6433
    public function is_visible(
6434
        $lpId = 0,
6435
        $lpItemId = 0,
6436
        $lpItemViewId = 0,
6437
        $filterByAdmin = true
6438
    ) {
6439
        // 1. By default the exercise is visible
6440
        $isVisible = true;
6441
        $message = null;
6442
6443
        // 1.1 Admins, teachers and tutors can access to the exercise
6444
        if ($filterByAdmin) {
6445
            if (api_is_platform_admin() || api_is_course_admin() || api_is_course_tutor()) {
6446
                return ['value' => true, 'message' => ''];
6447
            }
6448
        }
6449
6450
        // Deleted exercise.
6451
        if ($this->active == -1) {
6452
            return [
6453
                'value' => false,
6454
                'message' => Display::return_message(
6455
                    get_lang('ExerciseNotFound'),
6456
                    'warning',
6457
                    false
6458
                ),
6459
                'rawMessage' => get_lang('ExerciseNotFound'),
6460
            ];
6461
        }
6462
6463
        // Checking visibility in the item_property table.
6464
        $visibility = api_get_item_visibility(
6465
            api_get_course_info(),
6466
            TOOL_QUIZ,
6467
            $this->id,
6468
            api_get_session_id()
6469
        );
6470
6471
        if ($visibility == 0 || $visibility == 2) {
6472
            $this->active = 0;
6473
        }
6474
6475
        // 2. If the exercise is not active.
6476
        if (empty($lpId)) {
6477
            // 2.1 LP is OFF
6478
            if ($this->active == 0) {
6479
                return [
6480
                    'value' => false,
6481
                    'message' => Display::return_message(
6482
                        get_lang('ExerciseNotFound'),
6483
                        'warning',
6484
                        false
6485
                    ),
6486
                    'rawMessage' => get_lang('ExerciseNotFound'),
6487
                ];
6488
            }
6489
        } else {
6490
            // 2.1 LP is loaded
6491
            if ($this->active == 0 &&
6492
                !learnpath::is_lp_visible_for_student($lpId, api_get_user_id())
6493
            ) {
6494
                return [
6495
                    'value' => false,
6496
                    'message' => Display::return_message(
6497
                        get_lang('ExerciseNotFound'),
6498
                        'warning',
6499
                        false
6500
                    ),
6501
                    'rawMessage' => get_lang('ExerciseNotFound'),
6502
                ];
6503
            }
6504
        }
6505
6506
        // 3. We check if the time limits are on
6507
        $limitTimeExists = false;
6508
        if (!empty($this->start_time) || !empty($this->end_time)) {
6509
            $limitTimeExists = true;
6510
        }
6511
6512
        if ($limitTimeExists) {
6513
            $timeNow = time();
6514
            $existsStartDate = false;
6515
            $nowIsAfterStartDate = true;
6516
            $existsEndDate = false;
6517
            $nowIsBeforeEndDate = true;
6518
6519
            if (!empty($this->start_time)) {
6520
                $existsStartDate = true;
6521
            }
6522
6523
            if (!empty($this->end_time)) {
6524
                $existsEndDate = true;
6525
            }
6526
6527
            // check if we are before-or-after end-or-start date
6528
            if ($existsStartDate && $timeNow < api_strtotime($this->start_time, 'UTC')) {
6529
                $nowIsAfterStartDate = false;
6530
            }
6531
6532
            if ($existsEndDate & $timeNow >= api_strtotime($this->end_time, 'UTC')) {
6533
                $nowIsBeforeEndDate = false;
6534
            }
6535
6536
            // lets check all cases
6537
            if ($existsStartDate && !$existsEndDate) {
6538
                // exists start date and dont exists end date
6539
                if ($nowIsAfterStartDate) {
6540
                    // after start date, no end date
6541
                    $isVisible = true;
6542
                    $message = sprintf(
6543
                        get_lang('ExerciseAvailableSinceX'),
6544
                        api_convert_and_format_date($this->start_time)
6545
                    );
6546
                } else {
6547
                    // before start date, no end date
6548
                    $isVisible = false;
6549
                    $message = sprintf(
6550
                        get_lang('ExerciseAvailableFromX'),
6551
                        api_convert_and_format_date($this->start_time)
6552
                    );
6553
                }
6554
            } elseif (!$existsStartDate && $existsEndDate) {
6555
                // doesnt exist start date, exists end date
6556
                if ($nowIsBeforeEndDate) {
6557
                    // before end date, no start date
6558
                    $isVisible = true;
6559
                    $message = sprintf(
6560
                        get_lang('ExerciseAvailableUntilX'),
6561
                        api_convert_and_format_date($this->end_time)
6562
                    );
6563
                } else {
6564
                    // after end date, no start date
6565
                    $isVisible = false;
6566
                    $message = sprintf(
6567
                        get_lang('ExerciseAvailableUntilX'),
6568
                        api_convert_and_format_date($this->end_time)
6569
                    );
6570
                }
6571
            } elseif ($existsStartDate && $existsEndDate) {
6572
                // exists start date and end date
6573
                if ($nowIsAfterStartDate) {
6574
                    if ($nowIsBeforeEndDate) {
6575
                        // after start date and before end date
6576
                        $isVisible = true;
6577
                        $message = sprintf(
6578
                            get_lang('ExerciseIsActivatedFromXToY'),
6579
                            api_convert_and_format_date($this->start_time),
6580
                            api_convert_and_format_date($this->end_time)
6581
                        );
6582
                    } else {
6583
                        // after start date and after end date
6584
                        $isVisible = false;
6585
                        $message = sprintf(
6586
                            get_lang('ExerciseWasActivatedFromXToY'),
6587
                            api_convert_and_format_date($this->start_time),
6588
                            api_convert_and_format_date($this->end_time)
6589
                        );
6590
                    }
6591
                } else {
6592
                    if ($nowIsBeforeEndDate) {
6593
                        // before start date and before end date
6594
                        $isVisible = false;
6595
                        $message = sprintf(
6596
                            get_lang('ExerciseWillBeActivatedFromXToY'),
6597
                            api_convert_and_format_date($this->start_time),
6598
                            api_convert_and_format_date($this->end_time)
6599
                        );
6600
                    }
6601
                    // case before start date and after end date is impossible
6602
                }
6603
            } elseif (!$existsStartDate && !$existsEndDate) {
6604
                // doesnt exist start date nor end date
6605
                $isVisible = true;
6606
                $message = '';
6607
            }
6608
        }
6609
6610
        // 4. We check if the student have attempts
6611
        if ($isVisible) {
6612
            $exerciseAttempts = $this->selectAttempts();
6613
6614
            if ($exerciseAttempts > 0) {
6615
                $attemptCount = Event::get_attempt_count_not_finished(
6616
                    api_get_user_id(),
6617
                    $this->id,
6618
                    $lpId,
6619
                    $lpItemId,
6620
                    $lpItemViewId
6621
                );
6622
6623
                if ($attemptCount >= $exerciseAttempts) {
6624
                    $message = sprintf(
6625
                        get_lang('ReachedMaxAttempts'),
6626
                        $this->name,
6627
                        $exerciseAttempts
6628
                    );
6629
                    $isVisible = false;
6630
                }
6631
            }
6632
        }
6633
6634
        $rawMessage = '';
6635
        if (!empty($message)) {
6636
            $rawMessage = $message;
6637
            $message = Display::return_message($message, 'warning', false);
6638
        }
6639
6640
        return [
6641
            'value' => $isVisible,
6642
            'message' => $message,
6643
            'rawMessage' => $rawMessage,
6644
        ];
6645
    }
6646
6647
    /**
6648
     * @return bool
6649
     */
6650
    public function added_in_lp()
6651
    {
6652
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
6653
        $sql = "SELECT max_score FROM $TBL_LP_ITEM
6654
                WHERE
6655
                    c_id = {$this->course_id} AND
6656
                    item_type = '".TOOL_QUIZ."' AND
6657
                    path = '{$this->id}'";
6658
        $result = Database::query($sql);
6659
        if (Database::num_rows($result) > 0) {
6660
            return true;
6661
        }
6662
6663
        return false;
6664
    }
6665
6666
    /**
6667
     * Returns an array with this form.
6668
     *
6669
     * @example
6670
     * <code>
6671
     * array (size=3)
6672
     * 999 =>
6673
     * array (size=3)
6674
     * 0 => int 3422
6675
     * 1 => int 3423
6676
     * 2 => int 3424
6677
     * 100 =>
6678
     * array (size=2)
6679
     * 0 => int 3469
6680
     * 1 => int 3470
6681
     * 101 =>
6682
     * array (size=1)
6683
     * 0 => int 3482
6684
     * </code>
6685
     * The array inside the key 999 means the question list that belongs to the media id = 999,
6686
     * this case is special because 999 means "no media".
6687
     *
6688
     * @return array
6689
     */
6690
    public function getMediaList()
6691
    {
6692
        return $this->mediaList;
6693
    }
6694
6695
    /**
6696
     * Is media question activated?
6697
     *
6698
     * @return bool
6699
     */
6700
    public function mediaIsActivated()
6701
    {
6702
        $mediaQuestions = $this->getMediaList();
6703
        $active = false;
6704
        if (isset($mediaQuestions) && !empty($mediaQuestions)) {
6705
            $media_count = count($mediaQuestions);
6706
            if ($media_count > 1) {
6707
                return true;
6708
            } elseif ($media_count == 1) {
6709
                if (isset($mediaQuestions[999])) {
6710
                    return false;
6711
                } else {
6712
                    return true;
6713
                }
6714
            }
6715
        }
6716
6717
        return $active;
6718
    }
6719
6720
    /**
6721
     * Gets question list from the exercise.
6722
     *
6723
     * @return array
6724
     */
6725
    public function getQuestionList()
6726
    {
6727
        return $this->questionList;
6728
    }
6729
6730
    /**
6731
     * Question list with medias compressed like this.
6732
     *
6733
     * @example
6734
     * <code>
6735
     * array(
6736
     *      question_id_1,
6737
     *      question_id_2,
6738
     *      media_id, <- this media id contains question ids
6739
     *      question_id_3,
6740
     * )
6741
     * </code>
6742
     *
6743
     * @return array
6744
     */
6745
    public function getQuestionListWithMediasCompressed()
6746
    {
6747
        return $this->questionList;
6748
    }
6749
6750
    /**
6751
     * Question list with medias uncompressed like this.
6752
     *
6753
     * @example
6754
     * <code>
6755
     * array(
6756
     *      question_id,
6757
     *      question_id,
6758
     *      question_id, <- belongs to a media id
6759
     *      question_id, <- belongs to a media id
6760
     *      question_id,
6761
     * )
6762
     * </code>
6763
     *
6764
     * @return array
6765
     */
6766
    public function getQuestionListWithMediasUncompressed()
6767
    {
6768
        return $this->questionListUncompressed;
6769
    }
6770
6771
    /**
6772
     * Sets the question list when the exercise->read() is executed.
6773
     *
6774
     * @param bool $adminView Whether to view the set the list of *all* questions or just the normal student view
6775
     */
6776
    public function setQuestionList($adminView = false)
6777
    {
6778
        // Getting question list.
6779
        $questionList = $this->selectQuestionList(true, $adminView);
6780
        $this->setMediaList($questionList);
6781
        $this->questionList = $this->transformQuestionListWithMedias($questionList, false);
6782
        $this->questionListUncompressed = $this->transformQuestionListWithMedias(
6783
            $questionList,
6784
            true
6785
        );
6786
    }
6787
6788
    /**
6789
     * @params array question list
6790
     * @params bool expand or not question list (true show all questions,
6791
     * false show media question id instead of the question ids)
6792
     */
6793
    public function transformQuestionListWithMedias(
6794
        $question_list,
6795
        $expand_media_questions = false
6796
    ) {
6797
        $new_question_list = [];
6798
        if (!empty($question_list)) {
6799
            $media_questions = $this->getMediaList();
6800
            $media_active = $this->mediaIsActivated($media_questions);
6801
6802
            if ($media_active) {
6803
                $counter = 1;
6804
                foreach ($question_list as $question_id) {
6805
                    $add_question = true;
6806
                    foreach ($media_questions as $media_id => $question_list_in_media) {
6807
                        if ($media_id != 999 && in_array($question_id, $question_list_in_media)) {
6808
                            $add_question = false;
6809
                            if (!in_array($media_id, $new_question_list)) {
6810
                                $new_question_list[$counter] = $media_id;
6811
                                $counter++;
6812
                            }
6813
                            break;
6814
                        }
6815
                    }
6816
                    if ($add_question) {
6817
                        $new_question_list[$counter] = $question_id;
6818
                        $counter++;
6819
                    }
6820
                }
6821
                if ($expand_media_questions) {
6822
                    $media_key_list = array_keys($media_questions);
6823
                    foreach ($new_question_list as &$question_id) {
6824
                        if (in_array($question_id, $media_key_list)) {
6825
                            $question_id = $media_questions[$question_id];
6826
                        }
6827
                    }
6828
                    $new_question_list = array_flatten($new_question_list);
6829
                }
6830
            } else {
6831
                $new_question_list = $question_list;
6832
            }
6833
        }
6834
6835
        return $new_question_list;
6836
    }
6837
6838
    /**
6839
     * Get question list depend on the random settings.
6840
     *
6841
     * @return array
6842
     */
6843
    public function get_validated_question_list()
6844
    {
6845
        $isRandomByCategory = $this->isRandomByCat();
6846
        if ($isRandomByCategory == 0) {
6847
            if ($this->isRandom()) {
6848
                return $this->getRandomList();
6849
            }
6850
6851
            return $this->selectQuestionList();
6852
        }
6853
6854
        if ($this->isRandom()) {
6855
            // USE question categories
6856
            // get questions by category for this exercise
6857
            // we have to choice $objExercise->random question in each array values of $categoryQuestions
6858
            // key of $categoryQuestions are the categopy id (0 for not in a category)
6859
            // value is the array of question id of this category
6860
            $questionList = [];
6861
            $categoryQuestions = TestCategory::getQuestionsByCat($this->id);
6862
            $isRandomByCategory = $this->getRandomByCategory();
6863
            // We sort categories based on the term between [] in the head
6864
            // of the category's description
6865
            /* examples of categories :
6866
             * [biologie] Maitriser les mecanismes de base de la genetique
6867
             * [biologie] Relier les moyens de depenses et les agents infectieux
6868
             * [biologie] Savoir ou est produite l'enrgie dans les cellules et sous quelle forme
6869
             * [chimie] Classer les molles suivant leur pouvoir oxydant ou reacteur
6870
             * [chimie] Connaître la denition de la theoie acide/base selon Brönsted
6871
             * [chimie] Connaître les charges des particules
6872
             * We want that in the order of the groups defined by the term
6873
             * between brackets at the beginning of the category title
6874
            */
6875
            // If test option is Grouped By Categories
6876
            if ($isRandomByCategory == 2) {
6877
                $categoryQuestions = TestCategory::sortTabByBracketLabel($categoryQuestions);
6878
            }
6879
            foreach ($categoryQuestions as $question) {
6880
                $number_of_random_question = $this->random;
6881
                if ($this->random == -1) {
6882
                    $number_of_random_question = count($this->questionList);
6883
                }
6884
                $questionList = array_merge(
6885
                    $questionList,
6886
                    TestCategory::getNElementsFromArray(
6887
                        $question,
6888
                        $number_of_random_question
6889
                    )
6890
                );
6891
            }
6892
            // shuffle the question list if test is not grouped by categories
6893
            if ($isRandomByCategory == 1) {
6894
                shuffle($questionList); // or not
6895
            }
6896
6897
            return $questionList;
6898
        }
6899
6900
        // Problem, random by category has been selected and
6901
        // we have no $this->isRandom number of question selected
6902
        // Should not happened
6903
6904
        return [];
6905
    }
6906
6907
    public function get_question_list($expand_media_questions = false)
6908
    {
6909
        $question_list = $this->get_validated_question_list();
6910
        $question_list = $this->transform_question_list_with_medias($question_list, $expand_media_questions);
6911
6912
        return $question_list;
6913
    }
6914
6915
    public function transform_question_list_with_medias($question_list, $expand_media_questions = false)
6916
    {
6917
        $new_question_list = [];
6918
        if (!empty($question_list)) {
6919
            $media_questions = $this->getMediaList();
6920
            $media_active = $this->mediaIsActivated($media_questions);
6921
6922
            if ($media_active) {
6923
                $counter = 1;
6924
                foreach ($question_list as $question_id) {
6925
                    $add_question = true;
6926
                    foreach ($media_questions as $media_id => $question_list_in_media) {
6927
                        if ($media_id != 999 && in_array($question_id, $question_list_in_media)) {
6928
                            $add_question = false;
6929
                            if (!in_array($media_id, $new_question_list)) {
6930
                                $new_question_list[$counter] = $media_id;
6931
                                $counter++;
6932
                            }
6933
                            break;
6934
                        }
6935
                    }
6936
                    if ($add_question) {
6937
                        $new_question_list[$counter] = $question_id;
6938
                        $counter++;
6939
                    }
6940
                }
6941
                if ($expand_media_questions) {
6942
                    $media_key_list = array_keys($media_questions);
6943
                    foreach ($new_question_list as &$question_id) {
6944
                        if (in_array($question_id, $media_key_list)) {
6945
                            $question_id = $media_questions[$question_id];
6946
                        }
6947
                    }
6948
                    $new_question_list = array_flatten($new_question_list);
6949
                }
6950
            } else {
6951
                $new_question_list = $question_list;
6952
            }
6953
        }
6954
6955
        return $new_question_list;
6956
    }
6957
6958
    /**
6959
     * @param int $exe_id
6960
     *
6961
     * @return array
6962
     */
6963
    public function get_stat_track_exercise_info_by_exe_id($exe_id)
6964
    {
6965
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6966
        $exe_id = (int) $exe_id;
6967
        $sql_track = "SELECT * FROM $table WHERE exe_id = $exe_id ";
6968
        $result = Database::query($sql_track);
6969
        $new_array = [];
6970
        if (Database::num_rows($result) > 0) {
6971
            $new_array = Database::fetch_array($result, 'ASSOC');
6972
            $start_date = api_get_utc_datetime($new_array['start_date'], true);
6973
            $end_date = api_get_utc_datetime($new_array['exe_date'], true);
6974
            $new_array['duration_formatted'] = '';
6975
            if (!empty($new_array['exe_duration']) && !empty($start_date) && !empty($end_date)) {
6976
                $time = api_format_time($new_array['exe_duration'], 'js');
6977
                $new_array['duration_formatted'] = $time;
6978
            }
6979
        }
6980
6981
        return $new_array;
6982
    }
6983
6984
    /**
6985
     * @param int $exeId
6986
     *
6987
     * @return bool
6988
     */
6989
    public function removeAllQuestionToRemind($exeId)
6990
    {
6991
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6992
        $exeId = (int) $exeId;
6993
        if (empty($exeId)) {
6994
            return false;
6995
        }
6996
        $sql = "UPDATE $table
6997
                SET questions_to_check = ''
6998
                WHERE exe_id = $exeId ";
6999
        Database::query($sql);
7000
7001
        return true;
7002
    }
7003
7004
    /**
7005
     * @param int   $exeId
7006
     * @param array $questionList
7007
     *
7008
     * @return bool
7009
     */
7010
    public function addAllQuestionToRemind($exeId, $questionList = [])
7011
    {
7012
        $exeId = (int) $exeId;
7013
        if (empty($questionList)) {
7014
            return false;
7015
        }
7016
7017
        $questionListToString = implode(',', $questionList);
7018
        $questionListToString = Database::escape_string($questionListToString);
7019
7020
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7021
        $sql = "UPDATE $table
7022
                SET questions_to_check = '$questionListToString'
7023
                WHERE exe_id = $exeId";
7024
        Database::query($sql);
7025
7026
        return true;
7027
    }
7028
7029
    /**
7030
     * @param int    $exe_id
7031
     * @param int    $question_id
7032
     * @param string $action
7033
     */
7034
    public function editQuestionToRemind($exe_id, $question_id, $action = 'add')
7035
    {
7036
        $exercise_info = self::get_stat_track_exercise_info_by_exe_id($exe_id);
7037
        $question_id = (int) $question_id;
7038
        $exe_id = (int) $exe_id;
7039
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7040
        if ($exercise_info) {
7041
            if (empty($exercise_info['questions_to_check'])) {
7042
                if ($action == 'add') {
7043
                    $sql = "UPDATE $track_exercises
7044
                            SET questions_to_check = '$question_id'
7045
                            WHERE exe_id = $exe_id ";
7046
                    Database::query($sql);
7047
                }
7048
            } else {
7049
                $remind_list = explode(',', $exercise_info['questions_to_check']);
7050
                $remind_list_string = '';
7051
                if ($action == 'add') {
7052
                    if (!in_array($question_id, $remind_list)) {
7053
                        $newRemindList = [];
7054
                        $remind_list[] = $question_id;
7055
                        $questionListInSession = Session::read('questionList');
7056
                        if (!empty($questionListInSession)) {
7057
                            foreach ($questionListInSession as $originalQuestionId) {
7058
                                if (in_array($originalQuestionId, $remind_list)) {
7059
                                    $newRemindList[] = $originalQuestionId;
7060
                                }
7061
                            }
7062
                        }
7063
                        $remind_list_string = implode(',', $newRemindList);
7064
                    }
7065
                } elseif ($action == 'delete') {
7066
                    if (!empty($remind_list)) {
7067
                        if (in_array($question_id, $remind_list)) {
7068
                            $remind_list = array_flip($remind_list);
7069
                            unset($remind_list[$question_id]);
7070
                            $remind_list = array_flip($remind_list);
7071
7072
                            if (!empty($remind_list)) {
7073
                                sort($remind_list);
7074
                                array_filter($remind_list);
7075
                                $remind_list_string = implode(',', $remind_list);
7076
                            }
7077
                        }
7078
                    }
7079
                }
7080
                $value = Database::escape_string($remind_list_string);
7081
                $sql = "UPDATE $track_exercises
7082
                        SET questions_to_check = '$value'
7083
                        WHERE exe_id = $exe_id ";
7084
                Database::query($sql);
7085
            }
7086
        }
7087
    }
7088
7089
    /**
7090
     * @param string $answer
7091
     *
7092
     * @return mixed
7093
     */
7094
    public function fill_in_blank_answer_to_array($answer)
7095
    {
7096
        api_preg_match_all('/\[[^]]+\]/', $answer, $teacher_answer_list);
7097
        $teacher_answer_list = $teacher_answer_list[0];
7098
7099
        return $teacher_answer_list;
7100
    }
7101
7102
    /**
7103
     * @param string $answer
7104
     *
7105
     * @return string
7106
     */
7107
    public function fill_in_blank_answer_to_string($answer)
7108
    {
7109
        $teacher_answer_list = $this->fill_in_blank_answer_to_array($answer);
7110
        $result = '';
7111
        if (!empty($teacher_answer_list)) {
7112
            foreach ($teacher_answer_list as $teacher_item) {
7113
                // Cleaning student answer list
7114
                $value = strip_tags($teacher_item);
7115
                $value = api_substr($value, 1, api_strlen($value) - 2);
7116
                $value = explode('/', $value);
7117
                if (!empty($value[0])) {
7118
                    $value = trim($value[0]);
7119
                    $value = str_replace('&nbsp;', '', $value);
7120
                    $result .= $value;
7121
                }
7122
            }
7123
        }
7124
7125
        return $result;
7126
    }
7127
7128
    /**
7129
     * @return string
7130
     */
7131
    public function returnTimeLeftDiv()
7132
    {
7133
        $html = '<div id="clock_warning" style="display:none">';
7134
        $html .= Display::return_message(
7135
            get_lang('ReachedTimeLimit'),
7136
            'warning'
7137
        );
7138
        $html .= ' ';
7139
        $html .= sprintf(
7140
            get_lang('YouWillBeRedirectedInXSeconds'),
7141
            '<span id="counter_to_redirect" class="red_alert"></span>'
7142
        );
7143
        $html .= '</div>';
7144
        $html .= '<div id="exercise_clock_warning" class="count_down"></div>';
7145
7146
        return $html;
7147
    }
7148
7149
    /**
7150
     * Get categories added in the exercise--category matrix.
7151
     *
7152
     * @return array
7153
     */
7154
    public function getCategoriesInExercise()
7155
    {
7156
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7157
        if (!empty($this->id)) {
7158
            $sql = "SELECT * FROM $table
7159
                    WHERE exercise_id = {$this->id} AND c_id = {$this->course_id} ";
7160
            $result = Database::query($sql);
7161
            $list = [];
7162
            if (Database::num_rows($result)) {
7163
                while ($row = Database::fetch_array($result, 'ASSOC')) {
7164
                    $list[$row['category_id']] = $row;
7165
                }
7166
7167
                return $list;
7168
            }
7169
        }
7170
7171
        return [];
7172
    }
7173
7174
    /**
7175
     * Get total number of question that will be parsed when using the category/exercise.
7176
     *
7177
     * @return int
7178
     */
7179
    public function getNumberQuestionExerciseCategory()
7180
    {
7181
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7182
        if (!empty($this->id)) {
7183
            $sql = "SELECT SUM(count_questions) count_questions
7184
                    FROM $table
7185
                    WHERE exercise_id = {$this->id} AND c_id = {$this->course_id}";
7186
            $result = Database::query($sql);
7187
            if (Database::num_rows($result)) {
7188
                $row = Database::fetch_array($result);
7189
7190
                return (int) $row['count_questions'];
7191
            }
7192
        }
7193
7194
        return 0;
7195
    }
7196
7197
    /**
7198
     * Save categories in the TABLE_QUIZ_REL_CATEGORY table.
7199
     *
7200
     * @param array $categories
7201
     */
7202
    public function save_categories_in_exercise($categories)
7203
    {
7204
        if (!empty($categories) && !empty($this->id)) {
7205
            $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7206
            $sql = "DELETE FROM $table
7207
                    WHERE exercise_id = {$this->id} AND c_id = {$this->course_id}";
7208
            Database::query($sql);
7209
            if (!empty($categories)) {
7210
                foreach ($categories as $categoryId => $countQuestions) {
7211
                    $params = [
7212
                        'c_id' => $this->course_id,
7213
                        'exercise_id' => $this->id,
7214
                        'category_id' => $categoryId,
7215
                        'count_questions' => $countQuestions,
7216
                    ];
7217
                    Database::insert($table, $params);
7218
                }
7219
            }
7220
        }
7221
    }
7222
7223
    /**
7224
     * @param array  $questionList
7225
     * @param int    $currentQuestion
7226
     * @param array  $conditions
7227
     * @param string $link
7228
     *
7229
     * @return string
7230
     */
7231
    public function progressExercisePaginationBar(
7232
        $questionList,
7233
        $currentQuestion,
7234
        $conditions,
7235
        $link
7236
    ) {
7237
        $mediaQuestions = $this->getMediaList();
7238
7239
        $html = '<div class="exercise_pagination pagination pagination-mini"><ul>';
7240
        $counter = 0;
7241
        $nextValue = 0;
7242
        $wasMedia = false;
7243
        $before = 0;
7244
        $counterNoMedias = 0;
7245
        foreach ($questionList as $questionId) {
7246
            $isCurrent = $currentQuestion == ($counterNoMedias + 1) ? true : false;
7247
7248
            if (!empty($nextValue)) {
7249
                if ($wasMedia) {
7250
                    $nextValue = $nextValue - $before + 1;
7251
                }
7252
            }
7253
7254
            if (isset($mediaQuestions) && isset($mediaQuestions[$questionId])) {
7255
                $fixedValue = $counterNoMedias;
7256
7257
                $html .= Display::progressPaginationBar(
7258
                    $nextValue,
7259
                    $mediaQuestions[$questionId],
7260
                    $currentQuestion,
7261
                    $fixedValue,
7262
                    $conditions,
7263
                    $link,
7264
                    true,
7265
                    true
7266
                );
7267
7268
                $counter += count($mediaQuestions[$questionId]) - 1;
7269
                $before = count($questionList);
7270
                $wasMedia = true;
7271
                $nextValue += count($questionList);
7272
            } else {
7273
                $html .= Display::parsePaginationItem(
7274
                    $questionId,
7275
                    $isCurrent,
7276
                    $conditions,
7277
                    $link,
7278
                    $counter
7279
                );
7280
                $counter++;
7281
                $nextValue++;
7282
                $wasMedia = false;
7283
            }
7284
            $counterNoMedias++;
7285
        }
7286
        $html .= '</ul></div>';
7287
7288
        return $html;
7289
    }
7290
7291
    /**
7292
     *  Shows a list of numbers that represents the question to answer in a exercise.
7293
     *
7294
     * @param array  $categories
7295
     * @param int    $current
7296
     * @param array  $conditions
7297
     * @param string $link
7298
     *
7299
     * @return string
7300
     */
7301
    public function progressExercisePaginationBarWithCategories(
7302
        $categories,
7303
        $current,
7304
        $conditions = [],
7305
        $link = null
7306
    ) {
7307
        $html = null;
7308
        $counterNoMedias = 0;
7309
        $nextValue = 0;
7310
        $wasMedia = false;
7311
        $before = 0;
7312
7313
        if (!empty($categories)) {
7314
            $selectionType = $this->getQuestionSelectionType();
7315
            $useRootAsCategoryTitle = false;
7316
7317
            // Grouping questions per parent category see BT#6540
7318
            if (in_array(
7319
                $selectionType,
7320
                [
7321
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED,
7322
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM,
7323
                ]
7324
            )) {
7325
                $useRootAsCategoryTitle = true;
7326
            }
7327
7328
            // If the exercise is set to only show the titles of the categories
7329
            // at the root of the tree, then pre-order the categories tree by
7330
            // removing children and summing their questions into the parent
7331
            // categories
7332
            if ($useRootAsCategoryTitle) {
7333
                // The new categories list starts empty
7334
                $newCategoryList = [];
7335
                foreach ($categories as $category) {
7336
                    $rootElement = $category['root'];
7337
7338
                    if (isset($category['parent_info'])) {
7339
                        $rootElement = $category['parent_info']['id'];
7340
                    }
7341
7342
                    //$rootElement = $category['id'];
7343
                    // If the current category's ancestor was never seen
7344
                    // before, then declare it and assign the current
7345
                    // category to it.
7346
                    if (!isset($newCategoryList[$rootElement])) {
7347
                        $newCategoryList[$rootElement] = $category;
7348
                    } else {
7349
                        // If it was already seen, then merge the previous with
7350
                        // the current category
7351
                        $oldQuestionList = $newCategoryList[$rootElement]['question_list'];
7352
                        $category['question_list'] = array_merge($oldQuestionList, $category['question_list']);
7353
                        $newCategoryList[$rootElement] = $category;
7354
                    }
7355
                }
7356
                // Now use the newly built categories list, with only parents
7357
                $categories = $newCategoryList;
7358
            }
7359
7360
            foreach ($categories as $category) {
7361
                $questionList = $category['question_list'];
7362
                // Check if in this category there questions added in a media
7363
                $mediaQuestionId = $category['media_question'];
7364
                $isMedia = false;
7365
                $fixedValue = null;
7366
7367
                // Media exists!
7368
                if ($mediaQuestionId != 999) {
7369
                    $isMedia = true;
7370
                    $fixedValue = $counterNoMedias;
7371
                }
7372
7373
                //$categoryName = $category['path']; << show the path
7374
                $categoryName = $category['name'];
7375
7376
                if ($useRootAsCategoryTitle) {
7377
                    if (isset($category['parent_info'])) {
7378
                        $categoryName = $category['parent_info']['title'];
7379
                    }
7380
                }
7381
                $html .= '<div class="row">';
7382
                $html .= '<div class="span2">'.$categoryName.'</div>';
7383
                $html .= '<div class="span8">';
7384
7385
                if (!empty($nextValue)) {
7386
                    if ($wasMedia) {
7387
                        $nextValue = $nextValue - $before + 1;
7388
                    }
7389
                }
7390
                $html .= Display::progressPaginationBar(
7391
                    $nextValue,
7392
                    $questionList,
7393
                    $current,
7394
                    $fixedValue,
7395
                    $conditions,
7396
                    $link,
7397
                    $isMedia,
7398
                    true
7399
                );
7400
                $html .= '</div>';
7401
                $html .= '</div>';
7402
7403
                if ($mediaQuestionId == 999) {
7404
                    $counterNoMedias += count($questionList);
7405
                } else {
7406
                    $counterNoMedias++;
7407
                }
7408
7409
                $nextValue += count($questionList);
7410
                $before = count($questionList);
7411
7412
                if ($mediaQuestionId != 999) {
7413
                    $wasMedia = true;
7414
                } else {
7415
                    $wasMedia = false;
7416
                }
7417
            }
7418
        }
7419
7420
        return $html;
7421
    }
7422
7423
    /**
7424
     * Renders a question list.
7425
     *
7426
     * @param array $questionList    (with media questions compressed)
7427
     * @param int   $currentQuestion
7428
     * @param array $exerciseResult
7429
     * @param array $attemptList
7430
     * @param array $remindList
7431
     */
7432
    public function renderQuestionList(
7433
        $questionList,
7434
        $currentQuestion,
7435
        $exerciseResult,
7436
        $attemptList,
7437
        $remindList
7438
    ) {
7439
        $mediaQuestions = $this->getMediaList();
7440
        $i = 0;
7441
7442
        // Normal question list render (medias compressed)
7443
        foreach ($questionList as $questionId) {
7444
            $i++;
7445
            // For sequential exercises
7446
7447
            if ($this->type == ONE_PER_PAGE) {
7448
                // If it is not the right question, goes to the next loop iteration
7449
                if ($currentQuestion != $i) {
7450
                    continue;
7451
                } else {
7452
                    if (!in_array(
7453
                        $this->getFeedbackType(),
7454
                        [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
7455
                    )) {
7456
                        // if the user has already answered this question
7457
                        if (isset($exerciseResult[$questionId])) {
7458
                            echo Display::return_message(
7459
                                get_lang('AlreadyAnswered'),
7460
                                'normal'
7461
                            );
7462
                            break;
7463
                        }
7464
                    }
7465
                }
7466
            }
7467
7468
            // The $questionList contains the media id we check
7469
            // if this questionId is a media question type
7470
            if (isset($mediaQuestions[$questionId]) &&
7471
                $mediaQuestions[$questionId] != 999
7472
            ) {
7473
                // The question belongs to a media
7474
                $mediaQuestionList = $mediaQuestions[$questionId];
7475
                $objQuestionTmp = Question::read($questionId);
7476
7477
                $counter = 1;
7478
                if ($objQuestionTmp->type == MEDIA_QUESTION) {
7479
                    echo $objQuestionTmp->show_media_content();
7480
7481
                    $countQuestionsInsideMedia = count($mediaQuestionList);
7482
7483
                    // Show questions that belongs to a media
7484
                    if (!empty($mediaQuestionList)) {
7485
                        // In order to parse media questions we use letters a, b, c, etc.
7486
                        $letterCounter = 97;
7487
                        foreach ($mediaQuestionList as $questionIdInsideMedia) {
7488
                            $isLastQuestionInMedia = false;
7489
                            if ($counter == $countQuestionsInsideMedia) {
7490
                                $isLastQuestionInMedia = true;
7491
                            }
7492
                            $this->renderQuestion(
7493
                                $questionIdInsideMedia,
7494
                                $attemptList,
7495
                                $remindList,
7496
                                chr($letterCounter),
7497
                                $currentQuestion,
7498
                                $mediaQuestionList,
7499
                                $isLastQuestionInMedia,
7500
                                $questionList
7501
                            );
7502
                            $letterCounter++;
7503
                            $counter++;
7504
                        }
7505
                    }
7506
                } else {
7507
                    $this->renderQuestion(
7508
                        $questionId,
7509
                        $attemptList,
7510
                        $remindList,
7511
                        $i,
7512
                        $currentQuestion,
7513
                        null,
7514
                        null,
7515
                        $questionList
7516
                    );
7517
                    $i++;
7518
                }
7519
            } else {
7520
                // Normal question render.
7521
                $this->renderQuestion(
7522
                    $questionId,
7523
                    $attemptList,
7524
                    $remindList,
7525
                    $i,
7526
                    $currentQuestion,
7527
                    null,
7528
                    null,
7529
                    $questionList
7530
                );
7531
            }
7532
7533
            // For sequential exercises.
7534
            if ($this->type == ONE_PER_PAGE) {
7535
                // quits the loop
7536
                break;
7537
            }
7538
        }
7539
        // end foreach()
7540
7541
        if ($this->type == ALL_ON_ONE_PAGE) {
7542
            $exercise_actions = $this->show_button($questionId, $currentQuestion);
7543
            echo Display::div($exercise_actions, ['class' => 'exercise_actions']);
7544
        }
7545
    }
7546
7547
    /**
7548
     * @param int   $questionId
7549
     * @param array $attemptList
7550
     * @param array $remindList
7551
     * @param int   $i
7552
     * @param int   $current_question
7553
     * @param array $questions_in_media
7554
     * @param bool  $last_question_in_media
7555
     * @param array $realQuestionList
7556
     * @param bool  $generateJS
7557
     */
7558
    public function renderQuestion(
7559
        $questionId,
7560
        $attemptList,
7561
        $remindList,
7562
        $i,
7563
        $current_question,
7564
        $questions_in_media = [],
7565
        $last_question_in_media = false,
7566
        $realQuestionList,
7567
        $generateJS = true
7568
    ) {
7569
        // With this option on the question is loaded via AJAX
7570
        //$generateJS = true;
7571
        //$this->loadQuestionAJAX = true;
7572
7573
        if ($generateJS && $this->loadQuestionAJAX) {
7574
            $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?a=get_question&id='.$questionId.'&'.api_get_cidreq();
7575
            $params = [
7576
                'questionId' => $questionId,
7577
                'attemptList' => $attemptList,
7578
                'remindList' => $remindList,
7579
                'i' => $i,
7580
                'current_question' => $current_question,
7581
                'questions_in_media' => $questions_in_media,
7582
                'last_question_in_media' => $last_question_in_media,
7583
            ];
7584
            $params = json_encode($params);
7585
7586
            $script = '<script>
7587
            $(function(){
7588
                var params = '.$params.';
7589
                $.ajax({
7590
                    type: "GET",
7591
                    data: params,
7592
                    url: "'.$url.'",
7593
                    success: function(return_value) {
7594
                        $("#ajaxquestiondiv'.$questionId.'").html(return_value);
7595
                    }
7596
                });
7597
            });
7598
            </script>
7599
            <div id="ajaxquestiondiv'.$questionId.'"></div>';
7600
            echo $script;
7601
        } else {
7602
            global $origin;
7603
            $question_obj = Question::read($questionId);
7604
            $user_choice = isset($attemptList[$questionId]) ? $attemptList[$questionId] : null;
7605
            $remind_highlight = null;
7606
7607
            // Hides questions when reviewing a ALL_ON_ONE_PAGE exercise
7608
            // see #4542 no_remind_highlight class hide with jquery
7609
            if ($this->type == ALL_ON_ONE_PAGE && isset($_GET['reminder']) && $_GET['reminder'] == 2) {
7610
                $remind_highlight = 'no_remind_highlight';
7611
                if (in_array($question_obj->type, Question::question_type_no_review())) {
7612
                    return null;
7613
                }
7614
            }
7615
7616
            $attributes = ['id' => 'remind_list['.$questionId.']'];
7617
7618
            // Showing the question
7619
            $exercise_actions = null;
7620
            echo '<a id="questionanchor'.$questionId.'"></a><br />';
7621
            echo '<div id="question_div_'.$questionId.'" class="main_question '.$remind_highlight.'" >';
7622
7623
            // Shows the question + possible answers
7624
            $showTitle = $this->getHideQuestionTitle() == 1 ? false : true;
7625
            echo $this->showQuestion(
7626
                $question_obj,
7627
                false,
7628
                $origin,
7629
                $i,
7630
                $showTitle,
7631
                false,
7632
                $user_choice,
7633
                false,
7634
                null,
7635
                false,
7636
                $this->getModelType(),
7637
                $this->categoryMinusOne
7638
            );
7639
7640
            // Button save and continue
7641
            switch ($this->type) {
7642
                case ONE_PER_PAGE:
7643
                    $exercise_actions .= $this->show_button(
7644
                        $questionId,
7645
                        $current_question,
7646
                        null,
7647
                        $remindList
7648
                    );
7649
                    break;
7650
                case ALL_ON_ONE_PAGE:
7651
                    if (api_is_allowed_to_session_edit()) {
7652
                        $button = [
7653
                            Display::button(
7654
                                'save_now',
7655
                                get_lang('SaveForNow'),
7656
                                ['type' => 'button', 'class' => 'btn btn-primary', 'data-question' => $questionId]
7657
                            ),
7658
                            '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>',
7659
                        ];
7660
                        $exercise_actions .= Display::div(
7661
                            implode(PHP_EOL, $button),
7662
                            ['class' => 'exercise_save_now_button']
7663
                        );
7664
                    }
7665
                    break;
7666
            }
7667
7668
            if (!empty($questions_in_media)) {
7669
                $count_of_questions_inside_media = count($questions_in_media);
7670
                if ($count_of_questions_inside_media > 1 && api_is_allowed_to_session_edit()) {
7671
                    $button = [
7672
                        Display::button(
7673
                            'save_now',
7674
                            get_lang('SaveForNow'),
7675
                            ['type' => 'button', 'class' => 'btn btn-primary', 'data-question' => $questionId]
7676
                        ),
7677
                        '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>&nbsp;',
7678
                    ];
7679
                    $exercise_actions = Display::div(
7680
                        implode(PHP_EOL, $button),
7681
                        ['class' => 'exercise_save_now_button']
7682
                    );
7683
                }
7684
7685
                if ($last_question_in_media && $this->type == ONE_PER_PAGE) {
7686
                    $exercise_actions = $this->show_button($questionId, $current_question, $questions_in_media);
7687
                }
7688
            }
7689
7690
            // Checkbox review answers
7691
            if ($this->review_answers &&
7692
                !in_array($question_obj->type, Question::question_type_no_review())
7693
            ) {
7694
                $remind_question_div = Display::tag(
7695
                    'label',
7696
                    Display::input(
7697
                        'checkbox',
7698
                        'remind_list['.$questionId.']',
7699
                        '',
7700
                        $attributes
7701
                    ).get_lang('ReviewQuestionLater'),
7702
                    [
7703
                        'class' => 'checkbox',
7704
                        'for' => 'remind_list['.$questionId.']',
7705
                    ]
7706
                );
7707
                $exercise_actions .= Display::div(
7708
                    $remind_question_div,
7709
                    ['class' => 'exercise_save_now_button']
7710
                );
7711
            }
7712
7713
            echo Display::div(' ', ['class' => 'clear']);
7714
7715
            $paginationCounter = null;
7716
            if ($this->type == ONE_PER_PAGE) {
7717
                if (empty($questions_in_media)) {
7718
                    $paginationCounter = Display::paginationIndicator(
7719
                        $current_question,
7720
                        count($realQuestionList)
7721
                    );
7722
                } else {
7723
                    if ($last_question_in_media) {
7724
                        $paginationCounter = Display::paginationIndicator(
7725
                            $current_question,
7726
                            count($realQuestionList)
7727
                        );
7728
                    }
7729
                }
7730
            }
7731
7732
            echo '<div class="row"><div class="pull-right">'.$paginationCounter.'</div></div>';
7733
            echo Display::div($exercise_actions, ['class' => 'form-actions']);
7734
            echo '</div>';
7735
        }
7736
    }
7737
7738
    /**
7739
     * Returns an array of categories details for the questions of the current
7740
     * exercise.
7741
     *
7742
     * @return array
7743
     */
7744
    public function getQuestionWithCategories()
7745
    {
7746
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
7747
        $categoryRelTable = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
7748
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
7749
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
7750
        $sql = "SELECT DISTINCT cat.*
7751
                FROM $TBL_EXERCICE_QUESTION e
7752
                INNER JOIN $TBL_QUESTIONS q
7753
                ON (e.question_id = q.id AND e.c_id = q.c_id)
7754
                INNER JOIN $categoryRelTable catRel
7755
                ON (catRel.question_id = e.question_id AND catRel.c_id = e.c_id)
7756
                INNER JOIN $categoryTable cat
7757
                ON (cat.id = catRel.category_id AND cat.c_id = e.c_id)
7758
                WHERE
7759
                  e.c_id = {$this->course_id} AND
7760
                  e.exercice_id	= ".intval($this->id);
7761
7762
        $result = Database::query($sql);
7763
        $categoriesInExercise = [];
7764
        if (Database::num_rows($result)) {
7765
            $categoriesInExercise = Database::store_result($result, 'ASSOC');
7766
        }
7767
7768
        return $categoriesInExercise;
7769
    }
7770
7771
    /**
7772
     * Calculate the max_score of the quiz, depending of question inside, and quiz advanced option.
7773
     */
7774
    public function get_max_score()
7775
    {
7776
        $out_max_score = 0;
7777
        // list of question's id !!! the array key start at 1 !!!
7778
        $questionList = $this->selectQuestionList(true);
7779
7780
        // test is randomQuestions - see field random of test
7781
        if ($this->random > 0 && 0 == $this->randomByCat) {
7782
            $numberRandomQuestions = $this->random;
7783
            $questionScoreList = [];
7784
            foreach ($questionList as $questionId) {
7785
                $tmpobj_question = Question::read($questionId);
7786
                if (is_object($tmpobj_question)) {
7787
                    $questionScoreList[] = $tmpobj_question->weighting;
7788
                }
7789
            }
7790
7791
            rsort($questionScoreList);
7792
            // add the first $numberRandomQuestions value of score array to get max_score
7793
            for ($i = 0; $i < min($numberRandomQuestions, count($questionScoreList)); $i++) {
7794
                $out_max_score += $questionScoreList[$i];
7795
            }
7796
        } elseif ($this->random > 0 && $this->randomByCat > 0) {
7797
            // test is random by category
7798
            // get the $numberRandomQuestions best score question of each category
7799
            $numberRandomQuestions = $this->random;
7800
            $tab_categories_scores = [];
7801
            foreach ($questionList as $questionId) {
7802
                $question_category_id = TestCategory::getCategoryForQuestion($questionId);
7803
                if (!is_array($tab_categories_scores[$question_category_id])) {
7804
                    $tab_categories_scores[$question_category_id] = [];
7805
                }
7806
                $tmpobj_question = Question::read($questionId);
7807
                if (is_object($tmpobj_question)) {
7808
                    $tab_categories_scores[$question_category_id][] = $tmpobj_question->weighting;
7809
                }
7810
            }
7811
7812
            // here we've got an array with first key, the category_id, second key, score of question for this cat
7813
            foreach ($tab_categories_scores as $tab_scores) {
7814
                rsort($tab_scores);
7815
                for ($i = 0; $i < min($numberRandomQuestions, count($tab_scores)); $i++) {
7816
                    $out_max_score += $tab_scores[$i];
7817
                }
7818
            }
7819
        } else {
7820
            // standard test, just add each question score
7821
            foreach ($questionList as $questionId) {
7822
                $question = Question::read($questionId, $this->course);
7823
                $out_max_score += $question->weighting;
7824
            }
7825
        }
7826
7827
        return $out_max_score;
7828
    }
7829
7830
    /**
7831
     * @return string
7832
     */
7833
    public function get_formated_title()
7834
    {
7835
        if (api_get_configuration_value('save_titles_as_html')) {
7836
        }
7837
7838
        return api_html_entity_decode($this->selectTitle());
7839
    }
7840
7841
    /**
7842
     * @param string $title
7843
     *
7844
     * @return string
7845
     */
7846
    public static function get_formated_title_variable($title)
7847
    {
7848
        return api_html_entity_decode($title);
7849
    }
7850
7851
    /**
7852
     * @return string
7853
     */
7854
    public function format_title()
7855
    {
7856
        return api_htmlentities($this->title);
7857
    }
7858
7859
    /**
7860
     * @param string $title
7861
     *
7862
     * @return string
7863
     */
7864
    public static function format_title_variable($title)
7865
    {
7866
        return api_htmlentities($title);
7867
    }
7868
7869
    /**
7870
     * @param int $courseId
7871
     * @param int $sessionId
7872
     *
7873
     * @return array exercises
7874
     */
7875
    public function getExercisesByCourseSession($courseId, $sessionId)
7876
    {
7877
        $courseId = (int) $courseId;
7878
        $sessionId = (int) $sessionId;
7879
7880
        $tbl_quiz = Database::get_course_table(TABLE_QUIZ_TEST);
7881
        $sql = "SELECT * FROM $tbl_quiz cq
7882
                WHERE
7883
                    cq.c_id = %s AND
7884
                    (cq.session_id = %s OR cq.session_id = 0) AND
7885
                    cq.active = 0
7886
                ORDER BY cq.id";
7887
        $sql = sprintf($sql, $courseId, $sessionId);
7888
7889
        $result = Database::query($sql);
7890
7891
        $rows = [];
7892
        while ($row = Database::fetch_array($result, 'ASSOC')) {
7893
            $rows[] = $row;
7894
        }
7895
7896
        return $rows;
7897
    }
7898
7899
    /**
7900
     * @param int   $courseId
7901
     * @param int   $sessionId
7902
     * @param array $quizId
7903
     *
7904
     * @return array exercises
7905
     */
7906
    public function getExerciseAndResult($courseId, $sessionId, $quizId = [])
7907
    {
7908
        if (empty($quizId)) {
7909
            return [];
7910
        }
7911
7912
        $sessionId = (int) $sessionId;
7913
        $courseId = (int) $courseId;
7914
7915
        $ids = is_array($quizId) ? $quizId : [$quizId];
7916
        $ids = array_map('intval', $ids);
7917
        $ids = implode(',', $ids);
7918
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7919
        if (0 != $sessionId) {
7920
            $sql = "SELECT * FROM $track_exercises te
7921
              INNER JOIN c_quiz cq ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id
7922
              WHERE
7923
              te.id = %s AND
7924
              te.session_id = %s AND
7925
              cq.id IN (%s)
7926
              ORDER BY cq.id";
7927
7928
            $sql = sprintf($sql, $courseId, $sessionId, $ids);
7929
        } else {
7930
            $sql = "SELECT * FROM $track_exercises te
7931
              INNER JOIN c_quiz cq ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id
7932
              WHERE
7933
              te.id = %s AND
7934
              cq.id IN (%s)
7935
              ORDER BY cq.id";
7936
            $sql = sprintf($sql, $courseId, $ids);
7937
        }
7938
        $result = Database::query($sql);
7939
        $rows = [];
7940
        while ($row = Database::fetch_array($result, 'ASSOC')) {
7941
            $rows[] = $row;
7942
        }
7943
7944
        return $rows;
7945
    }
7946
7947
    /**
7948
     * @param $exeId
7949
     * @param $exercise_stat_info
7950
     * @param $remindList
7951
     * @param $currentQuestion
7952
     *
7953
     * @return int|null
7954
     */
7955
    public static function getNextQuestionId(
7956
        $exeId,
7957
        $exercise_stat_info,
7958
        $remindList,
7959
        $currentQuestion
7960
    ) {
7961
        $result = Event::get_exercise_results_by_attempt($exeId, 'incomplete');
7962
7963
        if (isset($result[$exeId])) {
7964
            $result = $result[$exeId];
7965
        } else {
7966
            return null;
7967
        }
7968
7969
        $data_tracking = $exercise_stat_info['data_tracking'];
7970
        $data_tracking = explode(',', $data_tracking);
7971
7972
        // if this is the final question do nothing.
7973
        if ($currentQuestion == count($data_tracking)) {
7974
            return null;
7975
        }
7976
7977
        $currentQuestion--;
7978
7979
        if (!empty($result['question_list'])) {
7980
            $answeredQuestions = [];
7981
            foreach ($result['question_list'] as $question) {
7982
                if (!empty($question['answer'])) {
7983
                    $answeredQuestions[] = $question['question_id'];
7984
                }
7985
            }
7986
7987
            // Checking answered questions
7988
            $counterAnsweredQuestions = 0;
7989
            foreach ($data_tracking as $questionId) {
7990
                if (!in_array($questionId, $answeredQuestions)) {
7991
                    if ($currentQuestion != $counterAnsweredQuestions) {
7992
                        break;
7993
                    }
7994
                }
7995
                $counterAnsweredQuestions++;
7996
            }
7997
7998
            $counterRemindListQuestions = 0;
7999
            // Checking questions saved in the reminder list
8000
            if (!empty($remindList)) {
8001
                foreach ($data_tracking as $questionId) {
8002
                    if (in_array($questionId, $remindList)) {
8003
                        // Skip the current question
8004
                        if ($currentQuestion != $counterRemindListQuestions) {
8005
                            break;
8006
                        }
8007
                    }
8008
                    $counterRemindListQuestions++;
8009
                }
8010
8011
                if ($counterRemindListQuestions < $currentQuestion) {
8012
                    return null;
8013
                }
8014
8015
                if (!empty($counterRemindListQuestions)) {
8016
                    if ($counterRemindListQuestions > $counterAnsweredQuestions) {
8017
                        return $counterAnsweredQuestions;
8018
                    } else {
8019
                        return $counterRemindListQuestions;
8020
                    }
8021
                }
8022
            }
8023
8024
            return $counterAnsweredQuestions;
8025
        }
8026
    }
8027
8028
    /**
8029
     * Gets the position of a questionId in the question list.
8030
     *
8031
     * @param $questionId
8032
     *
8033
     * @return int
8034
     */
8035
    public function getPositionInCompressedQuestionList($questionId)
8036
    {
8037
        $questionList = $this->getQuestionListWithMediasCompressed();
8038
        $mediaQuestions = $this->getMediaList();
8039
        $position = 1;
8040
        foreach ($questionList as $id) {
8041
            if (isset($mediaQuestions[$id]) && in_array($questionId, $mediaQuestions[$id])) {
8042
                $mediaQuestionList = $mediaQuestions[$id];
8043
                if (in_array($questionId, $mediaQuestionList)) {
8044
                    return $position;
8045
                } else {
8046
                    $position++;
8047
                }
8048
            } else {
8049
                if ($id == $questionId) {
8050
                    return $position;
8051
                } else {
8052
                    $position++;
8053
                }
8054
            }
8055
        }
8056
8057
        return 1;
8058
    }
8059
8060
    /**
8061
     * Get the correct answers in all attempts.
8062
     *
8063
     * @param int  $learnPathId
8064
     * @param int  $learnPathItemId
8065
     * @param bool $onlyCorrect
8066
     *
8067
     * @return array
8068
     */
8069
    public function getAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0, $onlyCorrect = true)
8070
    {
8071
        $attempts = Event::getExerciseResultsByUser(
8072
            api_get_user_id(),
8073
            $this->id,
8074
            api_get_course_int_id(),
8075
            api_get_session_id(),
8076
            $learnPathId,
8077
            $learnPathItemId,
8078
            'DESC'
8079
        );
8080
8081
        $list = [];
8082
        foreach ($attempts as $attempt) {
8083
            foreach ($attempt['question_list'] as $answers) {
8084
                foreach ($answers as $answer) {
8085
                    $objAnswer = new Answer($answer['question_id']);
8086
                    if ($onlyCorrect) {
8087
                        switch ($objAnswer->getQuestionType()) {
8088
                            case FILL_IN_BLANKS:
8089
                                $isCorrect = FillBlanks::isCorrect($answer['answer']);
8090
                                break;
8091
                            case MATCHING:
8092
                            case DRAGGABLE:
8093
                            case MATCHING_DRAGGABLE:
8094
                                $isCorrect = Matching::isCorrect(
8095
                                    $answer['position'],
8096
                                    $answer['answer'],
8097
                                    $answer['question_id']
8098
                                );
8099
                                break;
8100
                            case ORAL_EXPRESSION:
8101
                                $isCorrect = false;
8102
                                break;
8103
                            default:
8104
                                $isCorrect = $objAnswer->isCorrectByAutoId($answer['answer']);
8105
                        }
8106
                        if ($isCorrect) {
8107
                            $list[$answer['question_id']][] = $answer;
8108
                        }
8109
                    } else {
8110
                        $list[$answer['question_id']][] = $answer;
8111
                    }
8112
                }
8113
            }
8114
8115
            if (false === $onlyCorrect) {
8116
                // Only take latest attempt
8117
                break;
8118
            }
8119
        }
8120
8121
        return $list;
8122
    }
8123
8124
    /**
8125
     * Get the correct answers in all attempts.
8126
     *
8127
     * @param int $learnPathId
8128
     * @param int $learnPathItemId
8129
     *
8130
     * @return array
8131
     */
8132
    public function getCorrectAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0)
8133
    {
8134
        return $this->getAnswersInAllAttempts($learnPathId, $learnPathItemId);
8135
    }
8136
8137
    /**
8138
     * @return bool
8139
     */
8140
    public function showPreviousButton()
8141
    {
8142
        $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
8143
        if (false === $allow) {
8144
            return true;
8145
        }
8146
8147
        return $this->showPreviousButton;
8148
    }
8149
8150
    public function getPreventBackwards()
8151
    {
8152
        $allow = api_get_configuration_value('quiz_prevent_backwards_move');
8153
        if (false === $allow) {
8154
            return 0;
8155
        }
8156
8157
        return (int) $this->preventBackwards;
8158
    }
8159
8160
    /**
8161
     * @return int
8162
     */
8163
    public function getExerciseCategoryId()
8164
    {
8165
        if (empty($this->exerciseCategoryId)) {
8166
            return null;
8167
        }
8168
8169
        return (int) $this->exerciseCategoryId;
8170
    }
8171
8172
    /**
8173
     * @param int $value
8174
     */
8175
    public function setExerciseCategoryId($value)
8176
    {
8177
        if (!empty($value)) {
8178
            $this->exerciseCategoryId = (int) $value;
8179
        }
8180
    }
8181
8182
    /**
8183
     * @param array $values
8184
     *
8185
     * @throws \Doctrine\DBAL\DBALException
8186
     */
8187
    public function setPageResultConfiguration($values)
8188
    {
8189
        $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
8190
        if ($pageConfig) {
8191
            $params = [
8192
                'hide_expected_answer' => isset($values['hide_expected_answer']) ? $values['hide_expected_answer'] : '',
8193
                'hide_question_score' => isset($values['hide_question_score']) ? $values['hide_question_score'] : '',
8194
                'hide_total_score' => isset($values['hide_total_score']) ? $values['hide_total_score'] : '',
8195
            ];
8196
            $type = Type::getType('array');
8197
            $platform = Database::getManager()->getConnection()->getDatabasePlatform();
8198
            $this->pageResultConfiguration = $type->convertToDatabaseValue($params, $platform);
8199
        }
8200
    }
8201
8202
    /**
8203
     * @param array $defaults
8204
     */
8205
    public function setPageResultConfigurationDefaults(&$defaults)
8206
    {
8207
        $configuration = $this->getPageResultConfiguration();
8208
        if (!empty($configuration) && !empty($defaults)) {
8209
            $defaults = array_merge($defaults, $configuration);
8210
        }
8211
    }
8212
8213
    /**
8214
     * @return array
8215
     */
8216
    public function getPageResultConfiguration()
8217
    {
8218
        $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
8219
        if ($pageConfig) {
8220
            /*$params = [
8221
                'hide_expected_answer' => isset($values['hide_expected_answer']) ? $values['hide_expected_answer'] : '',
8222
                'hide_question_score' => isset($values['hide_question_score']) ? $values['hide_question_score'] : '',
8223
                'hide_total_score' => isset($values['hide_total_score']) ? $values['hide_total_score'] : ''
8224
            ];*/
8225
            $type = Type::getType('array');
8226
            $platform = Database::getManager()->getConnection()->getDatabasePlatform();
8227
8228
            return $type->convertToPHPValue($this->pageResultConfiguration, $platform);
8229
        }
8230
8231
        return [];
8232
    }
8233
8234
    /**
8235
     * @param string $attribute
8236
     *
8237
     * @return mixed|null
8238
     */
8239
    public function getPageConfigurationAttribute($attribute)
8240
    {
8241
        $result = $this->getPageResultConfiguration();
8242
8243
        if (!empty($result)) {
8244
            return isset($result[$attribute]) ? $result[$attribute] : null;
8245
        }
8246
8247
        return null;
8248
    }
8249
8250
    /**
8251
     * @param bool $showPreviousButton
8252
     *
8253
     * @return Exercise
8254
     */
8255
    public function setShowPreviousButton($showPreviousButton)
8256
    {
8257
        $this->showPreviousButton = $showPreviousButton;
8258
8259
        return $this;
8260
    }
8261
8262
    /**
8263
     * @param array $notifications
8264
     */
8265
    public function setNotifications($notifications)
8266
    {
8267
        $this->notifications = $notifications;
8268
    }
8269
8270
    /**
8271
     * @return array
8272
     */
8273
    public function getNotifications()
8274
    {
8275
        return $this->notifications;
8276
    }
8277
8278
    /**
8279
     * @return bool
8280
     */
8281
    public function showExpectedChoice()
8282
    {
8283
        return api_get_configuration_value('show_exercise_expected_choice');
8284
    }
8285
8286
    /**
8287
     * @return bool
8288
     */
8289
    public function showExpectedChoiceColumn()
8290
    {
8291
        if ($this->hideExpectedAnswer) {
8292
            return false;
8293
        }
8294
8295
        if (!in_array($this->results_disabled, [
8296
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
8297
        ])
8298
        ) {
8299
            $hide = (int) $this->getPageConfigurationAttribute('hide_expected_answer');
8300
            if (1 === $hide) {
8301
                return false;
8302
            }
8303
8304
            return true;
8305
        }
8306
8307
        return false;
8308
    }
8309
8310
    /**
8311
     * @param string $class
8312
     * @param string $scoreLabel
8313
     * @param string $result
8314
     * @param array
8315
     *
8316
     * @return string
8317
     */
8318
    public function getQuestionRibbon($class, $scoreLabel, $result, $array)
8319
    {
8320
        $hide = (int) $this->getPageConfigurationAttribute('hide_question_score');
8321
        if (1 === $hide) {
8322
            return '';
8323
        }
8324
8325
        if ($this->showExpectedChoice()) {
8326
            $html = null;
8327
            $hideLabel = api_get_configuration_value('exercise_hide_label');
8328
            $label = '<div class="rib rib-'.$class.'">
8329
                        <h3>'.$scoreLabel.'</h3>
8330
                      </div>';
8331
            if (!empty($result)) {
8332
                $label .= '<h4>'.get_lang('Score').': '.$result.'</h4>';
8333
            }
8334
            if (true === $hideLabel) {
8335
                $answerUsed = (int) $array['used'];
8336
                $answerMissing = (int) $array['missing'] - $answerUsed;
8337
                for ($i = 1; $i <= $answerUsed; $i++) {
8338
                    $html .= '<span class="score-img">'.
8339
                        Display::return_icon('attempt-check.png', null, null, ICON_SIZE_SMALL).
8340
                        '</span>';
8341
                }
8342
                for ($i = 1; $i <= $answerMissing; $i++) {
8343
                    $html .= '<span class="score-img">'.
8344
                        Display::return_icon('attempt-nocheck.png', null, null, ICON_SIZE_SMALL).
8345
                        '</span>';
8346
                }
8347
                $label = '<div class="score-title">'.get_lang('CorrectAnswers').': '.$result.'</div>';
8348
                $label .= '<div class="score-limits">';
8349
                $label .= $html;
8350
                $label .= '</div>';
8351
            }
8352
8353
            return '<div class="ribbon">
8354
                '.$label.'
8355
                </div>'
8356
                ;
8357
        } else {
8358
            $html = '<div class="ribbon">
8359
                        <div class="rib rib-'.$class.'">
8360
                            <h3>'.$scoreLabel.'</h3>
8361
                        </div>';
8362
            if (!empty($result)) {
8363
                $html .= '<h4>'.get_lang('Score').': '.$result.'</h4>';
8364
            }
8365
            $html .= '</div>';
8366
8367
            return $html;
8368
        }
8369
    }
8370
8371
    /**
8372
     * @return int
8373
     */
8374
    public function getAutoLaunch()
8375
    {
8376
        return $this->autolaunch;
8377
    }
8378
8379
    /**
8380
     * Clean auto launch settings for all exercise in course/course-session.
8381
     */
8382
    public function enableAutoLaunch()
8383
    {
8384
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8385
        $sql = "UPDATE $table SET autolaunch = 1
8386
                WHERE iid = ".$this->iId;
8387
        Database::query($sql);
8388
    }
8389
8390
    /**
8391
     * Clean auto launch settings for all exercise in course/course-session.
8392
     */
8393
    public function cleanCourseLaunchSettings()
8394
    {
8395
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8396
        $sql = "UPDATE $table SET autolaunch = 0
8397
                WHERE c_id = ".$this->course_id." AND session_id = ".$this->sessionId;
8398
        Database::query($sql);
8399
    }
8400
8401
    /**
8402
     * Get the title without HTML tags.
8403
     *
8404
     * @return string
8405
     */
8406
    public function getUnformattedTitle()
8407
    {
8408
        return strip_tags(api_html_entity_decode($this->title));
8409
    }
8410
8411
    /**
8412
     * Get the question IDs from quiz_rel_question for the current quiz,
8413
     * using the parameters as the arguments to the SQL's LIMIT clause.
8414
     * Because the exercise_id is known, it also comes with a filter on
8415
     * the session, so sessions are not specified here.
8416
     *
8417
     * @param int $start  At which question do we want to start the list
8418
     * @param int $length Up to how many results we want
8419
     *
8420
     * @return array A list of question IDs
8421
     */
8422
    public function getQuestionForTeacher($start = 0, $length = 10)
8423
    {
8424
        $start = (int) $start;
8425
        if ($start < 0) {
8426
            $start = 0;
8427
        }
8428
8429
        $length = (int) $length;
8430
8431
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8432
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
8433
        $sql = "SELECT DISTINCT e.question_id
8434
                FROM $quizRelQuestion e
8435
                INNER JOIN $question q
8436
                ON (e.question_id = q.iid AND e.c_id = q.c_id)
8437
                WHERE
8438
                    e.c_id = {$this->course_id} AND
8439
                    e.exercice_id = '".$this->id."'
8440
                ORDER BY question_order
8441
                LIMIT $start, $length
8442
            ";
8443
        $result = Database::query($sql);
8444
        $questionList = [];
8445
        while ($object = Database::fetch_object($result)) {
8446
            $questionList[] = $object->question_id;
8447
        }
8448
8449
        return $questionList;
8450
    }
8451
8452
    /**
8453
     * @param int   $exerciseId
8454
     * @param array $courseInfo
8455
     * @param int   $sessionId
8456
     *
8457
     * @throws \Doctrine\ORM\OptimisticLockException
8458
     *
8459
     * @return bool
8460
     */
8461
    public function generateStats($exerciseId, $courseInfo, $sessionId)
8462
    {
8463
        $allowStats = api_get_configuration_value('allow_gradebook_stats');
8464
        if (!$allowStats) {
8465
            return false;
8466
        }
8467
8468
        if (empty($courseInfo)) {
8469
            return false;
8470
        }
8471
8472
        $courseId = $courseInfo['real_id'];
8473
8474
        $sessionId = (int) $sessionId;
8475
        $exerciseId = (int) $exerciseId;
8476
8477
        $result = $this->read($exerciseId);
8478
8479
        if (empty($result)) {
8480
            api_not_allowed(true);
8481
        }
8482
8483
        $statusToFilter = empty($sessionId) ? STUDENT : 0;
8484
8485
        $studentList = CourseManager::get_user_list_from_course_code(
8486
            $courseInfo['code'],
8487
            $sessionId,
8488
            null,
8489
            null,
8490
            $statusToFilter
8491
        );
8492
8493
        if (empty($studentList)) {
8494
            Display::addFlash(Display::return_message(get_lang('NoUsersInCourse')));
8495
            header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
8496
            exit;
8497
        }
8498
8499
        $tblStats = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8500
8501
        $studentIdList = [];
8502
        if (!empty($studentList)) {
8503
            $studentIdList = array_column($studentList, 'user_id');
8504
        }
8505
8506
        if (false == $this->exercise_was_added_in_lp) {
8507
            $sql = "SELECT * FROM $tblStats
8508
                        WHERE
8509
                            exe_exo_id = $exerciseId AND
8510
                            orig_lp_id = 0 AND
8511
                            orig_lp_item_id = 0 AND
8512
                            status <> 'incomplete' AND
8513
                            session_id = $sessionId AND
8514
                            c_id = $courseId
8515
                        ";
8516
        } else {
8517
            $lpId = null;
8518
            if (!empty($this->lpList)) {
8519
                // Taking only the first LP
8520
                $lpId = $this->getLpBySession($sessionId);
8521
                $lpId = $lpId['lp_id'];
8522
            }
8523
8524
            $sql = "SELECT *
8525
                        FROM $tblStats
8526
                        WHERE
8527
                            exe_exo_id = $exerciseId AND
8528
                            orig_lp_id = $lpId AND
8529
                            status <> 'incomplete' AND
8530
                            session_id = $sessionId AND
8531
                            c_id = $courseId ";
8532
        }
8533
8534
        $sql .= ' ORDER BY exe_id DESC';
8535
8536
        $studentCount = 0;
8537
        $sum = 0;
8538
        $bestResult = 0;
8539
        $sumResult = 0;
8540
        $result = Database::query($sql);
8541
        while ($data = Database::fetch_array($result, 'ASSOC')) {
8542
            // Only take into account users in the current student list.
8543
            if (!empty($studentIdList)) {
8544
                if (!in_array($data['exe_user_id'], $studentIdList)) {
8545
                    continue;
8546
                }
8547
            }
8548
8549
            if (!isset($students[$data['exe_user_id']])) {
8550
                if ($data['exe_weighting'] != 0) {
8551
                    $students[$data['exe_user_id']] = $data['exe_result'];
8552
                    if ($data['exe_result'] > $bestResult) {
8553
                        $bestResult = $data['exe_result'];
8554
                    }
8555
                    $sumResult += $data['exe_result'];
8556
                }
8557
            }
8558
        }
8559
8560
        $count = count($studentList);
8561
        $average = $sumResult / $count;
8562
        $em = Database::getManager();
8563
8564
        $links = AbstractLink::getGradebookLinksFromItem(
8565
            $this->id,
8566
            LINK_EXERCISE,
8567
            $courseInfo['code'],
8568
            $sessionId
8569
        );
8570
8571
        if (empty($links)) {
8572
            $links = AbstractLink::getGradebookLinksFromItem(
8573
                $this->iId,
8574
                LINK_EXERCISE,
8575
                $courseInfo['code'],
8576
                $sessionId
8577
            );
8578
        }
8579
8580
        if (!empty($links)) {
8581
            $repo = $em->getRepository('ChamiloCoreBundle:GradebookLink');
8582
8583
            foreach ($links as $link) {
8584
                $linkId = $link['id'];
8585
                /** @var GradebookLink $exerciseLink */
8586
                $exerciseLink = $repo->find($linkId);
8587
                if ($exerciseLink) {
8588
                    $exerciseLink
8589
                        ->setUserScoreList($students)
8590
                        ->setBestScore($bestResult)
8591
                        ->setAverageScore($average)
8592
                        ->setScoreWeight($this->get_max_score());
8593
                    $em->persist($exerciseLink);
8594
                    $em->flush();
8595
                }
8596
            }
8597
        }
8598
    }
8599
8600
    /**
8601
     * Return an HTML table of exercises for on-screen printing, including
8602
     * action icons. If no exercise is present and the user can edit the
8603
     * course, show a "create test" button.
8604
     *
8605
     * @param int    $categoryId
8606
     * @param string $keyword
8607
     * @param int    $userId     Optional.
8608
     * @param int    $courseId   Optional.
8609
     * @param int    $sessionId  Optional.
8610
     * @param bool   $returnData Optional.
8611
     *
8612
     * @return string
8613
     */
8614
    public static function exerciseGrid(
8615
        $categoryId,
8616
        $keyword = '',
8617
        $userId = 0,
8618
        $courseId = 0,
8619
        $sessionId = 0,
8620
        $returnData = false
8621
    ) {
8622
        $allowDelete = Exercise::allowAction('delete');
8623
        $allowClean = Exercise::allowAction('clean_results');
8624
8625
        $TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
8626
        $TBL_ITEM_PROPERTY = Database::get_course_table(TABLE_ITEM_PROPERTY);
8627
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8628
        $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
8629
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8630
8631
        $categoryId = (int) $categoryId;
8632
        $keyword = Database::escape_string($keyword);
8633
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
8634
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
8635
8636
        $autoLaunchAvailable = false;
8637
        if (api_get_course_setting('enable_exercise_auto_launch') == 1 &&
8638
            api_get_configuration_value('allow_exercise_auto_launch')
8639
        ) {
8640
            $autoLaunchAvailable = true;
8641
        }
8642
8643
        $is_allowedToEdit = api_is_allowed_to_edit(null, true);
8644
        $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : api_get_course_info();
8645
        $sessionId = $sessionId ? (int) $sessionId : api_get_session_id();
8646
        $courseId = $courseInfo['real_id'];
8647
        $tableRows = [];
8648
        $uploadPath = DIR_HOTPOTATOES; //defined in main_api
8649
        $exercisePath = api_get_self();
8650
        $origin = api_get_origin();
8651
        $userInfo = $userId ? api_get_user_info($userId) : api_get_user_info();
8652
        $charset = 'utf-8';
8653
        $token = Security::get_token();
8654
        $userId = $userId ? (int) $userId : api_get_user_id();
8655
        $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh($userId, $courseInfo);
8656
        $documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
8657
        $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
8658
8659
        // Condition for the session
8660
        $condition_session = api_get_session_condition($sessionId, true, true, 'e.session_id');
8661
        $content = '';
8662
        $column = 0;
8663
        if ($is_allowedToEdit) {
8664
            $column = 1;
8665
        }
8666
8667
        $table = new SortableTableFromArrayConfig(
8668
            [],
8669
            $column,
8670
            self::PAGINATION_ITEMS_PER_PAGE,
8671
            'exercises_cat_'.$categoryId
8672
        );
8673
8674
        $limit = $table->per_page;
8675
        $page = $table->page_nr;
8676
        $from = $limit * ($page - 1);
8677
8678
        $categoryCondition = '';
8679
        if (api_get_configuration_value('allow_exercise_categories')) {
8680
            if (!empty($categoryId)) {
8681
                $categoryCondition = " AND exercise_category_id = $categoryId ";
8682
            } else {
8683
                $categoryCondition = ' AND exercise_category_id IS NULL ';
8684
            }
8685
        }
8686
8687
        $keywordCondition = '';
8688
        if (!empty($keyword)) {
8689
            $keywordCondition = " AND title LIKE '%$keyword%' ";
8690
        }
8691
8692
        // Only for administrators
8693
        if ($is_allowedToEdit) {
8694
            $total_sql = "SELECT count(iid) as count
8695
                          FROM $TBL_EXERCISES e
8696
                          WHERE
8697
                                c_id = $courseId AND
8698
                                active <> -1
8699
                                $condition_session
8700
                                $categoryCondition
8701
                                $keywordCondition
8702
                                ";
8703
            $sql = "SELECT * FROM $TBL_EXERCISES e
8704
                    WHERE
8705
                        c_id = $courseId AND
8706
                        active <> -1
8707
                        $condition_session
8708
                        $categoryCondition
8709
                        $keywordCondition
8710
                    ORDER BY title
8711
                    LIMIT $from , $limit";
8712
        } else {
8713
            // Only for students
8714
            if (empty($sessionId)) {
8715
                $condition_session = ' AND ( session_id = 0 OR session_id IS NULL) ';
8716
                $total_sql = "SELECT count(DISTINCT(e.iid)) as count
8717
                              FROM $TBL_EXERCISES e
8718
                              WHERE
8719
                                    e.c_id = $courseId AND
8720
                                    e.active = 1
8721
                                    $condition_session
8722
                                    $categoryCondition
8723
                                    $keywordCondition
8724
                              ";
8725
8726
                $sql = "SELECT DISTINCT e.* FROM $TBL_EXERCISES e
8727
                        WHERE
8728
                             e.c_id = $courseId AND
8729
                             e.active = 1
8730
                             $condition_session
8731
                             $categoryCondition
8732
                             $keywordCondition
8733
                        ORDER BY title
8734
                        LIMIT $from , $limit";
8735
            } else {
8736
                $invisibleSql = "SELECT e.iid
8737
                                  FROM $TBL_EXERCISES e
8738
                                  INNER JOIN $TBL_ITEM_PROPERTY ip
8739
                                  ON (e.id = ip.ref AND e.c_id = ip.c_id)
8740
                                  WHERE
8741
                                        ip.tool = '".TOOL_QUIZ."' AND
8742
                                        e.c_id = $courseId AND
8743
                                        e.active = 1 AND
8744
                                        ip.visibility = 0 AND
8745
                                        ip.session_id = $sessionId
8746
                                        $categoryCondition
8747
                                        $keywordCondition
8748
                                  ";
8749
8750
                $result = Database::query($invisibleSql);
8751
                $result = Database::store_result($result);
8752
                $hiddenFromSessionCondition = ' 1=1 ';
8753
                if (!empty($result)) {
8754
                    $hiddenFromSession = implode("','", array_column($result, 'iid'));
8755
                    $hiddenFromSessionCondition = " e.iid not in ('$hiddenFromSession')";
8756
                }
8757
8758
                $condition_session = " AND (
8759
                        (e.session_id = $sessionId OR e.session_id = 0 OR e.session_id IS NULL) AND
8760
                        $hiddenFromSessionCondition
8761
                )
8762
                ";
8763
8764
                // Only for students
8765
                $total_sql = "SELECT count(DISTINCT(e.iid)) as count
8766
                              FROM $TBL_EXERCISES e
8767
                              WHERE
8768
                                    e.c_id = $courseId AND
8769
                                    e.active = 1
8770
                                    $condition_session
8771
                                    $categoryCondition
8772
                                    $keywordCondition
8773
                              ";
8774
                $sql = "SELECT DISTINCT e.* FROM $TBL_EXERCISES e
8775
                        WHERE
8776
                             e.c_id = $courseId AND
8777
                             e.active = 1
8778
                             $condition_session
8779
                             $categoryCondition
8780
                             $keywordCondition
8781
                        ORDER BY title
8782
                        LIMIT $from , $limit";
8783
            }
8784
        }
8785
8786
        $result = Database::query($sql);
8787
        $result_total = Database::query($total_sql);
8788
8789
        $total_exercises = 0;
8790
        if (Database::num_rows($result_total)) {
8791
            $result_total = Database::fetch_array($result_total);
8792
            $total_exercises = $result_total['count'];
8793
        }
8794
8795
        //get HotPotatoes files (active and inactive)
8796
        if ($is_allowedToEdit) {
8797
            $sql = "SELECT * FROM $TBL_DOCUMENT
8798
                    WHERE
8799
                        c_id = $courseId AND
8800
                        path LIKE '".Database::escape_string($uploadPath.'/%/%')."'";
8801
            $res = Database::query($sql);
8802
            $hp_count = Database :: num_rows($res);
8803
        } else {
8804
            $sql = "SELECT * FROM $TBL_DOCUMENT d
8805
                    INNER JOIN $TBL_ITEM_PROPERTY ip
8806
                    ON (d.id = ip.ref AND d.c_id = ip.c_id)
8807
                    WHERE
8808
                        ip.tool = '".TOOL_DOCUMENT."' AND
8809
                        d.path LIKE '".Database::escape_string($uploadPath.'/%/%')."' AND
8810
                        ip.visibility = 1 AND
8811
                        d.c_id = $courseId AND
8812
                        ip.c_id  = $courseId";
8813
            $res = Database::query($sql);
8814
            $hp_count = Database::num_rows($res);
8815
        }
8816
8817
        $total = $total_exercises + $hp_count;
8818
        $exerciseList = [];
8819
        while ($row = Database::fetch_array($result, 'ASSOC')) {
8820
            $exerciseList[] = $row;
8821
        }
8822
8823
        if (!empty($exerciseList) &&
8824
            api_get_setting('exercise_invisible_in_session') === 'true'
8825
        ) {
8826
            if (!empty($sessionId)) {
8827
                $changeDefaultVisibility = true;
8828
                if (api_get_setting('configure_exercise_visibility_in_course') === 'true') {
8829
                    $changeDefaultVisibility = false;
8830
                    if (api_get_course_setting('exercise_invisible_in_session') == 1) {
8831
                        $changeDefaultVisibility = true;
8832
                    }
8833
                }
8834
8835
                if ($changeDefaultVisibility) {
8836
                    // Check exercise
8837
                    foreach ($exerciseList as $exercise) {
8838
                        if ($exercise['session_id'] == 0) {
8839
                            $visibilityInfo = api_get_item_property_info(
8840
                                $courseId,
8841
                                TOOL_QUIZ,
8842
                                $exercise['iid'],
8843
                                $sessionId
8844
                            );
8845
8846
                            if (empty($visibilityInfo)) {
8847
                                // Create a record for this
8848
                                api_item_property_update(
8849
                                    $courseInfo,
8850
                                    TOOL_QUIZ,
8851
                                    $exercise['iid'],
8852
                                    'invisible',
8853
                                    api_get_user_id(),
8854
                                    0,
8855
                                    null,
8856
                                    '',
8857
                                    '',
8858
                                    $sessionId
8859
                                );
8860
                            }
8861
                        }
8862
                    }
8863
                }
8864
            }
8865
        }
8866
8867
        $webPath = api_get_path(WEB_CODE_PATH);
8868
8869
        if (!empty($exerciseList)) {
8870
            if ($origin !== 'learnpath') {
8871
                $visibilitySetting = api_get_configuration_value('show_hidden_exercise_added_to_lp');
8872
                //avoid sending empty parameters
8873
                $mylpid = empty($learnpath_id) ? '' : '&learnpath_id='.$learnpath_id;
8874
                $mylpitemid = empty($learnpath_item_id) ? '' : '&learnpath_item_id='.$learnpath_item_id;
8875
                foreach ($exerciseList as $row) {
8876
                    $currentRow = [];
8877
                    $my_exercise_id = $row['id'];
8878
                    $attempt_text = '';
8879
                    $actions = '';
8880
                    $exercise = new Exercise($returnData ? $courseId : 0);
8881
                    $exercise->read($my_exercise_id, false);
8882
8883
                    if (empty($exercise->id)) {
8884
                        continue;
8885
                    }
8886
8887
                    $locked = $exercise->is_gradebook_locked;
8888
                    // Validation when belongs to a session
8889
                    $session_img = api_get_session_image($row['session_id'], $userInfo['status']);
8890
8891
                    $time_limits = false;
8892
                    if (!empty($row['start_time']) || !empty($row['end_time'])) {
8893
                        $time_limits = true;
8894
                    }
8895
8896
                    $is_actived_time = false;
8897
                    if ($time_limits) {
8898
                        // check if start time
8899
                        $start_time = false;
8900
                        if (!empty($row['start_time'])) {
8901
                            $start_time = api_strtotime($row['start_time'], 'UTC');
8902
                        }
8903
                        $end_time = false;
8904
                        if (!empty($row['end_time'])) {
8905
                            $end_time = api_strtotime($row['end_time'], 'UTC');
8906
                        }
8907
                        $now = time();
8908
8909
                        //If both "clocks" are enable
8910
                        if ($start_time && $end_time) {
8911
                            if ($now > $start_time && $end_time > $now) {
8912
                                $is_actived_time = true;
8913
                            }
8914
                        } else {
8915
                            //we check the start and end
8916
                            if ($start_time) {
8917
                                if ($now > $start_time) {
8918
                                    $is_actived_time = true;
8919
                                }
8920
                            }
8921
                            if ($end_time) {
8922
                                if ($end_time > $now) {
8923
                                    $is_actived_time = true;
8924
                                }
8925
                            }
8926
                        }
8927
                    }
8928
8929
                    // Blocking empty start times see BT#2800
8930
                    global $_custom;
8931
                    if (isset($_custom['exercises_hidden_when_no_start_date']) &&
8932
                        $_custom['exercises_hidden_when_no_start_date']
8933
                    ) {
8934
                        if (empty($row['start_time'])) {
8935
                            $time_limits = true;
8936
                            $is_actived_time = false;
8937
                        }
8938
                    }
8939
8940
                    $cut_title = $exercise->getCutTitle();
8941
                    $alt_title = '';
8942
                    if ($cut_title != $row['title']) {
8943
                        $alt_title = ' title = "'.$exercise->getUnformattedTitle().'" ';
8944
                    }
8945
8946
                    // Teacher only
8947
                    if ($is_allowedToEdit) {
8948
                        $lp_blocked = null;
8949
                        if ($exercise->exercise_was_added_in_lp == true) {
8950
                            $lp_blocked = Display::div(
8951
                                get_lang('AddedToLPCannotBeAccessed'),
8952
                                ['class' => 'lp_content_type_label']
8953
                            );
8954
                        }
8955
8956
                        // Get visibility in base course
8957
                        $visibility = api_get_item_visibility(
8958
                            $courseInfo,
8959
                            TOOL_QUIZ,
8960
                            $my_exercise_id,
8961
                            0
8962
                        );
8963
8964
                        if (!empty($sessionId)) {
8965
                            // If we are in a session, the test is invisible
8966
                            // in the base course, it is included in a LP
8967
                            // *and* the setting to show it is *not*
8968
                            // specifically set to true, then hide it.
8969
                            if ($visibility == 0) {
8970
                                if (!$visibilitySetting) {
8971
                                    if ($exercise->exercise_was_added_in_lp == true) {
8972
                                        continue;
8973
                                    }
8974
                                }
8975
                            }
8976
8977
                            $visibility = api_get_item_visibility(
8978
                                $courseInfo,
8979
                                TOOL_QUIZ,
8980
                                $my_exercise_id,
8981
                                $sessionId
8982
                            );
8983
                        }
8984
8985
                        if ($row['active'] == 0 || $visibility == 0) {
8986
                            $title = Display::tag('font', $cut_title, ['style' => 'color:grey']);
8987
                        } else {
8988
                            $title = $cut_title;
8989
                        }
8990
8991
                        /*$count_exercise_not_validated = (int) Event::count_exercise_result_not_validated(
8992
                            $my_exercise_id,
8993
                            $courseId,
8994
                            $sessionId
8995
                        );*/
8996
                        $move = null;
8997
                        $class_tip = '';
8998
                        /*if (!empty($count_exercise_not_validated)) {
8999
                            $results_text = $count_exercise_not_validated == 1 ? get_lang('ResultNotRevised') : get_lang('ResultsNotRevised');
9000
                            $title .= '<span class="exercise_tooltip" style="display: none;">'.$count_exercise_not_validated.' '.$results_text.' </span>';
9001
                        }*/
9002
9003
                        $url = $move.'<a '.$alt_title.' class="'.$class_tip.'" id="tooltip_'.$row['id'].'" href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$row['id'].'">
9004
                             '.Display::return_icon('quiz.png', $row['title']).'
9005
                             '.$title.' </a>'.PHP_EOL;
9006
9007
                        if (ExerciseLib::isQuizEmbeddable($row)) {
9008
                            $embeddableIcon = Display::return_icon('om_integration.png', get_lang('ThisQuizCanBeEmbeddable'));
9009
                            $url .= Display::div($embeddableIcon, ['class' => 'pull-right']);
9010
                        }
9011
9012
                        $currentRow['title'] = $url.' '.$session_img.$lp_blocked;
9013
9014
                        // Count number exercise - teacher
9015
                        $sql = "SELECT count(*) count FROM $TBL_EXERCISE_QUESTION
9016
                                WHERE c_id = $courseId AND exercice_id = $my_exercise_id";
9017
                        $sqlresult = Database::query($sql);
9018
                        $rowi = (int) Database::result($sqlresult, 0, 0);
9019
9020
                        if ($sessionId == $row['session_id']) {
9021
                            // Questions list
9022
                            $actions = Display::url(
9023
                                Display::return_icon('edit.png', get_lang('Edit'), '', ICON_SIZE_SMALL),
9024
                                'admin.php?'.api_get_cidreq().'&exerciseId='.$row['id']
9025
                            );
9026
9027
                            // Test settings
9028
                            $settings = Display::url(
9029
                                Display::return_icon('settings.png', get_lang('Configure'), '', ICON_SIZE_SMALL),
9030
                                'exercise_admin.php?'.api_get_cidreq().'&exerciseId='.$row['id']
9031
                            );
9032
9033
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
9034
                                $settings = '';
9035
                            }
9036
                            $actions .= $settings;
9037
9038
                            // Exercise results
9039
                            $resultsLink = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$row['id'].'">'.
9040
                                Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
9041
9042
                            if ($limitTeacherAccess) {
9043
                                if (api_is_platform_admin()) {
9044
                                    $actions .= $resultsLink;
9045
                                }
9046
                            } else {
9047
                                // Exercise results
9048
                                $actions .= $resultsLink;
9049
                            }
9050
9051
                            // Auto launch
9052
                            if ($autoLaunchAvailable) {
9053
                                $autoLaunch = $exercise->getAutoLaunch();
9054
                                if (empty($autoLaunch)) {
9055
                                    $actions .= Display::url(
9056
                                        Display::return_icon(
9057
                                            'launch_na.png',
9058
                                            get_lang('Enable'),
9059
                                            '',
9060
                                            ICON_SIZE_SMALL
9061
                                        ),
9062
                                        'exercise.php?'.api_get_cidreq().'&choice=enable_launch&sec_token='.$token.'&exerciseId='.$row['id']
9063
                                    );
9064
                                } else {
9065
                                    $actions .= Display::url(
9066
                                        Display::return_icon(
9067
                                            'launch.png',
9068
                                            get_lang('Disable'),
9069
                                            '',
9070
                                            ICON_SIZE_SMALL
9071
                                        ),
9072
                                        'exercise.php?'.api_get_cidreq().'&choice=disable_launch&sec_token='.$token.'&exerciseId='.$row['id']
9073
                                    );
9074
                                }
9075
                            }
9076
9077
                            // Export
9078
                            $actions .= Display::url(
9079
                                Display::return_icon('cd.png', get_lang('CopyExercise')),
9080
                                '',
9081
                                [
9082
                                    'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('AreYouSureToCopy'), ENT_QUOTES, $charset))." ".addslashes($row['title'])."?"."')) return false;",
9083
                                    'href' => 'exercise.php?'.api_get_cidreq().'&choice=copy_exercise&sec_token='.$token.'&exerciseId='.$row['id'],
9084
                                ]
9085
                            );
9086
9087
                            // Clean exercise
9088
                            $clean = '';
9089
                            if (true === $allowClean) {
9090
                                if (false == $locked) {
9091
                                    $clean = Display::url(
9092
                                        Display::return_icon(
9093
                                            'clean.png',
9094
                                            get_lang('CleanStudentResults'),
9095
                                            '',
9096
                                            ICON_SIZE_SMALL
9097
                                        ),
9098
                                        '',
9099
                                        [
9100
                                            'onclick' => "javascript:if(!confirm('".addslashes(
9101
                                                    api_htmlentities(
9102
                                                        get_lang('AreYouSureToDeleteResults'),
9103
                                                        ENT_QUOTES,
9104
                                                        $charset
9105
                                                    )
9106
                                                )." ".addslashes($row['title'])."?"."')) return false;",
9107
                                            'href' => 'exercise.php?'.api_get_cidreq(
9108
                                                ).'&choice=clean_results&sec_token='.$token.'&exerciseId='.$row['id'],
9109
                                        ]
9110
                                    );
9111
                                } else {
9112
                                    $clean = Display::return_icon(
9113
                                        'clean_na.png',
9114
                                        get_lang('ResourceLockedByGradebook'),
9115
                                        '',
9116
                                        ICON_SIZE_SMALL
9117
                                    );
9118
                                }
9119
                            }
9120
9121
                            $actions .= $clean;
9122
                            // Visible / invisible
9123
                            // Check if this exercise was added in a LP
9124
                            if ($exercise->exercise_was_added_in_lp == true) {
9125
                                $visibility = Display::return_icon(
9126
                                    'invisible.png',
9127
                                    get_lang('AddedToLPCannotBeAccessed'),
9128
                                    '',
9129
                                    ICON_SIZE_SMALL
9130
                                );
9131
                            } else {
9132
                                if ($row['active'] == 0 || $visibility == 0) {
9133
                                    $visibility = Display::url(
9134
                                        Display::return_icon(
9135
                                            'invisible.png',
9136
                                            get_lang('Activate'),
9137
                                            '',
9138
                                            ICON_SIZE_SMALL
9139
                                        ),
9140
                                        'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&exerciseId='.$row['id']
9141
                                    );
9142
                                } else {
9143
                                    // else if not active
9144
                                    $visibility = Display::url(
9145
                                        Display::return_icon(
9146
                                            'visible.png',
9147
                                            get_lang('Deactivate'),
9148
                                            '',
9149
                                            ICON_SIZE_SMALL
9150
                                        ),
9151
                                        'exercise.php?'.api_get_cidreq().'&choice=disable&sec_token='.$token.'&exerciseId='.$row['id']
9152
                                    );
9153
                                }
9154
                            }
9155
9156
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
9157
                                $visibility = '';
9158
                            }
9159
9160
                            $actions .= $visibility;
9161
9162
                            // Export qti ...
9163
                            $export = Display::url(
9164
                                Display::return_icon(
9165
                                    'export_qti2.png',
9166
                                    'IMS/QTI',
9167
                                    '',
9168
                                    ICON_SIZE_SMALL
9169
                                ),
9170
                                'exercise.php?action=exportqti2&exerciseId='.$row['id'].'&'.api_get_cidreq()
9171
                            );
9172
9173
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
9174
                                $export = '';
9175
                            }
9176
9177
                            $actions .= $export;
9178
                        } else {
9179
                            // not session
9180
                            $actions = Display::return_icon(
9181
                                'edit_na.png',
9182
                                get_lang('ExerciseEditionNotAvailableInSession')
9183
                            );
9184
9185
                            // Check if this exercise was added in a LP
9186
                            if ($exercise->exercise_was_added_in_lp == true) {
9187
                                $visibility = Display::return_icon(
9188
                                    'invisible.png',
9189
                                    get_lang('AddedToLPCannotBeAccessed'),
9190
                                    '',
9191
                                    ICON_SIZE_SMALL
9192
                                );
9193
                            } else {
9194
                                if ($row['active'] == 0 || $visibility == 0) {
9195
                                    $visibility = Display::url(
9196
                                        Display::return_icon(
9197
                                            'invisible.png',
9198
                                            get_lang('Activate'),
9199
                                            '',
9200
                                            ICON_SIZE_SMALL
9201
                                        ),
9202
                                        'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&exerciseId='.$row['id']
9203
                                    );
9204
                                } else {
9205
                                    // else if not active
9206
                                    $visibility = Display::url(
9207
                                        Display::return_icon(
9208
                                            'visible.png',
9209
                                            get_lang('Deactivate'),
9210
                                            '',
9211
                                            ICON_SIZE_SMALL
9212
                                        ),
9213
                                        'exercise.php?'.api_get_cidreq().'&choice=disable&sec_token='.$token.'&exerciseId='.$row['id']
9214
                                    );
9215
                                }
9216
                            }
9217
9218
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
9219
                                $visibility = '';
9220
                            }
9221
9222
                            $actions .= $visibility;
9223
                            $actions .= '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$row['id'].'">'.
9224
                                Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
9225
                            $actions .= Display::url(
9226
                                Display::return_icon('cd.gif', get_lang('CopyExercise')),
9227
                                '',
9228
                                [
9229
                                    'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('AreYouSureToCopy'), ENT_QUOTES, $charset))." ".addslashes($row['title'])."?"."')) return false;",
9230
                                    'href' => 'exercise.php?'.api_get_cidreq().'&choice=copy_exercise&sec_token='.$token.'&exerciseId='.$row['id'],
9231
                                ]
9232
                            );
9233
                        }
9234
9235
                        // Delete
9236
                        $delete = '';
9237
                        if ($sessionId == $row['session_id']) {
9238
                            if ($locked == false) {
9239
                                $delete = Display::url(
9240
                                    Display::return_icon(
9241
                                        'delete.png',
9242
                                        get_lang('Delete'),
9243
                                        '',
9244
                                        ICON_SIZE_SMALL
9245
                                    ),
9246
                                    '',
9247
                                    [
9248
                                        'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('AreYouSureToDeleteJS'), ENT_QUOTES, $charset))." ".addslashes($exercise->getUnformattedTitle())."?"."')) return false;",
9249
                                        'href' => 'exercise.php?'.api_get_cidreq().'&choice=delete&sec_token='.$token.'&exerciseId='.$row['id'],
9250
                                    ]
9251
                                );
9252
                            } else {
9253
                                $delete = Display::return_icon(
9254
                                    'delete_na.png',
9255
                                    get_lang('ResourceLockedByGradebook'),
9256
                                    '',
9257
                                    ICON_SIZE_SMALL
9258
                                );
9259
                            }
9260
                        }
9261
9262
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9263
                            $delete = '';
9264
                        }
9265
9266
                        $actions .= $delete;
9267
9268
                        // Number of questions
9269
                        $random_label = null;
9270
                        if ($row['random'] > 0 || $row['random'] == -1) {
9271
                            // if random == -1 means use random questions with all questions
9272
                            $random_number_of_question = $row['random'];
9273
                            if ($random_number_of_question == -1) {
9274
                                $random_number_of_question = $rowi;
9275
                            }
9276
                            if ($row['random_by_category'] > 0) {
9277
                                $nbQuestionsTotal = TestCategory::getNumberOfQuestionRandomByCategory(
9278
                                    $my_exercise_id,
9279
                                    $random_number_of_question
9280
                                );
9281
                                $number_of_questions = $nbQuestionsTotal.' ';
9282
                                $number_of_questions .= ($nbQuestionsTotal > 1) ? get_lang('QuestionsLowerCase') : get_lang('QuestionLowerCase');
9283
                                $number_of_questions .= ' - ';
9284
                                $number_of_questions .= min(
9285
                                        TestCategory::getNumberMaxQuestionByCat($my_exercise_id), $random_number_of_question
9286
                                    ).' '.get_lang('QuestionByCategory');
9287
                            } else {
9288
                                $random_label = ' ('.get_lang('Random').') ';
9289
                                $number_of_questions = $random_number_of_question.' '.$random_label.' / '.$rowi;
9290
                                // Bug if we set a random value bigger than the real number of questions
9291
                                if ($random_number_of_question > $rowi) {
9292
                                    $number_of_questions = $rowi.' '.$random_label;
9293
                                }
9294
                            }
9295
                        } else {
9296
                            $number_of_questions = $rowi;
9297
                        }
9298
9299
                        $currentRow['count_questions'] = $number_of_questions;
9300
                    } else {
9301
                        // Student only.
9302
                        $visibility = api_get_item_visibility(
9303
                            $courseInfo,
9304
                            TOOL_QUIZ,
9305
                            $my_exercise_id,
9306
                            $sessionId
9307
                        );
9308
9309
                        if ($visibility == 0) {
9310
                            continue;
9311
                        }
9312
9313
                        $url = '<a '.$alt_title.'  href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$row['id'].'">'.
9314
                            $cut_title.'</a>';
9315
9316
                        // Link of the exercise.
9317
                        $currentRow['title'] = $url.' '.$session_img;
9318
9319
                        if ($returnData) {
9320
                            $currentRow['title'] = $exercise->getUnformattedTitle();
9321
                        }
9322
9323
                        // This query might be improved later on by ordering by the new "tms" field rather than by exe_id
9324
                        // Don't remove this marker: note-query-exe-results
9325
                        $sql = "SELECT * FROM $TBL_TRACK_EXERCISES
9326
                                WHERE
9327
                                    exe_exo_id = ".$row['id']." AND
9328
                                    exe_user_id = $userId AND
9329
                                    c_id = ".api_get_course_int_id()." AND
9330
                                    status <> 'incomplete' AND
9331
                                    orig_lp_id = 0 AND
9332
                                    orig_lp_item_id = 0 AND
9333
                                    session_id =  '".api_get_session_id()."'
9334
                                ORDER BY exe_id DESC";
9335
9336
                        $qryres = Database::query($sql);
9337
                        $num = Database :: num_rows($qryres);
9338
9339
                        // Hide the results.
9340
                        $my_result_disabled = $row['results_disabled'];
9341
9342
                        $attempt_text = '-';
9343
                        // Time limits are on
9344
                        if ($time_limits) {
9345
                            // Exam is ready to be taken
9346
                            if ($is_actived_time) {
9347
                                // Show results
9348
                                if (
9349
                                in_array(
9350
                                    $my_result_disabled,
9351
                                    [
9352
                                        RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9353
                                        RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9354
                                        RESULT_DISABLE_SHOW_SCORE_ONLY,
9355
                                        RESULT_DISABLE_RANKING,
9356
                                    ]
9357
                                )
9358
                                ) {
9359
                                    // More than one attempt
9360
                                    if ($num > 0) {
9361
                                        $row_track = Database :: fetch_array($qryres);
9362
                                        $attempt_text = get_lang('LatestAttempt').' : ';
9363
                                        $attempt_text .= ExerciseLib::show_score(
9364
                                            $row_track['exe_result'],
9365
                                            $row_track['exe_weighting']
9366
                                        );
9367
                                    } else {
9368
                                        //No attempts
9369
                                        $attempt_text = get_lang('NotAttempted');
9370
                                    }
9371
                                } else {
9372
                                    $attempt_text = '-';
9373
                                }
9374
                            } else {
9375
                                // Quiz not ready due to time limits
9376
                                //@todo use the is_visible function
9377
                                if (!empty($row['start_time']) && !empty($row['end_time'])) {
9378
                                    $today = time();
9379
                                    $start_time = api_strtotime($row['start_time'], 'UTC');
9380
                                    $end_time = api_strtotime($row['end_time'], 'UTC');
9381
                                    if ($today < $start_time) {
9382
                                        $attempt_text = sprintf(
9383
                                            get_lang('ExerciseWillBeActivatedFromXToY'),
9384
                                            api_convert_and_format_date($row['start_time']),
9385
                                            api_convert_and_format_date($row['end_time'])
9386
                                        );
9387
                                    } else {
9388
                                        if ($today > $end_time) {
9389
                                            $attempt_text = sprintf(
9390
                                                get_lang('ExerciseWasActivatedFromXToY'),
9391
                                                api_convert_and_format_date($row['start_time']),
9392
                                                api_convert_and_format_date($row['end_time'])
9393
                                            );
9394
                                        }
9395
                                    }
9396
                                } else {
9397
                                    //$attempt_text = get_lang('ExamNotAvailableAtThisTime');
9398
                                    if (!empty($row['start_time'])) {
9399
                                        $attempt_text = sprintf(
9400
                                            get_lang('ExerciseAvailableFromX'),
9401
                                            api_convert_and_format_date($row['start_time'])
9402
                                        );
9403
                                    }
9404
                                    if (!empty($row['end_time'])) {
9405
                                        $attempt_text = sprintf(
9406
                                            get_lang('ExerciseAvailableUntilX'),
9407
                                            api_convert_and_format_date($row['end_time'])
9408
                                        );
9409
                                    }
9410
                                }
9411
                            }
9412
                        } else {
9413
                            // Normal behaviour.
9414
                            // Show results.
9415
                            if (
9416
                            in_array(
9417
                                $my_result_disabled,
9418
                                [
9419
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9420
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9421
                                    RESULT_DISABLE_SHOW_SCORE_ONLY,
9422
                                    RESULT_DISABLE_RANKING,
9423
                                ]
9424
                            )
9425
                            ) {
9426
                                if ($num > 0) {
9427
                                    $row_track = Database :: fetch_array($qryres);
9428
                                    $attempt_text = get_lang('LatestAttempt').' : ';
9429
                                    $attempt_text .= ExerciseLib::show_score(
9430
                                        $row_track['exe_result'],
9431
                                        $row_track['exe_weighting']
9432
                                    );
9433
                                } else {
9434
                                    $attempt_text = get_lang('NotAttempted');
9435
                                }
9436
                            }
9437
                        }
9438
9439
                        if ($returnData) {
9440
                            $attempt_text = $num;
9441
                        }
9442
                    }
9443
9444
                    $currentRow['attempt'] = $attempt_text;
9445
9446
                    if ($is_allowedToEdit) {
9447
                        $additionalActions = ExerciseLib::getAdditionalTeacherActions($row['id']);
9448
9449
                        if (!empty($additionalActions)) {
9450
                            $actions .= $additionalActions.PHP_EOL;
9451
                        }
9452
9453
                        $currentRow = [
9454
                            $row['id'],
9455
                            $currentRow['title'],
9456
                            $currentRow['count_questions'],
9457
                            $actions,
9458
                        ];
9459
                    } else {
9460
                        $currentRow = [
9461
                            $currentRow['title'],
9462
                            $currentRow['attempt'],
9463
                        ];
9464
9465
                        if ($isDrhOfCourse) {
9466
                            $currentRow[] = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$row['id'].'">'.
9467
                                Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
9468
                        }
9469
9470
                        if ($returnData) {
9471
                            $currentRow['id'] = $exercise->id;
9472
                            $currentRow['url'] = $webPath.'exercise/overview.php?'
9473
                                .api_get_cidreq_params($courseInfo['code'], $sessionId).'&'
9474
                                ."$mylpid$mylpitemid&exerciseId={$row['id']}";
9475
                            $currentRow['name'] = $currentRow[0];
9476
                        }
9477
                    }
9478
9479
                    $tableRows[] = $currentRow;
9480
                }
9481
            }
9482
        }
9483
9484
        // end exercise list
9485
        // Hotpotatoes results
9486
        if ($is_allowedToEdit) {
9487
            $sql = "SELECT d.iid, d.path as path, d.comment as comment
9488
                    FROM $TBL_DOCUMENT d
9489
                    WHERE
9490
                        d.c_id = $courseId AND
9491
                        (d.path LIKE '%htm%') AND
9492
                        d.path  LIKE '".Database :: escape_string($uploadPath.'/%/%')."'
9493
                    LIMIT $from , $limit"; // only .htm or .html files listed
9494
        } else {
9495
            $sql = "SELECT d.iid, d.path as path, d.comment as comment
9496
                    FROM $TBL_DOCUMENT d
9497
                    WHERE
9498
                        d.c_id = $courseId AND
9499
                        (d.path LIKE '%htm%') AND
9500
                        d.path  LIKE '".Database :: escape_string($uploadPath.'/%/%')."'
9501
                    LIMIT $from , $limit";
9502
        }
9503
9504
        $result = Database::query($sql);
9505
        $attributes = [];
9506
        while ($row = Database::fetch_array($result, 'ASSOC')) {
9507
            $attributes[$row['iid']] = $row;
9508
        }
9509
9510
        $nbrActiveTests = 0;
9511
        if (!empty($attributes)) {
9512
            foreach ($attributes as $item) {
9513
                $id = $item['iid'];
9514
                $path = $item['path'];
9515
9516
                $title = GetQuizName($path, $documentPath);
9517
                if ($title == '') {
9518
                    $title = basename($path);
9519
                }
9520
9521
                // prof only
9522
                if ($is_allowedToEdit) {
9523
                    $visibility = api_get_item_visibility(
9524
                        ['real_id' => $courseId],
9525
                        TOOL_DOCUMENT,
9526
                        $id,
9527
                        0
9528
                    );
9529
9530
                    if (!empty($sessionId)) {
9531
                        if (0 == $visibility) {
9532
                            continue;
9533
                        }
9534
9535
                        $visibility = api_get_item_visibility(
9536
                            ['real_id' => $courseId],
9537
                            TOOL_DOCUMENT,
9538
                            $id,
9539
                            $sessionId
9540
                        );
9541
                    }
9542
9543
                    $title =
9544
                        implode(PHP_EOL, [
9545
                            Display::return_icon('hotpotatoes_s.png', 'HotPotatoes'),
9546
                            Display::url(
9547
                                $title,
9548
                                'showinframes.php?'.api_get_cidreq().'&'.http_build_query([
9549
                                    'file' => $path,
9550
                                    'uid' => $userId,
9551
                                ]),
9552
                                ['class' => $visibility == 0 ? 'text-muted' : null]
9553
                            ),
9554
                        ]);
9555
9556
                    $actions = Display::url(
9557
                        Display::return_icon(
9558
                            'edit.png',
9559
                            get_lang('Edit'),
9560
                            '',
9561
                            ICON_SIZE_SMALL
9562
                        ),
9563
                        'adminhp.php?'.api_get_cidreq().'&hotpotatoesName='.$path
9564
                    );
9565
9566
                    $actions .= '<a href="hotpotatoes_exercise_report.php?'.api_get_cidreq().'&path='.$path.'">'.
9567
                        Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).
9568
                        '</a>';
9569
9570
                    // if active
9571
                    if ($visibility != 0) {
9572
                        $nbrActiveTests = $nbrActiveTests + 1;
9573
                        $actions .= '      <a href="'.$exercisePath.'?'.api_get_cidreq().'&hpchoice=disable&file='.$path.'">'.
9574
                            Display::return_icon('visible.png', get_lang('Deactivate'), '', ICON_SIZE_SMALL).'</a>';
9575
                    } else { // else if not active
9576
                        $actions .= '    <a href="'.$exercisePath.'?'.api_get_cidreq().'&hpchoice=enable&file='.$path.'">'.
9577
                            Display::return_icon('invisible.png', get_lang('Activate'), '', ICON_SIZE_SMALL).'</a>';
9578
                    }
9579
                    $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;">'.
9580
                        Display::return_icon('delete.png', get_lang('Delete'), '', ICON_SIZE_SMALL).'</a>';
9581
9582
                    $currentRow = [
9583
                        '',
9584
                        $title,
9585
                        '',
9586
                        $actions,
9587
                    ];
9588
                } else {
9589
                    $visibility = api_get_item_visibility(
9590
                        ['real_id' => $courseId],
9591
                        TOOL_DOCUMENT,
9592
                        $id,
9593
                        $sessionId
9594
                    );
9595
9596
                    if (0 == $visibility) {
9597
                        continue;
9598
                    }
9599
9600
                    // Student only
9601
                    $attempt = ExerciseLib::getLatestHotPotatoResult(
9602
                        $path,
9603
                        $userId,
9604
                        api_get_course_int_id(),
9605
                        api_get_session_id()
9606
                    );
9607
9608
                    $nbrActiveTests = $nbrActiveTests + 1;
9609
                    $title = Display::url(
9610
                        $title,
9611
                        'showinframes.php?'.api_get_cidreq().'&'.http_build_query(
9612
                            [
9613
                                'file' => $path,
9614
                                'cid' => api_get_course_id(),
9615
                                'uid' => $userId,
9616
                            ]
9617
                        )
9618
                    );
9619
9620
                    if (!empty($attempt)) {
9621
                        $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>';
9622
                        $attemptText = get_lang('LatestAttempt').' : ';
9623
                        $attemptText .= ExerciseLib::show_score(
9624
                                $attempt['exe_result'],
9625
                                $attempt['exe_weighting']
9626
                            ).' ';
9627
                        $attemptText .= $actions;
9628
                    } else {
9629
                        // No attempts.
9630
                        $attemptText = get_lang('NotAttempted').' ';
9631
                    }
9632
9633
                    $currentRow = [
9634
                        $title,
9635
                        $attemptText,
9636
                    ];
9637
9638
                    if ($isDrhOfCourse) {
9639
                        $currentRow[] = '<a href="hotpotatoes_exercise_report.php?'.api_get_cidreq().'&path='.$path.'">'.
9640
                            Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
9641
                    }
9642
                }
9643
9644
                $tableRows[] = $currentRow;
9645
            }
9646
        }
9647
9648
        if ($returnData) {
9649
            return $tableRows;
9650
        }
9651
9652
        if (empty($tableRows) && empty($categoryId)) {
9653
            if ($is_allowedToEdit && $origin !== 'learnpath') {
9654
                $content .= '<div id="no-data-view">';
9655
                $content .= '<h3>'.get_lang('Quiz').'</h3>';
9656
                $content .= Display::return_icon('quiz.png', '', [], 64);
9657
                $content .= '<div class="controls">';
9658
                $content .= Display::url(
9659
                    '<em class="fa fa-plus"></em> '.get_lang('NewEx'),
9660
                    'exercise_admin.php?'.api_get_cidreq(),
9661
                    ['class' => 'btn btn-primary']
9662
                );
9663
                $content .= '</div>';
9664
                $content .= '</div>';
9665
            }
9666
        } else {
9667
            if (empty($tableRows)) {
9668
                return '';
9669
            }
9670
            $table->setTableData($tableRows);
9671
            $table->setTotalNumberOfItems($total);
9672
            $table->set_additional_parameters([
9673
                'cidReq' => api_get_course_id(),
9674
                'id_session' => api_get_session_id(),
9675
                'category_id' => $categoryId,
9676
            ]);
9677
9678
            if ($is_allowedToEdit) {
9679
                $formActions = [];
9680
                $formActions['visible'] = get_lang('Activate');
9681
                $formActions['invisible'] = get_lang('Deactivate');
9682
                $formActions['delete'] = get_lang('Delete');
9683
                $table->set_form_actions($formActions);
9684
            }
9685
9686
            $i = 0;
9687
            if ($is_allowedToEdit) {
9688
                $table->set_header($i++, '', false, 'width="18px"');
9689
            }
9690
            $table->set_header($i++, get_lang('ExerciseName'), false);
9691
9692
            if ($is_allowedToEdit) {
9693
                $table->set_header($i++, get_lang('QuantityQuestions'), false);
9694
                $table->set_header($i++, get_lang('Actions'), false);
9695
            } else {
9696
                $table->set_header($i++, get_lang('Status'), false);
9697
                if ($isDrhOfCourse) {
9698
                    $table->set_header($i++, get_lang('Actions'), false);
9699
                }
9700
            }
9701
9702
            $content .= $table->return_table();
9703
        }
9704
9705
        return $content;
9706
    }
9707
9708
    /**
9709
     * @return int value in minutes
9710
     */
9711
    public function getResultAccess()
9712
    {
9713
        $extraFieldValue = new ExtraFieldValue('exercise');
9714
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9715
            $this->iId,
9716
            'results_available_for_x_minutes'
9717
        );
9718
9719
        if (!empty($value) && isset($value['value'])) {
9720
            return (int) $value['value'];
9721
        }
9722
9723
        return 0;
9724
    }
9725
9726
    /**
9727
     * @param array $exerciseResultInfo
9728
     *
9729
     * @return bool
9730
     */
9731
    public function getResultAccessTimeDiff($exerciseResultInfo)
9732
    {
9733
        $value = $this->getResultAccess();
9734
        if (!empty($value)) {
9735
            $endDate = new DateTime($exerciseResultInfo['exe_date'], new DateTimeZone('UTC'));
9736
            $endDate->add(new DateInterval('PT'.$value.'M'));
9737
            $now = time();
9738
            if ($endDate->getTimestamp() > $now) {
9739
                return (int) $endDate->getTimestamp() - $now;
9740
            }
9741
        }
9742
9743
        return 0;
9744
    }
9745
9746
    /**
9747
     * @param array $exerciseResultInfo
9748
     *
9749
     * @return bool
9750
     */
9751
    public function hasResultsAccess($exerciseResultInfo)
9752
    {
9753
        $diff = $this->getResultAccessTimeDiff($exerciseResultInfo);
9754
        if (0 === $diff) {
9755
            return false;
9756
        }
9757
9758
        return true;
9759
    }
9760
9761
    /**
9762
     * @return int
9763
     */
9764
    public function getResultsAccess()
9765
    {
9766
        $extraFieldValue = new ExtraFieldValue('exercise');
9767
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9768
            $this->iId,
9769
            'results_available_for_x_minutes'
9770
        );
9771
        if (!empty($value)) {
9772
            return (int) $value;
9773
        }
9774
9775
        return 0;
9776
    }
9777
9778
    /**
9779
     * @param int   $questionId
9780
     * @param bool  $show_results
9781
     * @param array $question_result
9782
     */
9783
    public function getDelineationResult(Question $objQuestionTmp, $questionId, $show_results, $question_result)
9784
    {
9785
        $id = (int) $objQuestionTmp->id;
9786
        $questionId = (int) $questionId;
9787
9788
        $final_overlap = $question_result['extra']['final_overlap'];
9789
        $final_missing = $question_result['extra']['final_missing'];
9790
        $final_excess = $question_result['extra']['final_excess'];
9791
9792
        $overlap_color = $question_result['extra']['overlap_color'];
9793
        $missing_color = $question_result['extra']['missing_color'];
9794
        $excess_color = $question_result['extra']['excess_color'];
9795
9796
        $threadhold1 = $question_result['extra']['threadhold1'];
9797
        $threadhold2 = $question_result['extra']['threadhold2'];
9798
        $threadhold3 = $question_result['extra']['threadhold3'];
9799
9800
        if ($show_results) {
9801
            if ($overlap_color) {
9802
                $overlap_color = 'green';
9803
            } else {
9804
                $overlap_color = 'red';
9805
            }
9806
9807
            if ($missing_color) {
9808
                $missing_color = 'green';
9809
            } else {
9810
                $missing_color = 'red';
9811
            }
9812
            if ($excess_color) {
9813
                $excess_color = 'green';
9814
            } else {
9815
                $excess_color = 'red';
9816
            }
9817
9818
            if (!is_numeric($final_overlap)) {
9819
                $final_overlap = 0;
9820
            }
9821
9822
            if (!is_numeric($final_missing)) {
9823
                $final_missing = 0;
9824
            }
9825
            if (!is_numeric($final_excess)) {
9826
                $final_excess = 0;
9827
            }
9828
9829
            if ($final_excess > 100) {
9830
                $final_excess = 100;
9831
            }
9832
9833
            $table_resume = '
9834
                    <table class="table table-hover table-striped data_table">
9835
                        <tr class="row_odd" >
9836
                            <td>&nbsp;</td>
9837
                            <td><b>'.get_lang('Requirements').'</b></td>
9838
                            <td><b>'.get_lang('YourAnswer').'</b></td>
9839
                        </tr>
9840
                        <tr class="row_even">
9841
                            <td><b>'.get_lang('Overlap').'</b></td>
9842
                            <td>'.get_lang('Min').' '.$threadhold1.'</td>
9843
                            <td>
9844
                                <div style="color:'.$overlap_color.'">
9845
                                    '.(($final_overlap < 0) ? 0 : intval($final_overlap)).'
9846
                                </div>
9847
                            </td>
9848
                        </tr>
9849
                        <tr>
9850
                            <td><b>'.get_lang('Excess').'</b></td>
9851
                            <td>'.get_lang('Max').' '.$threadhold2.'</td>
9852
                            <td>
9853
                                <div style="color:'.$excess_color.'">
9854
                                    '.(($final_excess < 0) ? 0 : intval($final_excess)).'
9855
                                </div>
9856
                            </td>
9857
                        </tr>
9858
                        <tr class="row_even">
9859
                            <td><b>'.get_lang('Missing').'</b></td>
9860
                            <td>'.get_lang('Max').' '.$threadhold3.'</td>
9861
                            <td>
9862
                                <div style="color:'.$missing_color.'">
9863
                                    '.(($final_missing < 0) ? 0 : intval($final_missing)).'
9864
                                </div>
9865
                            </td>
9866
                        </tr>
9867
                    </table>
9868
                ';
9869
9870
            $answerType = $objQuestionTmp->selectType();
9871
            if ($answerType != HOT_SPOT_DELINEATION) {
9872
                $item_list = explode('@@', $destination);
9873
                $try = $item_list[0];
9874
                $lp = $item_list[1];
9875
                $destinationid = $item_list[2];
9876
                $url = $item_list[3];
9877
                $table_resume = '';
9878
            } else {
9879
                if ($next == 0) {
9880
                    $try = $try_hotspot;
9881
                    $lp = $lp_hotspot;
9882
                    $destinationid = $select_question_hotspot;
9883
                    $url = $url_hotspot;
9884
                } else {
9885
                    //show if no error
9886
                    $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
9887
                    $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
9888
                }
9889
            }
9890
9891
            echo '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>';
9892
            if ($answerType == HOT_SPOT_DELINEATION) {
9893
                if ($organs_at_risk_hit > 0) {
9894
                    $message = '<br />'.get_lang('ResultIs').' <b>'.$result_comment.'</b><br />';
9895
                    $message .= '<p style="color:#DC0A0A;"><b>'.get_lang('OARHit').'</b></p>';
9896
                } else {
9897
                    $message = '<p>'.get_lang('YourDelineation').'</p>';
9898
                    $message .= $table_resume;
9899
                    $message .= '<br />'.get_lang('ResultIs').' <b>'.$result_comment.'</b><br />';
9900
                }
9901
                $message .= '<p>'.$comment.'</p>';
9902
                echo $message;
9903
            } else {
9904
                echo '<p>'.$comment.'</p>';
9905
            }
9906
9907
            // Showing the score
9908
            /*$queryfree = "SELECT marks FROM $TBL_TRACK_ATTEMPT
9909
                          WHERE exe_id = $id AND question_id =  $questionId";
9910
            $resfree = Database::query($queryfree);
9911
            $questionScore = Database::result($resfree, 0, 'marks');
9912
            $totalScore += $questionScore;*/
9913
            $relPath = api_get_path(REL_CODE_PATH);
9914
            echo '</table></td></tr>';
9915
            echo "
9916
                        <tr>
9917
                            <td colspan=\"2\">
9918
                                <div id=\"hotspot-solution\"></div>
9919
                                <script>
9920
                                    $(function() {
9921
                                        new HotspotQuestion({
9922
                                            questionId: $questionId,
9923
                                            exerciseId: {$this->id},
9924
                                            exeId: $id,
9925
                                            selector: '#hotspot-solution',
9926
                                            for: 'solution',
9927
                                            relPath: '$relPath'
9928
                                        });
9929
                                    });
9930
                                </script>
9931
                            </td>
9932
                        </tr>
9933
                    </table>
9934
                ";
9935
        }
9936
    }
9937
9938
    /**
9939
     * Clean exercise session variables.
9940
     */
9941
    public static function cleanSessionVariables()
9942
    {
9943
        Session::erase('objExercise');
9944
        Session::erase('exe_id');
9945
        Session::erase('calculatedAnswerId');
9946
        Session::erase('duration_time_previous');
9947
        Session::erase('duration_time');
9948
        Session::erase('objQuestion');
9949
        Session::erase('objAnswer');
9950
        Session::erase('questionList');
9951
        Session::erase('categoryList');
9952
        Session::erase('exerciseResult');
9953
        Session::erase('firstTime');
9954
        Session::erase('time_per_question');
9955
        Session::erase('exerciseResultCoordinates');
9956
        Session::erase('hotspot_coord');
9957
        Session::erase('hotspot_dest');
9958
        Session::erase('hotspot_delineation_result');
9959
    }
9960
9961
    /**
9962
     * Get the first LP found matching the session ID.
9963
     *
9964
     * @param int $sessionId
9965
     *
9966
     * @return array
9967
     */
9968
    public function getLpBySession($sessionId)
9969
    {
9970
        if (!empty($this->lpList)) {
9971
            $sessionId = (int) $sessionId;
9972
9973
            foreach ($this->lpList as $lp) {
9974
                if ((int) $lp['session_id'] == $sessionId) {
9975
                    return $lp;
9976
                }
9977
            }
9978
9979
            return current($this->lpList);
9980
        }
9981
9982
        return [
9983
            'lp_id' => 0,
9984
            'max_score' => 0,
9985
            'session_id' => 0,
9986
        ];
9987
    }
9988
9989
    public static function saveExerciseInLp($safe_item_id, $safe_exe_id)
9990
    {
9991
        $lp = Session::read('oLP');
9992
9993
        $safe_exe_id = (int) $safe_exe_id;
9994
        $safe_item_id = (int) $safe_item_id;
9995
9996
        if (empty($lp) || empty($safe_exe_id) || empty($safe_item_id)) {
9997
            return false;
9998
        }
9999
10000
        $viewId = $lp->get_view_id();
10001
        $course_id = api_get_course_int_id();
10002
        $userId = (int) api_get_user_id();
10003
        $viewId = (int) $viewId;
10004
10005
        $TBL_TRACK_EXERCICES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
10006
        $TBL_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
10007
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
10008
10009
        $sql = "SELECT start_date, exe_date, exe_result, exe_weighting, exe_exo_id, exe_duration
10010
                FROM $TBL_TRACK_EXERCICES
10011
                WHERE exe_id = $safe_exe_id AND exe_user_id = $userId";
10012
        $res = Database::query($sql);
10013
        $row_dates = Database::fetch_array($res);
10014
10015
        if (empty($row_dates)) {
10016
            return false;
10017
        }
10018
10019
        $duration = (int) $row_dates['exe_duration'];
10020
        $score = (float) $row_dates['exe_result'];
10021
        $max_score = (float) $row_dates['exe_weighting'];
10022
10023
        $sql = "UPDATE $TBL_LP_ITEM SET
10024
                    max_score = '$max_score'
10025
                WHERE iid = $safe_item_id";
10026
        Database::query($sql);
10027
10028
        $sql = "SELECT id FROM $TBL_LP_ITEM_VIEW
10029
                WHERE
10030
                    c_id = $course_id AND
10031
                    lp_item_id = $safe_item_id AND
10032
                    lp_view_id = $viewId
10033
                ORDER BY id DESC
10034
                LIMIT 1";
10035
        $res_last_attempt = Database::query($sql);
10036
10037
        if (Database::num_rows($res_last_attempt) && !api_is_invitee()) {
10038
            $row_last_attempt = Database::fetch_row($res_last_attempt);
10039
            $lp_item_view_id = $row_last_attempt[0];
10040
10041
            $exercise = new Exercise($course_id);
10042
            $exercise->read($row_dates['exe_exo_id']);
10043
            $status = 'completed';
10044
10045
            if (!empty($exercise->pass_percentage)) {
10046
                $status = 'failed';
10047
                $success = ExerciseLib::isSuccessExerciseResult(
10048
                    $score,
10049
                    $max_score,
10050
                    $exercise->pass_percentage
10051
                );
10052
                if ($success) {
10053
                    $status = 'passed';
10054
                }
10055
            }
10056
10057
            $sql = "UPDATE $TBL_LP_ITEM_VIEW SET
10058
                        status = '$status',
10059
                        score = $score,
10060
                        total_time = $duration
10061
                    WHERE iid = $lp_item_view_id";
10062
            Database::query($sql);
10063
10064
            $sql = "UPDATE $TBL_TRACK_EXERCICES SET
10065
                        orig_lp_item_view_id = $lp_item_view_id
10066
                    WHERE exe_id = ".$safe_exe_id;
10067
            Database::query($sql);
10068
        }
10069
    }
10070
10071
    /**
10072
     * Get the user answers saved in exercise.
10073
     *
10074
     * @param int $attemptId
10075
     *
10076
     * @return array
10077
     */
10078
    public function getUserAnswersSavedInExercise($attemptId)
10079
    {
10080
        $exerciseResult = [];
10081
10082
        $attemptList = Event::getAllExerciseEventByExeId($attemptId);
10083
10084
        foreach ($attemptList as $questionId => $options) {
10085
            foreach ($options as $option) {
10086
                $question = Question::read($option['question_id']);
10087
                if ($question) {
10088
                    switch ($question->type) {
10089
                        case FILL_IN_BLANKS:
10090
                            $option['answer'] = $this->fill_in_blank_answer_to_string($option['answer']);
10091
                            break;
10092
                    }
10093
                }
10094
10095
                if (!empty($option['answer'])) {
10096
                    $exerciseResult[] = $questionId;
10097
10098
                    break;
10099
                }
10100
            }
10101
        }
10102
10103
        return $exerciseResult;
10104
    }
10105
10106
    /**
10107
     * Get the number of user answers saved in exercise.
10108
     *
10109
     * @param int $attemptId
10110
     *
10111
     * @return int
10112
     */
10113
    public function countUserAnswersSavedInExercise($attemptId)
10114
    {
10115
        $answers = $this->getUserAnswersSavedInExercise($attemptId);
10116
10117
        return count($answers);
10118
    }
10119
10120
    public static function allowAction($action)
10121
    {
10122
        if (api_is_platform_admin()) {
10123
            return true;
10124
        }
10125
10126
        $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
10127
        $disableClean = api_get_configuration_value('disable_clean_exercise_results_for_teachers');
10128
10129
        switch ($action) {
10130
            case 'delete':
10131
                if (api_is_allowed_to_edit(null, true)) {
10132
                    if ($limitTeacherAccess) {
10133
                        return false;
10134
                    }
10135
10136
                    return true;
10137
                }
10138
                break;
10139
            case 'clean_results':
10140
                if (api_is_allowed_to_edit(null, true)) {
10141
                    if ($limitTeacherAccess) {
10142
                        return false;
10143
                    }
10144
10145
                    if ($disableClean) {
10146
                        return false;
10147
                    }
10148
10149
                    return true;
10150
                }
10151
10152
                break;
10153
        }
10154
10155
        return false;
10156
    }
10157
10158
    public static function getLpListFromExercise($exerciseId, $courseId)
10159
    {
10160
        $tableLpItem = Database::get_course_table(TABLE_LP_ITEM);
10161
        $tblLp = Database::get_course_table(TABLE_LP_MAIN);
10162
10163
        $exerciseId = (int) $exerciseId;
10164
        $courseId = (int) $courseId;
10165
10166
        $sql = "SELECT
10167
                    lp.name,
10168
                    lpi.lp_id,
10169
                    lpi.max_score,
10170
                    lp.session_id
10171
                FROM $tableLpItem lpi
10172
                INNER JOIN $tblLp lp
10173
                ON (lpi.lp_id = lp.iid AND lpi.c_id = lp.c_id)
10174
                WHERE
10175
                    lpi.c_id = $courseId AND
10176
                    lpi.item_type = '".TOOL_QUIZ."' AND
10177
                    lpi.path = '$exerciseId'";
10178
        $result = Database::query($sql);
10179
        $lpList = [];
10180
        if (Database::num_rows($result) > 0) {
10181
            $lpList = Database::store_result($result, 'ASSOC');
10182
        }
10183
10184
        return $lpList;
10185
    }
10186
10187
    /**
10188
     * Get number of questions in exercise by user attempt.
10189
     *
10190
     * @return int
10191
     */
10192
    private function countQuestionsInExercise()
10193
    {
10194
        $lpId = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10195
        $lpItemId = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10196
        $lpItemViewId = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10197
10198
        $trackInfo = $this->get_stat_track_exercise_info($lpId, $lpItemId, $lpItemViewId);
10199
10200
        if (!empty($trackInfo)) {
10201
            $questionIds = explode(',', $trackInfo['data_tracking']);
10202
10203
            return count($questionIds);
10204
        }
10205
10206
        return $this->getQuestionCount();
10207
    }
10208
10209
    /**
10210
     * Gets the question list ordered by the question_order setting (drag and drop).
10211
     *
10212
     * @param bool $adminView Optional.
10213
     *
10214
     * @return array
10215
     */
10216
    private function getQuestionOrderedList($adminView = false)
10217
    {
10218
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
10219
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
10220
10221
        // Getting question_order to verify that the question
10222
        // list is correct and all question_order's were set
10223
        $sql = "SELECT DISTINCT count(e.question_order) as count
10224
                FROM $TBL_EXERCICE_QUESTION e
10225
                INNER JOIN $TBL_QUESTIONS q
10226
                ON (e.question_id = q.id AND e.c_id = q.c_id)
10227
                WHERE
10228
                  e.c_id = {$this->course_id} AND
10229
                  e.exercice_id	= ".$this->id;
10230
10231
        $result = Database::query($sql);
10232
        $row = Database::fetch_array($result);
10233
        $count_question_orders = $row['count'];
10234
10235
        // Getting question list from the order (question list drag n drop interface).
10236
        $sql = "SELECT DISTINCT e.question_id, e.question_order
10237
                FROM $TBL_EXERCICE_QUESTION e
10238
                INNER JOIN $TBL_QUESTIONS q
10239
                ON (e.question_id = q.id AND e.c_id = q.c_id)
10240
                WHERE
10241
                    e.c_id = {$this->course_id} AND
10242
                    e.exercice_id = '".$this->id."'
10243
                ORDER BY question_order";
10244
        $result = Database::query($sql);
10245
10246
        // Fills the array with the question ID for this exercise
10247
        // the key of the array is the question position
10248
        $temp_question_list = [];
10249
        $counter = 1;
10250
        $questionList = [];
10251
        while ($new_object = Database::fetch_object($result)) {
10252
            if (!$adminView) {
10253
                // Correct order.
10254
                $questionList[$new_object->question_order] = $new_object->question_id;
10255
            } else {
10256
                $questionList[$counter] = $new_object->question_id;
10257
            }
10258
10259
            // Just in case we save the order in other array
10260
            $temp_question_list[$counter] = $new_object->question_id;
10261
            $counter++;
10262
        }
10263
10264
        if (!empty($temp_question_list)) {
10265
            /* If both array don't match it means that question_order was not correctly set
10266
               for all questions using the default mysql order */
10267
            if (count($temp_question_list) != $count_question_orders) {
10268
                $questionList = $temp_question_list;
10269
            }
10270
        }
10271
10272
        return $questionList;
10273
    }
10274
10275
    /**
10276
     * Select N values from the questions per category array.
10277
     *
10278
     * @param array $categoriesAddedInExercise
10279
     * @param array $question_list
10280
     * @param array $questions_by_category
10281
     * @param bool  $flatResult
10282
     * @param bool  $randomizeQuestions
10283
     * @param array $questionsByCategoryMandatory
10284
     *
10285
     * @return array
10286
     */
10287
    private function pickQuestionsPerCategory(
10288
        $categoriesAddedInExercise,
10289
        $question_list,
10290
        &$questions_by_category,
10291
        $flatResult = true,
10292
        $randomizeQuestions = false,
10293
        $questionsByCategoryMandatory = []
10294
    ) {
10295
        $addAll = true;
10296
        $categoryCountArray = [];
10297
10298
        // Getting how many questions will be selected per category.
10299
        if (!empty($categoriesAddedInExercise)) {
10300
            $addAll = false;
10301
            // Parsing question according the category rel exercise settings
10302
            foreach ($categoriesAddedInExercise as $category_info) {
10303
                $category_id = $category_info['category_id'];
10304
                if (isset($questions_by_category[$category_id])) {
10305
                    // How many question will be picked from this category.
10306
                    $count = $category_info['count_questions'];
10307
                    // -1 means all questions
10308
                    $categoryCountArray[$category_id] = $count;
10309
                    if (-1 == $count) {
10310
                        $categoryCountArray[$category_id] = 999;
10311
                    }
10312
                }
10313
            }
10314
        }
10315
10316
        if (!empty($questions_by_category)) {
10317
            $temp_question_list = [];
10318
            foreach ($questions_by_category as $category_id => &$categoryQuestionList) {
10319
                if (isset($categoryCountArray) && !empty($categoryCountArray)) {
10320
                    $numberOfQuestions = 0;
10321
                    if (isset($categoryCountArray[$category_id])) {
10322
                        $numberOfQuestions = $categoryCountArray[$category_id];
10323
                    }
10324
                }
10325
10326
                if ($addAll) {
10327
                    $numberOfQuestions = 999;
10328
                }
10329
                if (!empty($numberOfQuestions)) {
10330
                    $mandatoryQuestions = [];
10331
                    if (isset($questionsByCategoryMandatory[$category_id])) {
10332
                        $mandatoryQuestions = $questionsByCategoryMandatory[$category_id];
10333
                    }
10334
10335
                    $elements = TestCategory::getNElementsFromArray(
10336
                        $categoryQuestionList,
10337
                        $numberOfQuestions,
10338
                        $randomizeQuestions,
10339
                        $mandatoryQuestions
10340
                    );
10341
                    if (!empty($elements)) {
10342
                        $temp_question_list[$category_id] = $elements;
10343
                        $categoryQuestionList = $elements;
10344
                    }
10345
                }
10346
            }
10347
10348
            if (!empty($temp_question_list)) {
10349
                if ($flatResult) {
10350
                    $temp_question_list = array_flatten($temp_question_list);
10351
                }
10352
                $question_list = $temp_question_list;
10353
            }
10354
        }
10355
10356
        return $question_list;
10357
    }
10358
10359
    /**
10360
     * Changes the exercise id.
10361
     *
10362
     * @param int $id - exercise id
10363
     */
10364
    private function updateId($id)
10365
    {
10366
        $this->id = $id;
10367
    }
10368
10369
    /**
10370
     * Sends a notification when a user ends an examn.
10371
     *
10372
     * @param array  $question_list_answers
10373
     * @param string $origin
10374
     * @param array  $user_info
10375
     * @param string $url_email
10376
     * @param array  $teachers
10377
     */
10378
    private function sendNotificationForOpenQuestions(
10379
        $question_list_answers,
10380
        $origin,
10381
        $user_info,
10382
        $url_email,
10383
        $teachers
10384
    ) {
10385
        // Email configuration settings
10386
        $courseCode = api_get_course_id();
10387
        $courseInfo = api_get_course_info($courseCode);
10388
        $sessionId = api_get_session_id();
10389
        $sessionData = '';
10390
        if (!empty($sessionId)) {
10391
            $sessionInfo = api_get_session_info($sessionId);
10392
            if (!empty($sessionInfo)) {
10393
                $sessionData = '<tr>'
10394
                    .'<td><em>'.get_lang('SessionName').'</em></td>'
10395
                    .'<td>&nbsp;<b>'.$sessionInfo['name'].'</b></td>'
10396
                    .'</tr>';
10397
            }
10398
        }
10399
10400
        $msg = get_lang('OpenQuestionsAttempted').'<br /><br />'
10401
            .get_lang('AttemptDetails').' : <br /><br />'
10402
            .'<table>'
10403
            .'<tr>'
10404
            .'<td><em>'.get_lang('CourseName').'</em></td>'
10405
            .'<td>&nbsp;<b>#course#</b></td>'
10406
            .'</tr>'
10407
            .$sessionData
10408
            .'<tr>'
10409
            .'<td>'.get_lang('TestAttempted').'</td>'
10410
            .'<td>&nbsp;#exercise#</td>'
10411
            .'</tr>'
10412
            .'<tr>'
10413
            .'<td>'.get_lang('StudentName').'</td>'
10414
            .'<td>&nbsp;#firstName# #lastName#</td>'
10415
            .'</tr>'
10416
            .'<tr>'
10417
            .'<td>'.get_lang('StudentEmail').'</td>'
10418
            .'<td>&nbsp;#mail#</td>'
10419
            .'</tr>'
10420
            .'</table>';
10421
10422
        $open_question_list = null;
10423
        foreach ($question_list_answers as $item) {
10424
            $question = $item['question'];
10425
            $answer = $item['answer'];
10426
            $answer_type = $item['answer_type'];
10427
            if (!empty($question) && !empty($answer) && $answer_type == FREE_ANSWER) {
10428
                $open_question_list .=
10429
                    '<tr>'
10430
                    .'<td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>'
10431
                    .'<td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>'
10432
                    .'</tr>'
10433
                    .'<tr>'
10434
                    .'<td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>'
10435
                    .'<td valign="top" bgcolor="#F3F3F3">'.$answer.'</td>'
10436
                    .'</tr>';
10437
            }
10438
        }
10439
10440
        if (!empty($open_question_list)) {
10441
            $msg .= '<p><br />'.get_lang('OpenQuestionsAttemptedAre').' :</p>'.
10442
                '<table width="730" height="136" border="0" cellpadding="3" cellspacing="3">';
10443
            $msg .= $open_question_list;
10444
            $msg .= '</table><br />';
10445
10446
            $msg = str_replace('#exercise#', $this->exercise, $msg);
10447
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg);
10448
            $msg = str_replace('#lastName#', $user_info['lastname'], $msg);
10449
            $msg = str_replace('#mail#', $user_info['email'], $msg);
10450
            $msg = str_replace(
10451
                '#course#',
10452
                Display::url($courseInfo['title'], $courseInfo['course_public_url'].'?id_session='.$sessionId),
10453
                $msg
10454
            );
10455
10456
            if ($origin != 'learnpath') {
10457
                $msg .= '<br /><a href="#url#">'.get_lang('ClickToCommentAndGiveFeedback').'</a>';
10458
            }
10459
            $msg = str_replace('#url#', $url_email, $msg);
10460
            $subject = get_lang('OpenQuestionsAttempted');
10461
10462
            if (!empty($teachers)) {
10463
                foreach ($teachers as $user_id => $teacher_data) {
10464
                    MessageManager::send_message_simple(
10465
                        $user_id,
10466
                        $subject,
10467
                        $msg
10468
                    );
10469
                }
10470
            }
10471
        }
10472
    }
10473
10474
    /**
10475
     * Send notification for oral questions.
10476
     *
10477
     * @param array  $question_list_answers
10478
     * @param string $origin
10479
     * @param int    $exe_id
10480
     * @param array  $user_info
10481
     * @param string $url_email
10482
     * @param array  $teachers
10483
     */
10484
    private function sendNotificationForOralQuestions(
10485
        $question_list_answers,
10486
        $origin,
10487
        $exe_id,
10488
        $user_info,
10489
        $url_email,
10490
        $teachers
10491
    ) {
10492
        // Email configuration settings
10493
        $courseCode = api_get_course_id();
10494
        $courseInfo = api_get_course_info($courseCode);
10495
        $oral_question_list = null;
10496
        foreach ($question_list_answers as $item) {
10497
            $question = $item['question'];
10498
            $file = $item['generated_oral_file'];
10499
            $answer = $item['answer'];
10500
            if (0 == $answer) {
10501
                $answer = '';
10502
            }
10503
            $answer_type = $item['answer_type'];
10504
            if (!empty($question) && (!empty($answer) || !empty($file)) && $answer_type == ORAL_EXPRESSION) {
10505
                if (!empty($file)) {
10506
                    $file = Display::url($file, $file);
10507
                }
10508
                $oral_question_list .= '<br />
10509
                    <table width="730" height="136" border="0" cellpadding="3" cellspacing="3">
10510
                    <tr>
10511
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
10512
                        <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
10513
                    </tr>
10514
                    <tr>
10515
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
10516
                        <td valign="top" bgcolor="#F3F3F3">'.$answer.$file.'</td>
10517
                    </tr></table>';
10518
            }
10519
        }
10520
10521
        if (!empty($oral_question_list)) {
10522
            $msg = get_lang('OralQuestionsAttempted').'<br /><br />
10523
                    '.get_lang('AttemptDetails').' : <br /><br />
10524
                    <table>
10525
                        <tr>
10526
                            <td><em>'.get_lang('CourseName').'</em></td>
10527
                            <td>&nbsp;<b>#course#</b></td>
10528
                        </tr>
10529
                        <tr>
10530
                            <td>'.get_lang('TestAttempted').'</td>
10531
                            <td>&nbsp;#exercise#</td>
10532
                        </tr>
10533
                        <tr>
10534
                            <td>'.get_lang('StudentName').'</td>
10535
                            <td>&nbsp;#firstName# #lastName#</td>
10536
                        </tr>
10537
                        <tr>
10538
                            <td>'.get_lang('StudentEmail').'</td>
10539
                            <td>&nbsp;#mail#</td>
10540
                        </tr>
10541
                    </table>';
10542
            $msg .= '<br />'.sprintf(get_lang('OralQuestionsAttemptedAreX'), $oral_question_list).'<br />';
10543
            $msg1 = str_replace("#exercise#", $this->exercise, $msg);
10544
            $msg = str_replace("#firstName#", $user_info['firstname'], $msg1);
10545
            $msg1 = str_replace("#lastName#", $user_info['lastname'], $msg);
10546
            $msg = str_replace("#mail#", $user_info['email'], $msg1);
10547
            $msg = str_replace("#course#", $courseInfo['name'], $msg1);
10548
10549
            if (!in_array($origin, ['learnpath', 'embeddable'])) {
10550
                $msg .= '<br /><a href="#url#">'.get_lang('ClickToCommentAndGiveFeedback').'</a>';
10551
            }
10552
            $msg1 = str_replace("#url#", $url_email, $msg);
10553
            $mail_content = $msg1;
10554
            $subject = get_lang('OralQuestionsAttempted');
10555
10556
            if (!empty($teachers)) {
10557
                foreach ($teachers as $user_id => $teacher_data) {
10558
                    MessageManager::send_message_simple(
10559
                        $user_id,
10560
                        $subject,
10561
                        $mail_content
10562
                    );
10563
                }
10564
            }
10565
        }
10566
    }
10567
10568
    /**
10569
     * Returns an array with the media list.
10570
     *
10571
     * @param array question list
10572
     *
10573
     * @example there's 1 question with iid 5 that belongs to the media question with iid = 100
10574
     * <code>
10575
     * array (size=2)
10576
     *  999 =>
10577
     *    array (size=3)
10578
     *      0 => int 7
10579
     *      1 => int 6
10580
     *      2 => int 3254
10581
     *  100 =>
10582
     *   array (size=1)
10583
     *      0 => int 5
10584
     *  </code>
10585
     */
10586
    private function setMediaList($questionList)
10587
    {
10588
        $mediaList = [];
10589
        /*
10590
         * Media feature is not activated in 1.11.x
10591
        if (!empty($questionList)) {
10592
            foreach ($questionList as $questionId) {
10593
                $objQuestionTmp = Question::read($questionId, $this->course_id);
10594
                // If a media question exists
10595
                if (isset($objQuestionTmp->parent_id) && $objQuestionTmp->parent_id != 0) {
10596
                    $mediaList[$objQuestionTmp->parent_id][] = $objQuestionTmp->id;
10597
                } else {
10598
                    // Always the last item
10599
                    $mediaList[999][] = $objQuestionTmp->id;
10600
                }
10601
            }
10602
        }*/
10603
10604
        $this->mediaList = $mediaList;
10605
    }
10606
10607
    /**
10608
     * @return HTML_QuickForm_group
10609
     */
10610
    private function setResultDisabledGroup(FormValidator $form)
10611
    {
10612
        $resultDisabledGroup = [];
10613
10614
        $resultDisabledGroup[] = $form->createElement(
10615
            'radio',
10616
            'results_disabled',
10617
            null,
10618
            get_lang('ShowScoreAndRightAnswer'),
10619
            '0',
10620
            ['id' => 'result_disabled_0']
10621
        );
10622
10623
        $resultDisabledGroup[] = $form->createElement(
10624
            'radio',
10625
            'results_disabled',
10626
            null,
10627
            get_lang('DoNotShowScoreNorRightAnswer'),
10628
            '1',
10629
            ['id' => 'result_disabled_1', 'onclick' => 'check_results_disabled()']
10630
        );
10631
10632
        $resultDisabledGroup[] = $form->createElement(
10633
            'radio',
10634
            'results_disabled',
10635
            null,
10636
            get_lang('OnlyShowScore'),
10637
            '2',
10638
            ['id' => 'result_disabled_2', 'onclick' => 'check_results_disabled()']
10639
        );
10640
10641
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
10642
            return $form->addGroup(
10643
                $resultDisabledGroup,
10644
                null,
10645
                get_lang('ShowResultsToStudents')
10646
            );
10647
        }
10648
10649
        $resultDisabledGroup[] = $form->createElement(
10650
            'radio',
10651
            'results_disabled',
10652
            null,
10653
            get_lang('ShowScoreEveryAttemptShowAnswersLastAttempt'),
10654
            '4',
10655
            ['id' => 'result_disabled_4']
10656
        );
10657
10658
        $resultDisabledGroup[] = $form->createElement(
10659
            'radio',
10660
            'results_disabled',
10661
            null,
10662
            get_lang('DontShowScoreOnlyWhenUserFinishesAllAttemptsButShowFeedbackEachAttempt'),
10663
            '5',
10664
            ['id' => 'result_disabled_5', 'onclick' => 'check_results_disabled()']
10665
        );
10666
10667
        $resultDisabledGroup[] = $form->createElement(
10668
            'radio',
10669
            'results_disabled',
10670
            null,
10671
            get_lang('ExerciseRankingMode'),
10672
            RESULT_DISABLE_RANKING,
10673
            ['id' => 'result_disabled_6']
10674
        );
10675
10676
        $resultDisabledGroup[] = $form->createElement(
10677
            'radio',
10678
            'results_disabled',
10679
            null,
10680
            get_lang('ExerciseShowOnlyGlobalScoreAndCorrectAnswers'),
10681
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
10682
            ['id' => 'result_disabled_7']
10683
        );
10684
10685
        $resultDisabledGroup[] = $form->createElement(
10686
            'radio',
10687
            'results_disabled',
10688
            null,
10689
            get_lang('ExerciseAutoEvaluationAndRankingMode'),
10690
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
10691
            ['id' => 'result_disabled_8']
10692
        );
10693
10694
        return $form->addGroup(
10695
            $resultDisabledGroup,
10696
            null,
10697
            get_lang('ShowResultsToStudents')
10698
        );
10699
    }
10700
}
10701