Completed
Push — master ( cbf730...0a6a4f )
by Julito
18:04 queued 08:12
created

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