Passed
Push — 1.11.x ( bba802...2f1e0f )
by Julito
10:19
created

Exercise::setPageResultConfiguration()   A

Complexity

Conditions 5

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
5803
                        $ans = $reply[$i];
5804
                        Event::saveQuestionAttempt($questionScore, $ans, $quesId, $exeId, $i, $this->id);
5805
                    }
5806
                } else {
5807
                    Event::saveQuestionAttempt($questionScore, 0, $quesId, $exeId, 0, $this->id);
5808
                }
5809
            } elseif ($answerType == MULTIPLE_ANSWER_COMBINATION) {
5810
                if ($choice != 0) {
5811
                    $reply = array_keys($choice);
5812
                    for ($i = 0; $i < count($reply); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

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

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

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