Completed
Push — master ( 76a038...0c6f8d )
by Julito
10:18
created

Exercise::show_button()   F

Complexity

Conditions 32
Paths 1594

Size

Total Lines 166
Code Lines 107

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 32
eloc 107
nc 1594
nop 5
dl 0
loc 166
rs 0
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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