Passed
Push — 1.11.x ( 7a39aa...d775d8 )
by Julito
09:20
created

Exercise::getExerciseAndResult()   A

Complexity

Conditions 5

Size

Total Lines 39
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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