Passed
Push — master ( 3af017...447a4c )
by Julito
09:24
created

Exercise::addToList()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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