Completed
Push — master ( 5b557d...3742bc )
by Julito
12:11
created

Exercise::enable_results()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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