Completed
Push — master ( c65fe1...e7429e )
by Julito
11:34
created

Exercise::search_engine_edit()   B

Complexity

Conditions 9
Paths 18

Size

Total Lines 69
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 47
nc 18
nop 0
dl 0
loc 69
rs 7.6008
c 0
b 0
f 0

How to fix   Long Method   

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 iid = $id";
175
        $result = Database::query($sql);
176
177
        // if the exercise has been found
178
        if ($object = Database::fetch_object($result)) {
179
            $this->id = $this->iId = $object->iid;
180
            $this->exercise = $object->title;
181
            $this->name = $object->title;
182
            $this->title = $object->title;
183
            $this->description = $object->description;
184
            $this->sound = $object->sound;
185
            $this->type = $object->type;
186
            if (empty($this->type)) {
187
                $this->type = ONE_PER_PAGE;
188
            }
189
            $this->random = $object->random;
190
            $this->random_answers = $object->random_answers;
191
            $this->active = $object->active;
192
            $this->results_disabled = $object->results_disabled;
193
            $this->attempts = $object->max_attempt;
194
            $this->feedback_type = $object->feedback_type;
195
            $this->sessionId = $object->session_id;
196
            $this->propagate_neg = $object->propagate_neg;
197
            $this->saveCorrectAnswers = $object->save_correct_answers;
198
            $this->randomByCat = $object->random_by_category;
199
            $this->text_when_finished = $object->text_when_finished;
200
            $this->display_category_name = $object->display_category_name;
201
            $this->pass_percentage = $object->pass_percentage;
202
            $this->is_gradebook_locked = api_resource_is_locked_by_gradebook($id, LINK_EXERCISE);
203
            $this->review_answers = isset($object->review_answers) && 1 == $object->review_answers ? true : false;
204
            $this->globalCategoryId = isset($object->global_category_id) ? $object->global_category_id : null;
205
            $this->questionSelectionType = isset($object->question_selection_type) ? $object->question_selection_type : null;
206
            $this->hideQuestionTitle = isset($object->hide_question_title) ? (int) $object->hide_question_title : 0;
207
            $this->autolaunch = isset($object->autolaunch) ? (int) $object->autolaunch : 0;
208
            $this->exerciseCategoryId = isset($object->exercise_category_id) ? (int) $object->exercise_category_id : null;
209
            $this->preventBackwards = isset($object->prevent_backwards) ? (int) $object->prevent_backwards : 0;
210
            $this->exercise_was_added_in_lp = false;
211
            $this->lpList = [];
212
            $this->notifications = [];
213
            if (!empty($object->notifications)) {
214
                $this->notifications = explode(',', $object->notifications);
215
            }
216
217
            if (!empty($object->page_result_configuration)) {
218
                $this->pageResultConfiguration = $object->page_result_configuration;
219
            }
220
221
            if (isset($object->show_previous_button)) {
222
                $this->showPreviousButton = 1 == $object->show_previous_button ? true : false;
223
            }
224
225
            $list = self::getLpListFromExercise($id, $this->course_id);
226
            if (!empty($list)) {
227
                $this->exercise_was_added_in_lp = true;
228
                $this->lpList = $list;
229
            }
230
231
            $this->force_edit_exercise_in_lp = api_get_configuration_value('force_edit_exercise_in_lp');
232
            $this->edit_exercise_in_lp = true;
233
            if ($this->exercise_was_added_in_lp) {
234
                $this->edit_exercise_in_lp = $this->force_edit_exercise_in_lp == true;
235
            }
236
237
            if (!empty($object->end_time)) {
238
                $this->end_time = $object->end_time;
239
            }
240
            if (!empty($object->start_time)) {
241
                $this->start_time = $object->start_time;
242
            }
243
244
            // Control time
245
            $this->expired_time = $object->expired_time;
246
247
            // Checking if question_order is correctly set
248
            if ($parseQuestionList) {
249
                $this->setQuestionList(true);
250
            }
251
252
            //overload questions list with recorded questions list
253
            //load questions only for exercises of type 'one question per page'
254
            //this is needed only is there is no questions
255
256
            // @todo not sure were in the code this is used somebody mess with the exercise tool
257
            // @todo don't know who add that config and why $_configuration['live_exercise_tracking']
258
            /*global $_configuration, $questionList;
259
            if ($this->type == ONE_PER_PAGE && $_SERVER['REQUEST_METHOD'] != 'POST'
260
                && defined('QUESTION_LIST_ALREADY_LOGGED') &&
261
                isset($_configuration['live_exercise_tracking']) && $_configuration['live_exercise_tracking']
262
            ) {
263
                $this->questionList = $questionList;
264
            }*/
265
            return true;
266
        }
267
268
        return false;
269
    }
270
271
    /**
272
     * @return string
273
     */
274
    public function getCutTitle()
275
    {
276
        $title = $this->getUnformattedTitle();
277
278
        return cut($title, EXERCISE_MAX_NAME_SIZE);
279
    }
280
281
    /**
282
     * returns the exercise ID.
283
     * @deprecated
284
     * @author Olivier Brouckaert
285
     *
286
     * @return int - exercise ID
287
     */
288
    public function selectId()
289
    {
290
        return $this->iId;
291
    }
292
293
    public function getId()
294
    {
295
        return (int) $this->iId;
296
    }
297
298
    /**
299
     * returns the exercise title.
300
     *
301
     * @author Olivier Brouckaert
302
     *
303
     * @param bool $unformattedText Optional. Get the title without HTML tags
304
     *
305
     * @return string - exercise title
306
     */
307
    public function selectTitle($unformattedText = false)
308
    {
309
        if ($unformattedText) {
310
            return $this->getUnformattedTitle();
311
        }
312
313
        return $this->exercise;
314
    }
315
316
    /**
317
     * returns the number of attempts setted.
318
     *
319
     * @return int - exercise attempts
320
     */
321
    public function selectAttempts()
322
    {
323
        return $this->attempts;
324
    }
325
326
    /**
327
     * Returns the number of FeedbackType
328
     *  0: Feedback , 1: DirectFeedback, 2: NoFeedback.
329
     *
330
     * @return int - exercise attempts
331
     */
332
    public function getFeedbackType()
333
    {
334
        return (int) $this->feedback_type;
335
    }
336
337
    /**
338
     * returns the time limit.
339
     *
340
     * @return int
341
     */
342
    public function selectTimeLimit()
343
    {
344
        return $this->timeLimit;
345
    }
346
347
    /**
348
     * returns the exercise description.
349
     *
350
     * @author Olivier Brouckaert
351
     *
352
     * @return string - exercise description
353
     */
354
    public function selectDescription()
355
    {
356
        return $this->description;
357
    }
358
359
    /**
360
     * returns the exercise sound file.
361
     *
362
     * @author Olivier Brouckaert
363
     *
364
     * @return string - exercise description
365
     */
366
    public function selectSound()
367
    {
368
        return $this->sound;
369
    }
370
371
    /**
372
     * returns the exercise type.
373
     *
374
     * @author Olivier Brouckaert
375
     *
376
     * @return int - exercise type
377
     */
378
    public function selectType()
379
    {
380
        return $this->type;
381
    }
382
383
    /**
384
     * @return int
385
     */
386
    public function getModelType()
387
    {
388
        return $this->modelType;
389
    }
390
391
    /**
392
     * @return int
393
     */
394
    public function selectEndButton()
395
    {
396
        return $this->endButton;
397
    }
398
399
    /**
400
     * @author hubert borderiou 30-11-11
401
     *
402
     * @return int : do we display the question category name for students
403
     */
404
    public function selectDisplayCategoryName()
405
    {
406
        return $this->display_category_name;
407
    }
408
409
    /**
410
     * @return int
411
     */
412
    public function selectPassPercentage()
413
    {
414
        return $this->pass_percentage;
415
    }
416
417
    /**
418
     * Modify object to update the switch display_category_name.
419
     *
420
     * @author hubert borderiou 30-11-11
421
     *
422
     * @param int $value is an integer 0 or 1
423
     */
424
    public function updateDisplayCategoryName($value)
425
    {
426
        $this->display_category_name = $value;
427
    }
428
429
    /**
430
     * @author hubert borderiou 28-11-11
431
     *
432
     * @return string html text : the text to display ay the end of the test
433
     */
434
    public function getTextWhenFinished()
435
    {
436
        return $this->text_when_finished;
437
    }
438
439
    /**
440
     * @param string $text
441
     *
442
     * @author hubert borderiou 28-11-11
443
     */
444
    public function updateTextWhenFinished($text)
445
    {
446
        $this->text_when_finished = $text;
447
    }
448
449
    /**
450
     * return 1 or 2 if randomByCat.
451
     *
452
     * @author hubert borderiou
453
     *
454
     * @return int - quiz random by category
455
     */
456
    public function getRandomByCategory()
457
    {
458
        return $this->randomByCat;
459
    }
460
461
    /**
462
     * return 0 if no random by cat
463
     * return 1 if random by cat, categories shuffled
464
     * return 2 if random by cat, categories sorted by alphabetic order.
465
     *
466
     * @author hubert borderiou
467
     *
468
     * @return int - quiz random by category
469
     */
470
    public function isRandomByCat()
471
    {
472
        $res = EXERCISE_CATEGORY_RANDOM_DISABLED;
473
        if (EXERCISE_CATEGORY_RANDOM_SHUFFLED == $this->randomByCat) {
474
            $res = EXERCISE_CATEGORY_RANDOM_SHUFFLED;
475
        } elseif (EXERCISE_CATEGORY_RANDOM_ORDERED == $this->randomByCat) {
476
            $res = EXERCISE_CATEGORY_RANDOM_ORDERED;
477
        }
478
479
        return $res;
480
    }
481
482
    /**
483
     * return nothing
484
     * update randomByCat value for object.
485
     *
486
     * @param int $random
487
     *
488
     * @author hubert borderiou
489
     */
490
    public function updateRandomByCat($random)
491
    {
492
        $this->randomByCat = EXERCISE_CATEGORY_RANDOM_DISABLED;
493
        if (in_array(
494
            $random,
495
            [
496
                EXERCISE_CATEGORY_RANDOM_SHUFFLED,
497
                EXERCISE_CATEGORY_RANDOM_ORDERED,
498
                EXERCISE_CATEGORY_RANDOM_DISABLED,
499
            ]
500
        )) {
501
            $this->randomByCat = $random;
502
        }
503
    }
504
505
    /**
506
     * Tells if questions are selected randomly, and if so returns the draws.
507
     *
508
     * @author Carlos Vargas
509
     *
510
     * @return int - results disabled exercise
511
     */
512
    public function selectResultsDisabled()
513
    {
514
        return $this->results_disabled;
515
    }
516
517
    /**
518
     * tells if questions are selected randomly, and if so returns the draws.
519
     *
520
     * @author Olivier Brouckaert
521
     *
522
     * @return bool
523
     */
524
    public function isRandom()
525
    {
526
        $isRandom = false;
527
        // "-1" means all questions will be random
528
        if ($this->random > 0 || -1 == $this->random) {
529
            $isRandom = true;
530
        }
531
532
        return $isRandom;
533
    }
534
535
    /**
536
     * returns random answers status.
537
     *
538
     * @author Juan Carlos Rana
539
     */
540
    public function getRandomAnswers()
541
    {
542
        return $this->random_answers;
543
    }
544
545
    /**
546
     * Same as isRandom() but has a name applied to values different than 0 or 1.
547
     *
548
     * @return int
549
     */
550
    public function getShuffle()
551
    {
552
        return $this->random;
553
    }
554
555
    /**
556
     * returns the exercise status (1 = enabled ; 0 = disabled).
557
     *
558
     * @author Olivier Brouckaert
559
     *
560
     * @return int - 1 if enabled, otherwise 0
561
     */
562
    public function selectStatus()
563
    {
564
        return $this->active;
565
    }
566
567
    /**
568
     * If false the question list will be managed as always if true
569
     * the question will be filtered
570
     * depending of the exercise settings (table c_quiz_rel_category).
571
     *
572
     * @param bool $status active or inactive grouping
573
     */
574
    public function setCategoriesGrouping($status)
575
    {
576
        $this->categories_grouping = (bool) $status;
577
    }
578
579
    /**
580
     * @return int
581
     */
582
    public function getHideQuestionTitle()
583
    {
584
        return $this->hideQuestionTitle;
585
    }
586
587
    /**
588
     * @param $value
589
     */
590
    public function setHideQuestionTitle($value)
591
    {
592
        $this->hideQuestionTitle = (int) $value;
593
    }
594
595
    /**
596
     * @return int
597
     */
598
    public function getScoreTypeModel()
599
    {
600
        return $this->scoreTypeModel;
601
    }
602
603
    /**
604
     * @param int $value
605
     */
606
    public function setScoreTypeModel($value)
607
    {
608
        $this->scoreTypeModel = (int) $value;
609
    }
610
611
    /**
612
     * @return int
613
     */
614
    public function getGlobalCategoryId()
615
    {
616
        return $this->globalCategoryId;
617
    }
618
619
    /**
620
     * @param int $value
621
     */
622
    public function setGlobalCategoryId($value)
623
    {
624
        if (is_array($value) && isset($value[0])) {
625
            $value = $value[0];
626
        }
627
        $this->globalCategoryId = (int) $value;
628
    }
629
630
    /**
631
     * @param int    $start
632
     * @param int    $limit
633
     * @param int    $sidx
634
     * @param string $sord
635
     * @param array  $whereCondition
636
     * @param array  $extraFields
637
     *
638
     * @return array
639
     */
640
    public function getQuestionListPagination(
641
        $start,
642
        $limit,
643
        $sidx,
644
        $sord,
645
        $whereCondition = [],
646
        $extraFields = []
647
    ) {
648
        if (!empty($this->getId())) {
649
            $category_list = TestCategory::getListOfCategoriesNameForTest(
650
                $this->getId(),
651
                false
652
            );
653
            $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
654
            $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
655
656
            $sql = "SELECT q.iid
657
                    FROM $TBL_EXERCICE_QUESTION e
658
                    INNER JOIN $TBL_QUESTIONS  q
659
                    ON (e.question_id = q.iid AND e.c_id = ".$this->course_id." )
660
					WHERE e.exercice_id	= '".$this->getId()."' ";
661
662
            $orderCondition = ' ORDER BY question_order ';
663
664
            if (!empty($sidx) && !empty($sord)) {
665
                if ('question' === $sidx) {
666
                    if (in_array(strtolower($sord), ['desc', 'asc'])) {
667
                        $orderCondition = " ORDER BY q.$sidx $sord";
668
                    }
669
                }
670
            }
671
672
            $sql .= $orderCondition;
673
            $limitCondition = null;
674
            if (isset($start) && isset($limit)) {
675
                $start = (int) $start;
676
                $limit = (int) $limit;
677
                $limitCondition = " LIMIT $start, $limit";
678
            }
679
            $sql .= $limitCondition;
680
            $result = Database::query($sql);
681
            $questions = [];
682
            if (Database::num_rows($result)) {
683
                if (!empty($extraFields)) {
684
                    $extraFieldValue = new ExtraFieldValue('question');
685
                }
686
                while ($question = Database::fetch_array($result, 'ASSOC')) {
687
                    /** @var Question $objQuestionTmp */
688
                    $objQuestionTmp = Question::read($question['iid']);
689
                    $category_labels = TestCategory::return_category_labels(
690
                        $objQuestionTmp->category_list,
691
                        $category_list
692
                    );
693
694
                    if (empty($category_labels)) {
695
                        $category_labels = '-';
696
                    }
697
698
                    // Question type
699
                    $typeImg = $objQuestionTmp->getTypePicture();
700
                    $typeExpl = $objQuestionTmp->getExplanation();
701
702
                    $question_media = null;
703
                    if (!empty($objQuestionTmp->parent_id)) {
704
                        $objQuestionMedia = Question::read($objQuestionTmp->parent_id);
705
                        $question_media = Question::getMediaLabel($objQuestionMedia->question);
706
                    }
707
708
                    $questionType = Display::tag(
709
                        'div',
710
                        Display::return_icon($typeImg, $typeExpl, [], ICON_SIZE_MEDIUM).$question_media
711
                    );
712
713
                    $question = [
714
                        'id' => $question['iid'],
715
                        'question' => $objQuestionTmp->selectTitle(),
716
                        'type' => $questionType,
717
                        'category' => Display::tag(
718
                            'div',
719
                            '<a href="#" style="padding:0px; margin:0px;">'.$category_labels.'</a>'
720
                        ),
721
                        'score' => $objQuestionTmp->selectWeighting(),
722
                        'level' => $objQuestionTmp->level,
723
                    ];
724
725
                    if (!empty($extraFields)) {
726
                        foreach ($extraFields as $extraField) {
727
                            $value = $extraFieldValue->get_values_by_handler_and_field_id(
728
                                $question['id'],
729
                                $extraField['id']
730
                            );
731
                            $stringValue = null;
732
                            if ($value) {
733
                                $stringValue = $value['field_value'];
734
                            }
735
                            $question[$extraField['field_variable']] = $stringValue;
736
                        }
737
                    }
738
                    $questions[] = $question;
739
                }
740
            }
741
742
            return $questions;
743
        }
744
    }
745
746
    /**
747
     * Get question count per exercise from DB (any special treatment).
748
     *
749
     * @return int
750
     */
751
    public function getQuestionCount()
752
    {
753
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
754
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
755
        $sql = "SELECT count(q.iid) as count
756
                FROM $TBL_EXERCICE_QUESTION e
757
                INNER JOIN $TBL_QUESTIONS q
758
                ON (e.question_id = q.iid AND e.c_id = q.c_id)
759
                WHERE
760
                    e.c_id = {$this->course_id} AND
761
                    e.exercice_id = ".$this->getId();
762
        $result = Database::query($sql);
763
764
        $count = 0;
765
        if (Database::num_rows($result)) {
766
            $row = Database::fetch_array($result);
767
            $count = (int) $row['count'];
768
        }
769
770
        return $count;
771
    }
772
773
    /**
774
     * @return array
775
     */
776
    public function getQuestionOrderedListByName()
777
    {
778
        if (empty($this->course_id) || empty($this->getId())) {
779
            return [];
780
        }
781
782
        $exerciseQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
783
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
784
785
        // Getting question list from the order (question list drag n drop interface ).
786
        $sql = "SELECT e.question_id
787
                FROM $exerciseQuestionTable e
788
                INNER JOIN $questionTable q
789
                ON (e.question_id= q.iid AND e.c_id = q.c_id)
790
                WHERE
791
                    e.c_id = {$this->course_id} AND
792
                    e.exercice_id = '".$this->getId()."'
793
                ORDER BY q.question";
794
        $result = Database::query($sql);
795
        $list = [];
796
        if (Database::num_rows($result)) {
797
            $list = Database::store_result($result, 'ASSOC');
798
        }
799
800
        return $list;
801
    }
802
803
    /**
804
     * Selecting question list depending in the exercise-category
805
     * relationship (category table in exercise settings).
806
     *
807
     * @param array $question_list
808
     * @param int   $questionSelectionType
809
     *
810
     * @return array
811
     */
812
    public function getQuestionListWithCategoryListFilteredByCategorySettings(
813
        $question_list,
814
        $questionSelectionType
815
    ) {
816
        $result = [
817
            'question_list' => [],
818
            'category_with_questions_list' => [],
819
        ];
820
821
        // Order/random categories
822
        $cat = new TestCategory();
823
824
        // Setting category order.
825
        switch ($questionSelectionType) {
826
            case EX_Q_SELECTION_ORDERED: // 1
827
            case EX_Q_SELECTION_RANDOM:  // 2
828
                // This options are not allowed here.
829
                break;
830
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED: // 3
831
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
832
                    $this,
833
                    $this->course['real_id'],
834
                    'title ASC',
835
                    false,
836
                    true
837
                );
838
839
                $questions_by_category = TestCategory::getQuestionsByCat(
840
                    $this->getId(),
841
                    $question_list,
842
                    $categoriesAddedInExercise
843
                );
844
845
                $question_list = $this->pickQuestionsPerCategory(
846
                    $categoriesAddedInExercise,
847
                    $question_list,
848
                    $questions_by_category,
849
                    true,
850
                    false
851
                );
852
853
                break;
854
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED: // 4
855
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7
856
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
857
                    $this,
858
                    $this->course['real_id'],
859
                    null,
860
                    true,
861
                    true
862
                );
863
                $questions_by_category = TestCategory::getQuestionsByCat(
864
                    $this->getId(),
865
                    $question_list,
866
                    $categoriesAddedInExercise
867
                );
868
                $question_list = $this->pickQuestionsPerCategory(
869
                    $categoriesAddedInExercise,
870
                    $question_list,
871
                    $questions_by_category,
872
                    true,
873
                    false
874
                );
875
876
                break;
877
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM: // 5
878
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
879
                    $this,
880
                    $this->course['real_id'],
881
                    'title ASC',
882
                    false,
883
                    true
884
                );
885
                $questions_by_category = TestCategory::getQuestionsByCat(
886
                    $this->getId(),
887
                    $question_list,
888
                    $categoriesAddedInExercise
889
                );
890
                $question_list = $this->pickQuestionsPerCategory(
891
                    $categoriesAddedInExercise,
892
                    $question_list,
893
                    $questions_by_category,
894
                    true,
895
                    true
896
                );
897
898
                break;
899
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM: // 6
900
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED:
901
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
902
                    $this,
903
                    $this->course['real_id'],
904
                    null,
905
                    true,
906
                    true
907
                );
908
909
                $questions_by_category = TestCategory::getQuestionsByCat(
910
                    $this->getId(),
911
                    $question_list,
912
                    $categoriesAddedInExercise
913
                );
914
915
                $question_list = $this->pickQuestionsPerCategory(
916
                    $categoriesAddedInExercise,
917
                    $question_list,
918
                    $questions_by_category,
919
                    true,
920
                    true
921
                );
922
923
                break;
924
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7
925
                break;
926
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED: // 8
927
                break;
928
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED: // 9
929
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
930
                    $this,
931
                    $this->course['real_id'],
932
                    'root ASC, lft ASC',
933
                    false,
934
                    true
935
                );
936
                $questions_by_category = TestCategory::getQuestionsByCat(
937
                    $this->getId(),
938
                    $question_list,
939
                    $categoriesAddedInExercise
940
                );
941
                $question_list = $this->pickQuestionsPerCategory(
942
                    $categoriesAddedInExercise,
943
                    $question_list,
944
                    $questions_by_category,
945
                    true,
946
                    false
947
                );
948
949
                break;
950
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM: // 10
951
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
952
                    $this,
953
                    $this->course['real_id'],
954
                    'root, lft ASC',
955
                    false,
956
                    true
957
                );
958
                $questions_by_category = TestCategory::getQuestionsByCat(
959
                    $this->getId(),
960
                    $question_list,
961
                    $categoriesAddedInExercise
962
                );
963
                $question_list = $this->pickQuestionsPerCategory(
964
                    $categoriesAddedInExercise,
965
                    $question_list,
966
                    $questions_by_category,
967
                    true,
968
                    true
969
                );
970
971
                break;
972
        }
973
974
        $result['question_list'] = isset($question_list) ? $question_list : [];
975
        $result['category_with_questions_list'] = isset($questions_by_category) ? $questions_by_category : [];
976
        $parentsLoaded = [];
977
        // Adding category info in the category list with question list:
978
        if (!empty($questions_by_category)) {
979
            $newCategoryList = [];
980
            $em = Database::getManager();
981
            $repo = $em->getRepository('ChamiloCourseBundle:CQuizCategory');
982
983
            foreach ($questions_by_category as $categoryId => $questionList) {
984
                $cat = new TestCategory();
985
                $cat = $cat->getCategory($categoryId);
986
                if ($cat) {
987
                    $cat = (array) $cat;
988
                    $cat['iid'] = $cat['id'];
989
                }
990
991
                $categoryParentInfo = null;
992
                // Parent is not set no loop here
993
                if (isset($cat['parent_id']) && !empty($cat['parent_id'])) {
994
                    /** @var CQuizCategory $categoryEntity */
995
                    if (!isset($parentsLoaded[$cat['parent_id']])) {
996
                        $categoryEntity = $em->find('ChamiloCourseBundle:CQuizCategory', $cat['parent_id']);
997
                        $parentsLoaded[$cat['parent_id']] = $categoryEntity;
998
                    } else {
999
                        $categoryEntity = $parentsLoaded[$cat['parent_id']];
1000
                    }
1001
                    $path = $repo->getPath($categoryEntity);
1002
1003
                    $index = 0;
1004
                    if ($this->categoryMinusOne) {
1005
                        //$index = 1;
1006
                    }
1007
1008
                    /** @var CQuizCategory $categoryParent */
1009
                    foreach ($path as $categoryParent) {
1010
                        $visibility = $categoryParent->getVisibility();
1011
                        if (0 == $visibility) {
1012
                            $categoryParentId = $categoryId;
1013
                            $categoryTitle = $cat['title'];
1014
                            if (count($path) > 1) {
1015
                                continue;
1016
                            }
1017
                        } else {
1018
                            $categoryParentId = $categoryParent->getIid();
1019
                            $categoryTitle = $categoryParent->getTitle();
1020
                        }
1021
1022
                        $categoryParentInfo['id'] = $categoryParentId;
1023
                        $categoryParentInfo['iid'] = $categoryParentId;
1024
                        $categoryParentInfo['parent_path'] = null;
1025
                        $categoryParentInfo['title'] = $categoryTitle;
1026
                        $categoryParentInfo['name'] = $categoryTitle;
1027
                        $categoryParentInfo['parent_id'] = null;
1028
1029
                        break;
1030
                    }
1031
                }
1032
                $cat['parent_info'] = $categoryParentInfo;
1033
                $newCategoryList[$categoryId] = [
1034
                    'category' => $cat,
1035
                    'question_list' => $questionList,
1036
                ];
1037
            }
1038
1039
            $result['category_with_questions_list'] = $newCategoryList;
1040
        }
1041
1042
        return $result;
1043
    }
1044
1045
    /**
1046
     * returns the array with the question ID list.
1047
     *
1048
     * @param bool $fromDatabase Whether the results should be fetched in the database or just from memory
1049
     * @param bool $adminView    Whether we should return all questions (admin view) or
1050
     *                           just a list limited by the max number of random questions
1051
     *
1052
     * @author Olivier Brouckaert
1053
     *
1054
     * @return array - question ID list
1055
     */
1056
    public function selectQuestionList($fromDatabase = false, $adminView = false)
1057
    {
1058
        if ($fromDatabase && !empty($this->getId())) {
1059
            $nbQuestions = $this->getQuestionCount();
1060
            $questionSelectionType = $this->getQuestionSelectionType();
1061
1062
            switch ($questionSelectionType) {
1063
                case EX_Q_SELECTION_ORDERED:
1064
                    $questionList = $this->getQuestionOrderedList($adminView);
1065
1066
                    break;
1067
                case EX_Q_SELECTION_RANDOM:
1068
                    // Not a random exercise, or if there are not at least 2 questions
1069
                    if (0 == $this->random || $nbQuestions < 2) {
1070
                        $questionList = $this->getQuestionOrderedList($adminView);
1071
                    } else {
1072
                        $questionList = $this->getRandomList($adminView);
1073
                    }
1074
1075
                    break;
1076
                default:
1077
                    $questionList = $this->getQuestionOrderedList($adminView);
1078
                    $result = $this->getQuestionListWithCategoryListFilteredByCategorySettings(
1079
                        $questionList,
1080
                        $questionSelectionType
1081
                    );
1082
                    $this->categoryWithQuestionList = $result['category_with_questions_list'];
1083
                    $questionList = $result['question_list'];
1084
1085
                    break;
1086
            }
1087
1088
            return $questionList;
1089
        }
1090
1091
        return $this->questionList;
1092
    }
1093
1094
    /**
1095
     * returns the number of questions in this exercise.
1096
     *
1097
     * @author Olivier Brouckaert
1098
     *
1099
     * @return int - number of questions
1100
     */
1101
    public function selectNbrQuestions()
1102
    {
1103
        return count($this->questionList);
1104
    }
1105
1106
    /**
1107
     * @return int
1108
     */
1109
    public function selectPropagateNeg()
1110
    {
1111
        return $this->propagate_neg;
1112
    }
1113
1114
    /**
1115
     * @return int
1116
     */
1117
    public function getSaveCorrectAnswers()
1118
    {
1119
        return $this->saveCorrectAnswers;
1120
    }
1121
1122
    /**
1123
     * Selects questions randomly in the question list.
1124
     *
1125
     * @author Olivier Brouckaert
1126
     * @author Hubert Borderiou 15 nov 2011
1127
     *
1128
     * @param bool $adminView Whether we should return all
1129
     *                        questions (admin view) or just a list limited by the max number of random questions
1130
     *
1131
     * @return array - if the exercise is not set to take questions randomly, returns the question list
1132
     *               without randomizing, otherwise, returns the list with questions selected randomly
1133
     */
1134
    public function getRandomList($adminView = false)
1135
    {
1136
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1137
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
1138
        $random = isset($this->random) && !empty($this->random) ? $this->random : 0;
1139
1140
        // Random with limit
1141
        $randomLimit = " ORDER BY RAND() LIMIT $random";
1142
1143
        // Random with no limit
1144
        if (-1 == $random) {
1145
            $randomLimit = ' ORDER BY RAND() ';
1146
        }
1147
1148
        // Admin see the list in default order
1149
        if (true === $adminView) {
1150
            // If viewing it as admin for edition, don't show it randomly, use title + id
1151
            $randomLimit = 'ORDER BY e.question_order';
1152
        }
1153
1154
        $sql = "SELECT e.question_id
1155
                FROM $quizRelQuestion e
1156
                INNER JOIN $question q
1157
                ON (e.question_id= q.iid AND e.c_id = q.c_id)
1158
                WHERE
1159
                    e.c_id = {$this->course_id} AND
1160
                    e.exercice_id = '".$this->getId()."'
1161
                    $randomLimit ";
1162
        $result = Database::query($sql);
1163
        $questionList = [];
1164
        while ($row = Database::fetch_object($result)) {
1165
            $questionList[] = $row->question_id;
1166
        }
1167
1168
        return $questionList;
1169
    }
1170
1171
    /**
1172
     * returns 'true' if the question ID is in the question list.
1173
     *
1174
     * @author Olivier Brouckaert
1175
     *
1176
     * @param int $questionId - question ID
1177
     *
1178
     * @return bool - true if in the list, otherwise false
1179
     */
1180
    public function isInList($questionId)
1181
    {
1182
        $inList = false;
1183
        if (is_array($this->questionList)) {
1184
            $inList = in_array($questionId, $this->questionList);
1185
        }
1186
1187
        return $inList;
1188
    }
1189
1190
    /**
1191
     * If current exercise has a question.
1192
     *
1193
     * @param int $questionId
1194
     *
1195
     * @return int
1196
     */
1197
    public function hasQuestion($questionId)
1198
    {
1199
        $questionId = (int) $questionId;
1200
1201
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1202
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1203
        $sql = "SELECT q.iid
1204
                FROM $TBL_EXERCICE_QUESTION e
1205
                INNER JOIN $TBL_QUESTIONS q
1206
                ON (e.question_id = q.iid AND e.c_id = q.c_id)
1207
                WHERE
1208
                    q.iid = $questionId AND
1209
                    e.c_id = {$this->course_id} AND
1210
                    e.exercice_id = ".$this->getId();
1211
1212
        $result = Database::query($sql);
1213
1214
        return Database::num_rows($result) > 0;
1215
    }
1216
1217
    public function hasQuestionWithTypeNotInList(array $questionTypeList)
1218
    {
1219
        if (empty($questionTypeList)) {
1220
            return false;
1221
        }
1222
1223
        $questionTypeToString = implode("','", array_map('intval', $questionTypeList));
1224
1225
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1226
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
1227
        $sql = "SELECT q.iid
1228
                FROM $table e
1229
                INNER JOIN $tableQuestion q
1230
                ON (e.question_id = q.iid AND e.c_id = q.c_id)
1231
                WHERE
1232
                    q.type NOT IN ('$questionTypeToString')  AND
1233
                    e.c_id = {$this->course_id} AND
1234
                    e.exercice_id = ".$this->getId();
1235
1236
        $result = Database::query($sql);
1237
1238
        return Database::num_rows($result) > 0;
1239
    }
1240
1241
    /**
1242
     * changes the exercise title.
1243
     *
1244
     * @author Olivier Brouckaert
1245
     *
1246
     * @param string $title - exercise title
1247
     */
1248
    public function updateTitle($title)
1249
    {
1250
        $this->title = $this->exercise = $title;
1251
    }
1252
1253
    /**
1254
     * changes the exercise max attempts.
1255
     *
1256
     * @param int $attempts - exercise max attempts
1257
     */
1258
    public function updateAttempts($attempts)
1259
    {
1260
        $this->attempts = $attempts;
1261
    }
1262
1263
    /**
1264
     * changes the exercise feedback type.
1265
     *
1266
     * @param int $feedback_type
1267
     */
1268
    public function updateFeedbackType($feedback_type)
1269
    {
1270
        $this->feedback_type = $feedback_type;
1271
    }
1272
1273
    /**
1274
     * changes the exercise description.
1275
     *
1276
     * @author Olivier Brouckaert
1277
     *
1278
     * @param string $description - exercise description
1279
     */
1280
    public function updateDescription($description)
1281
    {
1282
        $this->description = $description;
1283
    }
1284
1285
    /**
1286
     * changes the exercise expired_time.
1287
     *
1288
     * @author Isaac flores
1289
     *
1290
     * @param int $expired_time The expired time of the quiz
1291
     */
1292
    public function updateExpiredTime($expired_time)
1293
    {
1294
        $this->expired_time = $expired_time;
1295
    }
1296
1297
    /**
1298
     * @param $value
1299
     */
1300
    public function updatePropagateNegative($value)
1301
    {
1302
        $this->propagate_neg = $value;
1303
    }
1304
1305
    /**
1306
     * @param int $value
1307
     */
1308
    public function updateSaveCorrectAnswers($value)
1309
    {
1310
        $this->saveCorrectAnswers = (int) $value;
1311
    }
1312
1313
    /**
1314
     * @param $value
1315
     */
1316
    public function updateReviewAnswers($value)
1317
    {
1318
        $this->review_answers = isset($value) && $value ? true : false;
1319
    }
1320
1321
    /**
1322
     * @param $value
1323
     */
1324
    public function updatePassPercentage($value)
1325
    {
1326
        $this->pass_percentage = $value;
1327
    }
1328
1329
    /**
1330
     * @param string $text
1331
     */
1332
    public function updateEmailNotificationTemplate($text)
1333
    {
1334
        $this->emailNotificationTemplate = $text;
1335
    }
1336
1337
    /**
1338
     * @param string $text
1339
     */
1340
    public function setEmailNotificationTemplateToUser($text)
1341
    {
1342
        $this->emailNotificationTemplateToUser = $text;
1343
    }
1344
1345
    /**
1346
     * @param string $value
1347
     */
1348
    public function setNotifyUserByEmail($value)
1349
    {
1350
        $this->notifyUserByEmail = $value;
1351
    }
1352
1353
    /**
1354
     * @param int $value
1355
     */
1356
    public function updateEndButton($value)
1357
    {
1358
        $this->endButton = (int) $value;
1359
    }
1360
1361
    /**
1362
     * @param string $value
1363
     */
1364
    public function setOnSuccessMessage($value)
1365
    {
1366
        $this->onSuccessMessage = $value;
1367
    }
1368
1369
    /**
1370
     * @param string $value
1371
     */
1372
    public function setOnFailedMessage($value)
1373
    {
1374
        $this->onFailedMessage = $value;
1375
    }
1376
1377
    /**
1378
     * @param $value
1379
     */
1380
    public function setModelType($value)
1381
    {
1382
        $this->modelType = (int) $value;
1383
    }
1384
1385
    /**
1386
     * @param int $value
1387
     */
1388
    public function setQuestionSelectionType($value)
1389
    {
1390
        $this->questionSelectionType = (int) $value;
1391
    }
1392
1393
    /**
1394
     * @return int
1395
     */
1396
    public function getQuestionSelectionType()
1397
    {
1398
        return (int) $this->questionSelectionType;
1399
    }
1400
1401
    /**
1402
     * @param array $categories
1403
     */
1404
    public function updateCategories($categories)
1405
    {
1406
        if (!empty($categories)) {
1407
            $categories = array_map('intval', $categories);
1408
            $this->categories = $categories;
1409
        }
1410
    }
1411
1412
    /**
1413
     * changes the exercise sound file.
1414
     *
1415
     * @author Olivier Brouckaert
1416
     *
1417
     * @param string $sound  - exercise sound file
1418
     * @param string $delete - ask to delete the file
1419
     */
1420
    public function updateSound($sound, $delete)
1421
    {
1422
        global $audioPath, $documentPath;
1423
        $TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
1424
1425
        if ($sound['size'] &&
1426
            (strstr($sound['type'], 'audio') || strstr($sound['type'], 'video'))
1427
        ) {
1428
            $this->sound = $sound['name'];
1429
1430
            if (@move_uploaded_file($sound['tmp_name'], $audioPath.'/'.$this->sound)) {
1431
                $sql = "SELECT 1 FROM $TBL_DOCUMENT
1432
                        WHERE
1433
                            c_id = ".$this->course_id." AND
1434
                            path = '".str_replace($documentPath, '', $audioPath).'/'.$this->sound."'";
1435
                $result = Database::query($sql);
1436
1437
                if (!Database::num_rows($result)) {
1438
                    DocumentManager::addDocument(
1439
                        $this->course,
1440
                        str_replace($documentPath, '', $audioPath).'/'.$this->sound,
1441
                        'file',
1442
                        $sound['size'],
1443
                        $sound['name']
1444
                    );
1445
                }
1446
            }
1447
        } elseif ($delete && is_file($audioPath.'/'.$this->sound)) {
1448
            $this->sound = '';
1449
        }
1450
    }
1451
1452
    /**
1453
     * changes the exercise type.
1454
     *
1455
     * @author Olivier Brouckaert
1456
     *
1457
     * @param int $type - exercise type
1458
     */
1459
    public function updateType($type)
1460
    {
1461
        $this->type = $type;
1462
    }
1463
1464
    /**
1465
     * sets to 0 if questions are not selected randomly
1466
     * if questions are selected randomly, sets the draws.
1467
     *
1468
     * @author Olivier Brouckaert
1469
     *
1470
     * @param int $random - 0 if not random, otherwise the draws
1471
     */
1472
    public function setRandom($random)
1473
    {
1474
        $this->random = $random;
1475
    }
1476
1477
    /**
1478
     * sets to 0 if answers are not selected randomly
1479
     * if answers are selected randomly.
1480
     *
1481
     * @author Juan Carlos Rana
1482
     *
1483
     * @param int $random_answers - random answers
1484
     */
1485
    public function updateRandomAnswers($random_answers)
1486
    {
1487
        $this->random_answers = $random_answers;
1488
    }
1489
1490
    /**
1491
     * enables the exercise.
1492
     *
1493
     * @author Olivier Brouckaert
1494
     */
1495
    public function enable()
1496
    {
1497
        $this->active = 1;
1498
    }
1499
1500
    /**
1501
     * disables the exercise.
1502
     *
1503
     * @author Olivier Brouckaert
1504
     */
1505
    public function disable()
1506
    {
1507
        $this->active = 0;
1508
    }
1509
1510
    /**
1511
     * Set disable results.
1512
     */
1513
    public function disable_results()
1514
    {
1515
        $this->results_disabled = true;
1516
    }
1517
1518
    /**
1519
     * Enable results.
1520
     */
1521
    public function enable_results()
1522
    {
1523
        $this->results_disabled = false;
1524
    }
1525
1526
    /**
1527
     * @param int $results_disabled
1528
     */
1529
    public function updateResultsDisabled($results_disabled)
1530
    {
1531
        $this->results_disabled = (int) $results_disabled;
1532
    }
1533
1534
    /**
1535
     * updates the exercise in the data base.
1536
     *
1537
     * @author Olivier Brouckaert
1538
     */
1539
    public function save()
1540
    {
1541
        $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
1542
1543
        $id = $this->getId();
1544
        $title = $this->exercise;
1545
        $description = $this->description;
1546
        $sound = $this->sound;
1547
        $type = $this->type;
1548
        $attempts = isset($this->attempts) ? $this->attempts : 0;
1549
        $feedback_type = isset($this->feedback_type) ? $this->feedback_type : 0;
1550
        $random = $this->random;
1551
        $random_answers = $this->random_answers;
1552
        $active = $this->active;
1553
        $propagate_neg = (int) $this->propagate_neg;
1554
        $saveCorrectAnswers = isset($this->saveCorrectAnswers) ? (int) $this->saveCorrectAnswers : 0;
1555
        $review_answers = isset($this->review_answers) && $this->review_answers ? 1 : 0;
1556
        $randomByCat = (int) $this->randomByCat;
1557
        $text_when_finished = $this->text_when_finished;
1558
        $display_category_name = (int) $this->display_category_name;
1559
        $pass_percentage = (int) $this->pass_percentage;
1560
        $session_id = $this->sessionId;
1561
1562
        // If direct we do not show results
1563
        $results_disabled = (int) $this->results_disabled;
1564
        if (in_array($feedback_type, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1565
            $results_disabled = 0;
1566
        }
1567
        $expired_time = (int) $this->expired_time;
1568
1569
        $em = Database::getManager();
1570
        $repo = Container::getExerciseRepository();
1571
        $repoCategory = Container::getExerciseCategoryRepository();
1572
1573
        // we prepare date in the database using the api_get_utc_datetime() function
1574
        $start_time = null;
1575
        if (!empty($this->start_time)) {
1576
            $start_time = $this->start_time;
1577
        }
1578
1579
        $end_time = null;
1580
        if (!empty($this->end_time)) {
1581
            $end_time = $this->end_time;
1582
        }
1583
1584
        // Exercise already exists
1585
        if ($id) {
1586
            /** @var CQuiz $exercise */
1587
            $exercise = $repo->find($id);
1588
        } else {
1589
            $exercise = new CQuiz();
1590
        }
1591
1592
        $exercise
1593
            ->setStartTime($start_time)
1594
            ->setEndTime($end_time)
1595
            ->setTitle($title)
1596
            ->setDescription($description)
1597
            ->setSound($sound)
1598
            ->setType($type)
1599
            ->setRandom($random)
1600
            ->setRandomAnswers($random_answers)
1601
            ->setActive($active)
1602
            ->setResultsDisabled($results_disabled)
1603
            ->setMaxAttempt($attempts)
1604
            ->setFeedbackType($feedback_type)
1605
            ->setExpiredTime($expired_time)
1606
            ->setReviewAnswers($review_answers)
1607
            ->setRandomByCategory($randomByCat)
1608
            ->setTextWhenFinished($text_when_finished)
1609
            ->setDisplayCategoryName($display_category_name)
1610
            ->setPassPercentage($pass_percentage)
1611
            ->setSaveCorrectAnswers($saveCorrectAnswers)
1612
            ->setPropagateNeg($propagate_neg)
1613
            ->setHideQuestionTitle($this->getHideQuestionTitle())
1614
            ->setQuestionSelectionType($this->getQuestionSelectionType())
1615
        ;
1616
1617
        $allow = api_get_configuration_value('allow_exercise_categories');
1618
        if (true === $allow) {
1619
            if (!empty($this->getExerciseCategoryId())) {
1620
                $exercise->setExerciseCategory($repoCategory->find($this->getExerciseCategoryId()));
1621
            }
1622
        }
1623
1624
        if (api_get_configuration_value('quiz_prevent_backwards_move')) {
1625
            $exercise->setPreventBackwards($this->getPreventBackwards());
1626
        }
1627
1628
        $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
1629
        if (true === $allow) {
1630
            $exercise->setShowPreviousButton($this->showPreviousButton());
1631
        }
1632
1633
        $allow = api_get_configuration_value('allow_notification_setting_per_exercise');
1634
        if (true === $allow) {
1635
            $notifications = $this->getNotifications();
1636
            if (!empty($notifications)) {
1637
                $notifications = implode(',', $notifications);
1638
                $exercise->setNotifications($notifications);
1639
            }
1640
        }
1641
1642
        if (!empty($this->pageResultConfiguration)) {
1643
            $exercise->setPageResultConfiguration($this->pageResultConfiguration);
1644
        }
1645
1646
        if ($id) {
1647
            $repo->updateNodeForResource($exercise);
1648
1649
            if ('true' === api_get_setting('search_enabled')) {
1650
                $this->search_engine_edit();
1651
            }
1652
            $em->persist($exercise);
1653
            $em->flush();
1654
        } else {
1655
            // Creates a new exercise
1656
            $courseEntity = api_get_course_entity($this->course_id);
1657
            $exercise
1658
                ->setSessionId(api_get_session_id())
1659
                ->setCId($courseEntity->getId())
1660
                ->setParent($courseEntity)
1661
                ->addCourseLink($courseEntity, api_get_session_entity())
1662
            ;
1663
            $em->persist($exercise);
1664
            $em->flush();
1665
            $id = $exercise->getIid();
1666
            if ($id) {
1667
                if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
1668
                    $this->search_engine_save();
1669
                }
1670
            }
1671
        }
1672
1673
        $this->save_categories_in_exercise($this->categories);
1674
1675
        return $this->iId;
1676
    }
1677
1678
    /**
1679
     * Updates question position.
1680
     *
1681
     * @return bool
1682
     */
1683
    public function update_question_positions()
1684
    {
1685
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1686
        // Fixes #3483 when updating order
1687
        $questionList = $this->selectQuestionList(true);
1688
1689
        if (empty($this->getId())) {
1690
            return false;
1691
        }
1692
1693
        if (!empty($questionList)) {
1694
            foreach ($questionList as $position => $questionId) {
1695
                $position = (int) $position;
1696
                $questionId = (int) $questionId;
1697
                $sql = "UPDATE $table SET
1698
                            question_order ='".$position."'
1699
                        WHERE
1700
                            c_id = ".$this->course_id.' AND
1701
                            question_id = '.$questionId.' AND
1702
                            exercice_id='.$this->getId();
1703
                Database::query($sql);
1704
            }
1705
        }
1706
1707
        return true;
1708
    }
1709
1710
    /**
1711
     * Adds a question into the question list.
1712
     *
1713
     * @author Olivier Brouckaert
1714
     *
1715
     * @param int $questionId - question ID
1716
     *
1717
     * @return bool - true if the question has been added, otherwise false
1718
     */
1719
    public function addToList($questionId)
1720
    {
1721
        // checks if the question ID is not in the list
1722
        if (!$this->isInList($questionId)) {
1723
            // selects the max position
1724
            if (!$this->selectNbrQuestions()) {
1725
                $pos = 1;
1726
            } else {
1727
                if (is_array($this->questionList)) {
1728
                    $pos = max(array_keys($this->questionList)) + 1;
1729
                }
1730
            }
1731
            $this->questionList[$pos] = $questionId;
1732
1733
            return true;
1734
        }
1735
1736
        return false;
1737
    }
1738
1739
    /**
1740
     * removes a question from the question list.
1741
     *
1742
     * @author Olivier Brouckaert
1743
     *
1744
     * @param int $questionId - question ID
1745
     *
1746
     * @return bool - true if the question has been removed, otherwise false
1747
     */
1748
    public function removeFromList($questionId)
1749
    {
1750
        // searches the position of the question ID in the list
1751
        $pos = array_search($questionId, $this->questionList);
1752
        // question not found
1753
        if (false === $pos) {
1754
            return false;
1755
        } else {
1756
            // dont reduce the number of random question if we use random by category option, or if
1757
            // random all questions
1758
            if ($this->isRandom() && 0 == $this->isRandomByCat()) {
1759
                if (count($this->questionList) >= $this->random && $this->random > 0) {
1760
                    $this->random--;
1761
                    $this->save();
1762
                }
1763
            }
1764
            // deletes the position from the array containing the wanted question ID
1765
            unset($this->questionList[$pos]);
1766
1767
            return true;
1768
        }
1769
    }
1770
1771
    /**
1772
     * deletes the exercise from the database
1773
     * Notice : leaves the question in the data base.
1774
     *
1775
     * @author Olivier Brouckaert
1776
     */
1777
    public function delete()
1778
    {
1779
        $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
1780
1781
        if ($limitTeacherAccess && !api_is_platform_admin()) {
1782
            return false;
1783
        }
1784
1785
        $exerciseId = $this->iId;
1786
1787
        $repo = Container::getExerciseRepository();
1788
        $exercise = $repo->find($exerciseId);
1789
1790
        if (null === $exercise) {
1791
            return false;
1792
        }
1793
1794
        $locked = api_resource_is_locked_by_gradebook(
1795
            $exerciseId,
1796
            LINK_EXERCISE
1797
        );
1798
1799
        if ($locked) {
1800
            return false;
1801
        }
1802
1803
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
1804
        $sql = "UPDATE $table SET active='-1'
1805
                WHERE c_id = ".$this->course_id.' AND iid = '.$exerciseId;
1806
        Database::query($sql);
1807
1808
        $repo->softDelete($exercise);
1809
1810
        Skill::deleteSkillsFromItem($exerciseId, ITEM_TYPE_EXERCISE);
1811
1812
        if ('true' === api_get_setting('search_enabled') &&
1813
            extension_loaded('xapian')
1814
        ) {
1815
            $this->search_engine_delete();
1816
        }
1817
1818
        $linkInfo = GradebookUtils::isResourceInCourseGradebook(
1819
            $this->course['code'],
1820
            LINK_EXERCISE,
1821
            $exerciseId,
1822
            $this->sessionId
1823
        );
1824
        if (false !== $linkInfo) {
1825
            GradebookUtils::remove_resource_from_course_gradebook($linkInfo['id']);
1826
        }
1827
1828
        return true;
1829
    }
1830
1831
    /**
1832
     * Creates the form to create / edit an exercise.
1833
     *
1834
     * @param FormValidator $form
1835
     * @param string        $type
1836
     */
1837
    public function createForm($form, $type = 'full')
1838
    {
1839
        if (empty($type)) {
1840
            $type = 'full';
1841
        }
1842
1843
        // Form title
1844
        $form_title = get_lang('Create a new test');
1845
        if (!empty($_GET['id'])) {
1846
            $form_title = get_lang('Edit test name and settings');
1847
        }
1848
1849
        $form->addHeader($form_title);
1850
1851
        // Title.
1852
        if (api_get_configuration_value('save_titles_as_html')) {
1853
            $form->addHtmlEditor(
1854
                'exerciseTitle',
1855
                get_lang('Test name'),
1856
                false,
1857
                false,
1858
                ['ToolbarSet' => 'TitleAsHtml']
1859
            );
1860
        } else {
1861
            $form->addElement(
1862
                'text',
1863
                'exerciseTitle',
1864
                get_lang('Test name'),
1865
                ['id' => 'exercise_title']
1866
            );
1867
        }
1868
1869
        $form->addElement('advanced_settings', 'advanced_params', get_lang('Advanced settings'));
1870
        $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
1871
1872
        if (api_get_configuration_value('allow_exercise_categories')) {
1873
            $categoryManager = new ExerciseCategoryManager();
1874
            $categories = $categoryManager->getCategories(api_get_course_int_id());
1875
            $options = [];
1876
            if (!empty($categories)) {
1877
                /** @var CExerciseCategory $category */
1878
                foreach ($categories as $category) {
1879
                    $options[$category->getId()] = $category->getName();
1880
                }
1881
            }
1882
1883
            $form->addSelect(
1884
                'exercise_category_id',
1885
                get_lang('Category'),
1886
                $options,
1887
                ['placeholder' => get_lang('Please select an option')]
1888
            );
1889
        }
1890
1891
        $editor_config = [
1892
            'ToolbarSet' => 'TestQuestionDescription',
1893
            'Width' => '100%',
1894
            'Height' => '150',
1895
        ];
1896
1897
        if (is_array($type)) {
1898
            $editor_config = array_merge($editor_config, $type);
1899
        }
1900
1901
        $form->addHtmlEditor(
1902
            'exerciseDescription',
1903
            get_lang('Give a context to the test'),
1904
            false,
1905
            false,
1906
            $editor_config
1907
        );
1908
1909
        $skillList = [];
1910
        if ('full' === $type) {
1911
            // Can't modify a DirectFeedback question.
1912
            if (!in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1913
                $this->setResultFeedbackGroup($form);
1914
1915
                // Type of results display on the final page
1916
                $this->setResultDisabledGroup($form);
1917
1918
                // Type of questions disposition on page
1919
                $radios = [];
1920
                $radios[] = $form->createElement(
1921
                    'radio',
1922
                    'exerciseType',
1923
                    null,
1924
                    get_lang('All questions on one page'),
1925
                    '1',
1926
                    [
1927
                        'onclick' => 'check_per_page_all()',
1928
                        'id' => 'option_page_all',
1929
                    ]
1930
                );
1931
                $radios[] = $form->createElement(
1932
                    'radio',
1933
                    'exerciseType',
1934
                    null,
1935
                    get_lang('One question by page'),
1936
                    '2',
1937
                    [
1938
                        'onclick' => 'check_per_page_one()',
1939
                        'id' => 'option_page_one',
1940
                    ]
1941
                );
1942
1943
                $form->addGroup($radios, null, get_lang('Questions per page'));
1944
            } else {
1945
                // if is Direct feedback but has not questions we can allow to modify the question type
1946
                if (0 === $this->getQuestionCount()) {
1947
                    $this->setResultFeedbackGroup($form);
1948
                    $this->setResultDisabledGroup($form);
1949
1950
                    // Type of questions disposition on page
1951
                    $radios = [];
1952
                    $radios[] = $form->createElement('radio', 'exerciseType', null, get_lang('All questions on one page'), '1');
1953
                    $radios[] = $form->createElement(
1954
                        'radio',
1955
                        'exerciseType',
1956
                        null,
1957
                        get_lang('One question by page'),
1958
                        '2'
1959
                    );
1960
                    $form->addGroup($radios, null, get_lang('Sequential'));
1961
                } else {
1962
                    $this->setResultFeedbackGroup($form, true);
1963
                    $group = $this->setResultDisabledGroup($form);
1964
                    $group->freeze();
1965
1966
                    // we force the options to the DirectFeedback exercisetype
1967
                    //$form->addElement('hidden', 'exerciseFeedbackType', $this->getFeedbackType());
1968
                    //$form->addElement('hidden', 'exerciseType', ONE_PER_PAGE);
1969
1970
                    // Type of questions disposition on page
1971
                    $radios[] = $form->createElement(
1972
                        'radio',
1973
                        'exerciseType',
1974
                        null,
1975
                        get_lang('All questions on one page'),
1976
                        '1',
1977
                        [
1978
                            'onclick' => 'check_per_page_all()',
1979
                            'id' => 'option_page_all',
1980
                        ]
1981
                    );
1982
                    $radios[] = $form->createElement(
1983
                        'radio',
1984
                        'exerciseType',
1985
                        null,
1986
                        get_lang('One question by page'),
1987
                        '2',
1988
                        [
1989
                            'onclick' => 'check_per_page_one()',
1990
                            'id' => 'option_page_one',
1991
                        ]
1992
                    );
1993
1994
                    $type_group = $form->addGroup($radios, null, get_lang('Questions per page'));
1995
                    $type_group->freeze();
1996
                }
1997
            }
1998
1999
            $option = [
2000
                EX_Q_SELECTION_ORDERED => get_lang('Ordered by user'),
2001
                //  Defined by user
2002
                EX_Q_SELECTION_RANDOM => get_lang('Random'),
2003
                // 1-10, All
2004
                'per_categories' => '--------'.get_lang('Using categories').'----------',
2005
                // Base (A 123 {3} B 456 {3} C 789{2} D 0{0}) --> Matrix {3, 3, 2, 0}
2006
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED => get_lang('Ordered categories alphabetically with questions ordered'),
2007
                // A 123 B 456 C 78 (0, 1, all)
2008
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED => get_lang('Random categories with questions ordered'),
2009
                // C 78 B 456 A 123
2010
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM => get_lang('Ordered categories alphabetically with random questions'),
2011
                // A 321 B 654 C 87
2012
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM => get_lang('Random categories with random questions'),
2013
                // C 87 B 654 A 321
2014
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED => get_lang('RandomCategoriesWithQuestionsOrderedNoQuestionGrouped'),
2015
                /*    B 456 C 78 A 123
2016
                        456 78 123
2017
                        123 456 78
2018
                */
2019
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED => get_lang('RandomCategoriesWithRandomQuestionsNoQuestionGrouped'),
2020
                /*
2021
                    A 123 B 456 C 78
2022
                    B 456 C 78 A 123
2023
                    B 654 C 87 A 321
2024
                    654 87 321
2025
                    165 842 73
2026
                */
2027
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED => get_lang('OrderedCategoriesByParentWithQuestionsOrdered'),
2028
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM => get_lang('OrderedCategoriesByParentWithQuestionsRandom'),
2029
            ];
2030
2031
            $form->addElement(
2032
                'select',
2033
                'question_selection_type',
2034
                [get_lang('Question selection type')],
2035
                $option,
2036
                [
2037
                    'id' => 'questionSelection',
2038
                    'onchange' => 'checkQuestionSelection()',
2039
                ]
2040
            );
2041
2042
            $group = [
2043
                $form->createElement(
2044
                    'checkbox',
2045
                    'hide_expected_answer',
2046
                    null,
2047
                    get_lang('Hide expected answers column')
2048
                ),
2049
                $form->createElement(
2050
                    'checkbox',
2051
                    'hide_total_score',
2052
                    null,
2053
                    get_lang('Hide total score')
2054
                ),
2055
                $form->createElement(
2056
                    'checkbox',
2057
                    'hide_question_score',
2058
                    null,
2059
                    get_lang('Hide question score')
2060
                ),
2061
            ];
2062
            $form->addGroup($group, null, get_lang('Results and feedback and feedback and feedback and feedback and feedback and feedback page configuration'));
2063
2064
            $displayMatrix = 'none';
2065
            $displayRandom = 'none';
2066
            $selectionType = $this->getQuestionSelectionType();
2067
            switch ($selectionType) {
2068
                case EX_Q_SELECTION_RANDOM:
2069
                    $displayRandom = 'block';
2070
2071
                    break;
2072
                case $selectionType >= EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED:
2073
                    $displayMatrix = 'block';
2074
2075
                    break;
2076
            }
2077
2078
            $form->addElement(
2079
                'html',
2080
                '<div id="hidden_random" style="display:'.$displayRandom.'">'
2081
            );
2082
            // Number of random question.
2083
            $max = $this->getId() > 0 ? $this->getQuestionCount() : 10;
2084
            $option = range(0, $max);
2085
            $option[0] = get_lang('No');
2086
            $option[-1] = get_lang('All');
2087
            $form->addElement(
2088
                'select',
2089
                'randomQuestions',
2090
                [
2091
                    get_lang('Random questions'),
2092
                    get_lang('Random questionsHelp'),
2093
                ],
2094
                $option,
2095
                ['id' => 'randomQuestions']
2096
            );
2097
            $form->addElement('html', '</div>');
2098
2099
            $form->addElement(
2100
                'html',
2101
                '<div id="hidden_matrix" style="display:'.$displayMatrix.'">'
2102
            );
2103
2104
            // Category selection.
2105
            $cat = new TestCategory();
2106
            $cat_form = $cat->returnCategoryForm($this);
2107
            if (empty($cat_form)) {
2108
                $cat_form = '<span class="label label-warning">'.get_lang('No categories defined').'</span>';
2109
            }
2110
            $form->addElement('label', null, $cat_form);
2111
            $form->addElement('html', '</div>');
2112
2113
            // Random answers.
2114
            $radios_random_answers = [
2115
                $form->createElement('radio', 'randomAnswers', null, get_lang('Yes'), '1'),
2116
                $form->createElement('radio', 'randomAnswers', null, get_lang('No'), '0'),
2117
            ];
2118
            $form->addGroup($radios_random_answers, null, get_lang('Shuffle answers'));
2119
2120
            // Category name.
2121
            $radio_display_cat_name = [
2122
                $form->createElement('radio', 'display_category_name', null, get_lang('Yes'), '1'),
2123
                $form->createElement('radio', 'display_category_name', null, get_lang('No'), '0'),
2124
            ];
2125
            $form->addGroup($radio_display_cat_name, null, get_lang('Display questions category'));
2126
2127
            // Hide question title.
2128
            $group = [
2129
                $form->createElement('radio', 'hide_question_title', null, get_lang('Yes'), '1'),
2130
                $form->createElement('radio', 'hide_question_title', null, get_lang('No'), '0'),
2131
            ];
2132
            $form->addGroup($group, null, get_lang('Hide question title'));
2133
2134
            $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
2135
2136
            if (true === $allow) {
2137
                // Hide question title.
2138
                $group = [
2139
                    $form->createElement(
2140
                        'radio',
2141
                        'show_previous_button',
2142
                        null,
2143
                        get_lang('Yes'),
2144
                        '1'
2145
                    ),
2146
                    $form->createElement(
2147
                        'radio',
2148
                        'show_previous_button',
2149
                        null,
2150
                        get_lang('No'),
2151
                        '0'
2152
                    ),
2153
                ];
2154
                $form->addGroup($group, null, get_lang('Show previous button'));
2155
            }
2156
2157
            $form->addElement(
2158
                'number',
2159
                'exerciseAttempts',
2160
                get_lang('max. 20 characters, e.g. <i>INNOV21</i> number of attempts'),
2161
                null,
2162
                ['id' => 'exerciseAttempts']
2163
            );
2164
2165
            // Exercise time limit
2166
            $form->addElement(
2167
                'checkbox',
2168
                'activate_start_date_check',
2169
                null,
2170
                get_lang('Enable start time'),
2171
                ['onclick' => 'activate_start_date()']
2172
            );
2173
2174
            if (!empty($this->start_time)) {
2175
                $form->addElement('html', '<div id="start_date_div" style="display:block;">');
2176
            } else {
2177
                $form->addElement('html', '<div id="start_date_div" style="display:none;">');
2178
            }
2179
2180
            $form->addElement('date_time_picker', 'start_time');
2181
            $form->addElement('html', '</div>');
2182
            $form->addElement(
2183
                'checkbox',
2184
                'activate_end_date_check',
2185
                null,
2186
                get_lang('Enable end time'),
2187
                ['onclick' => 'activate_end_date()']
2188
            );
2189
2190
            if (!empty($this->end_time)) {
2191
                $form->addElement('html', '<div id="end_date_div" style="display:block;">');
2192
            } else {
2193
                $form->addElement('html', '<div id="end_date_div" style="display:none;">');
2194
            }
2195
2196
            $form->addElement('date_time_picker', 'end_time');
2197
            $form->addElement('html', '</div>');
2198
2199
            $display = 'block';
2200
            $form->addElement(
2201
                'checkbox',
2202
                'propagate_neg',
2203
                null,
2204
                get_lang('Propagate negative results between questions')
2205
            );
2206
2207
            $options = [
2208
                '' => get_lang('Please select an option'),
2209
                1 => get_lang('Save the correct answer for the next attempt'),
2210
                2 => get_lang('Pre-fill with answers from previous attempt'),
2211
            ];
2212
            $form->addSelect(
2213
                'save_correct_answers',
2214
                get_lang('Save answers'),
2215
                $options
2216
            );
2217
2218
            $form->addElement('html', '<div class="clear">&nbsp;</div>');
2219
            $form->addElement('checkbox', 'review_answers', null, get_lang('Review my answers'));
2220
            $form->addElement('html', '<div id="divtimecontrol"  style="display:'.$display.';">');
2221
2222
            // Timer control
2223
            $form->addElement(
2224
                'checkbox',
2225
                'enabletimercontrol',
2226
                null,
2227
                get_lang('Enable time control'),
2228
                [
2229
                    'onclick' => 'option_time_expired()',
2230
                    'id' => 'enabletimercontrol',
2231
                    'onload' => 'check_load_time()',
2232
                ]
2233
            );
2234
2235
            $expired_date = (int) $this->selectExpiredTime();
2236
2237
            if (('0' != $expired_date)) {
2238
                $form->addElement('html', '<div id="timercontrol" style="display:block;">');
2239
            } else {
2240
                $form->addElement('html', '<div id="timercontrol" style="display:none;">');
2241
            }
2242
            $form->addText(
2243
                'enabletimercontroltotalminutes',
2244
                get_lang('Total duration in minutes of the test'),
2245
                false,
2246
                [
2247
                    'id' => 'enabletimercontroltotalminutes',
2248
                    'cols-size' => [2, 2, 8],
2249
                ]
2250
            );
2251
            $form->addElement('html', '</div>');
2252
2253
            if (api_get_configuration_value('quiz_prevent_backwards_move')) {
2254
                $form->addCheckBox(
2255
                    'prevent_backwards',
2256
                    null,
2257
                    get_lang('QuizPreventBackwards')
2258
                );
2259
            }
2260
2261
            $form->addElement(
2262
                'text',
2263
                'pass_percentage',
2264
                [get_lang('Pass percentage'), null, '%'],
2265
                ['id' => 'pass_percentage']
2266
            );
2267
2268
            $form->addRule('pass_percentage', get_lang('Numericalal'), 'numeric');
2269
            $form->addRule('pass_percentage', get_lang('Value is too small.'), 'min_numeric_length', 0);
2270
            $form->addRule('pass_percentage', get_lang('Value is too big.'), 'max_numeric_length', 100);
2271
2272
            // add the text_when_finished textbox
2273
            $form->addHtmlEditor(
2274
                'text_when_finished',
2275
                get_lang('Text appearing at the end of the test'),
2276
                false,
2277
                false,
2278
                $editor_config
2279
            );
2280
2281
            $allow = api_get_configuration_value('allow_notification_setting_per_exercise');
2282
            if (true === $allow) {
2283
                $settings = ExerciseLib::getNotificationSettings();
2284
                $group = [];
2285
                foreach ($settings as $itemId => $label) {
2286
                    $group[] = $form->createElement(
2287
                        'checkbox',
2288
                        'notifications[]',
2289
                        null,
2290
                        $label,
2291
                        ['value' => $itemId]
2292
                    );
2293
                }
2294
                $form->addGroup($group, '', [get_lang('E-mail notifications')]);
2295
            }
2296
2297
            $form->addCheckBox(
2298
                'update_title_in_lps',
2299
                null,
2300
                get_lang('Update this title in learning paths')
2301
            );
2302
2303
            $defaults = [];
2304
            if ('true' === api_get_setting('search_enabled')) {
2305
                require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
2306
                $form->addElement('checkbox', 'index_document', '', get_lang('Index document text?'));
2307
                $form->addSelectLanguage('language', get_lang('Document language for indexation'));
2308
                $specific_fields = get_specific_field_list();
2309
2310
                foreach ($specific_fields as $specific_field) {
2311
                    $form->addElement('text', $specific_field['code'], $specific_field['name']);
2312
                    $filter = [
2313
                        'c_id' => api_get_course_int_id(),
2314
                        'field_id' => $specific_field['id'],
2315
                        'ref_id' => $this->getId(),
2316
                        'tool_id' => "'".TOOL_QUIZ."'",
2317
                    ];
2318
                    $values = get_specific_field_values_list($filter, ['value']);
2319
                    if (!empty($values)) {
2320
                        $arr_str_values = [];
2321
                        foreach ($values as $value) {
2322
                            $arr_str_values[] = $value['value'];
2323
                        }
2324
                        $defaults[$specific_field['code']] = implode(', ', $arr_str_values);
2325
                    }
2326
                }
2327
            }
2328
2329
            $skillList = Skill::addSkillsToForm($form, ITEM_TYPE_EXERCISE, $this->iId);
2330
2331
            $extraField = new ExtraField('exercise');
2332
            $extraField->addElements(
2333
                $form,
2334
                $this->iId,
2335
                [], //exclude
2336
                false, // filter
2337
                false, // tag as select
2338
                [], //show only fields
2339
                [], // order fields
2340
                [] // extra data
2341
            );
2342
            $form->addElement('html', '</div>'); //End advanced setting
2343
            $form->addElement('html', '</div>');
2344
        }
2345
2346
        // submit
2347
        if (isset($_GET['id'])) {
2348
            $form->addButtonSave(get_lang('Edit test name and settings'), 'submitExercise');
2349
        } else {
2350
            $form->addButtonUpdate(get_lang('Proceed to questions'), 'submitExercise');
2351
        }
2352
2353
        $form->addRule('exerciseTitle', get_lang('Name'), 'required');
2354
2355
        // defaults
2356
        if ('full' == $type) {
2357
            // rules
2358
            $form->addRule('exerciseAttempts', get_lang('Numeric'), 'numeric');
2359
            $form->addRule('start_time', get_lang('Invalid date'), 'datetime');
2360
            $form->addRule('end_time', get_lang('Invalid date'), 'datetime');
2361
2362
            if ($this->getId() > 0) {
2363
                $defaults['randomQuestions'] = $this->random;
2364
                $defaults['randomAnswers'] = $this->getRandomAnswers();
2365
                $defaults['exerciseType'] = $this->selectType();
2366
                $defaults['exerciseTitle'] = $this->get_formated_title();
2367
                $defaults['exerciseDescription'] = $this->selectDescription();
2368
                $defaults['exerciseAttempts'] = $this->selectAttempts();
2369
                $defaults['exerciseFeedbackType'] = $this->getFeedbackType();
2370
                $defaults['results_disabled'] = $this->selectResultsDisabled();
2371
                $defaults['propagate_neg'] = $this->selectPropagateNeg();
2372
                $defaults['save_correct_answers'] = $this->getSaveCorrectAnswers();
2373
                $defaults['review_answers'] = $this->review_answers;
2374
                $defaults['randomByCat'] = $this->getRandomByCategory();
2375
                $defaults['text_when_finished'] = $this->getTextWhenFinished();
2376
                $defaults['display_category_name'] = $this->selectDisplayCategoryName();
2377
                $defaults['pass_percentage'] = $this->selectPassPercentage();
2378
                $defaults['question_selection_type'] = $this->getQuestionSelectionType();
2379
                $defaults['hide_question_title'] = $this->getHideQuestionTitle();
2380
                $defaults['show_previous_button'] = $this->showPreviousButton();
2381
                $defaults['exercise_category_id'] = $this->getExerciseCategoryId();
2382
                $defaults['prevent_backwards'] = $this->getPreventBackwards();
2383
2384
                if (!empty($this->start_time)) {
2385
                    $defaults['activate_start_date_check'] = 1;
2386
                }
2387
                if (!empty($this->end_time)) {
2388
                    $defaults['activate_end_date_check'] = 1;
2389
                }
2390
2391
                $defaults['start_time'] = !empty($this->start_time) ? api_get_local_time($this->start_time) : date('Y-m-d 12:00:00');
2392
                $defaults['end_time'] = !empty($this->end_time) ? api_get_local_time($this->end_time) : date('Y-m-d 12:00:00', time() + 84600);
2393
2394
                // Get expired time
2395
                if ('0' != $this->expired_time) {
2396
                    $defaults['enabletimercontrol'] = 1;
2397
                    $defaults['enabletimercontroltotalminutes'] = $this->expired_time;
2398
                } else {
2399
                    $defaults['enabletimercontroltotalminutes'] = 0;
2400
                }
2401
                $defaults['skills'] = array_keys($skillList);
2402
                $defaults['notifications'] = $this->getNotifications();
2403
            } else {
2404
                $defaults['exerciseType'] = 2;
2405
                $defaults['exerciseAttempts'] = 0;
2406
                $defaults['randomQuestions'] = 0;
2407
                $defaults['randomAnswers'] = 0;
2408
                $defaults['exerciseDescription'] = '';
2409
                $defaults['exerciseFeedbackType'] = 0;
2410
                $defaults['results_disabled'] = 0;
2411
                $defaults['randomByCat'] = 0;
2412
                $defaults['text_when_finished'] = '';
2413
                $defaults['start_time'] = date('Y-m-d 12:00:00');
2414
                $defaults['display_category_name'] = 1;
2415
                $defaults['end_time'] = date('Y-m-d 12:00:00', time() + 84600);
2416
                $defaults['pass_percentage'] = '';
2417
                $defaults['end_button'] = $this->selectEndButton();
2418
                $defaults['question_selection_type'] = 1;
2419
                $defaults['hide_question_title'] = 0;
2420
                $defaults['show_previous_button'] = 1;
2421
                $defaults['on_success_message'] = null;
2422
                $defaults['on_failed_message'] = null;
2423
            }
2424
        } else {
2425
            $defaults['exerciseTitle'] = $this->selectTitle();
2426
            $defaults['exerciseDescription'] = $this->selectDescription();
2427
        }
2428
2429
        if ('true' === api_get_setting('search_enabled')) {
2430
            $defaults['index_document'] = 'checked="checked"';
2431
        }
2432
2433
        $this->setPageResultConfigurationDefaults($defaults);
2434
        $form->setDefaults($defaults);
2435
2436
        // Freeze some elements.
2437
        if (0 != $this->getId() && false == $this->edit_exercise_in_lp) {
2438
            $elementsToFreeze = [
2439
                'randomQuestions',
2440
                //'randomByCat',
2441
                'exerciseAttempts',
2442
                'propagate_neg',
2443
                'enabletimercontrol',
2444
                'review_answers',
2445
            ];
2446
2447
            foreach ($elementsToFreeze as $elementName) {
2448
                /** @var HTML_QuickForm_element $element */
2449
                $element = $form->getElement($elementName);
2450
                $element->freeze();
2451
            }
2452
        }
2453
    }
2454
2455
    public function setResultFeedbackGroup(FormValidator $form, $checkFreeze = true)
2456
    {
2457
        // Feedback type.
2458
        $feedback = [];
2459
        $feedback[] = $form->createElement(
2460
            'radio',
2461
            'exerciseFeedbackType',
2462
            null,
2463
            get_lang('At end of test'),
2464
            EXERCISE_FEEDBACK_TYPE_END,
2465
            [
2466
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_END,
2467
                'onclick' => 'check_feedback()',
2468
            ]
2469
        );
2470
2471
        $noFeedBack = $form->createElement(
2472
            'radio',
2473
            'exerciseFeedbackType',
2474
            null,
2475
            get_lang('NoFeedback'),
2476
            EXERCISE_FEEDBACK_TYPE_EXAM,
2477
            [
2478
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_EXAM,
2479
            ]
2480
        );
2481
2482
        $freeze = true;
2483
        if ('true' === api_get_setting('enable_quiz_scenario')) {
2484
            if (0 === $this->getQuestionCount()) {
2485
                $freeze = false;
2486
            } else {
2487
                $hasDifferentQuestion = $this->hasQuestionWithTypeNotInList([UNIQUE_ANSWER, HOT_SPOT_DELINEATION]);
2488
                if (false === $hasDifferentQuestion) {
2489
                    $freeze = false;
2490
                }
2491
            }
2492
        }
2493
            // Can't convert a question from one feedback to another
2494
        $direct = $form->createElement(
2495
                    'radio',
2496
                    'exerciseFeedbackType',
2497
                    null,
2498
            get_lang('DirectFeedback'),
2499
                    EXERCISE_FEEDBACK_TYPE_DIRECT,
2500
                    [
2501
                        'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_DIRECT,
2502
                        'onclick' => 'check_direct_feedback()',
2503
                    ]
2504
                );
2505
2506
        if ($freeze) {
2507
            $direct->freeze();
2508
            $noFeedBack->freeze();
2509
            }
2510
2511
        $feedback[] = $noFeedBack;
2512
        $feedback[] = $direct;
2513
2514
        $feedback[] = $form->createElement(
2515
            'radio',
2516
            'exerciseFeedbackType',
2517
            null,
2518
            get_lang('ExerciseDirectPopUp'),
2519
            EXERCISE_FEEDBACK_TYPE_POPUP,
2520
            ['id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_POPUP, 'onclick' => 'check_direct_feedback()']
2521
        );
2522
2523
        $group = $form->addGroup(
2524
            $feedback,
2525
            null,
2526
            [
2527
                get_lang('FeedbackType'),
2528
                get_lang('FeedbackDisplayOptions'),
2529
            ]
2530
        );
2531
2532
        if ($freeze) {
2533
            //$group->freeze();
2534
        }
2535
    }
2536
2537
    /**
2538
     * function which process the creation of exercises.
2539
     *
2540
     * @param FormValidator $form
2541
     *
2542
     * @return int c_quiz.iid
2543
     */
2544
    public function processCreation($form)
2545
    {
2546
        $this->updateTitle(self::format_title_variable($form->getSubmitValue('exerciseTitle')));
2547
        $this->updateDescription($form->getSubmitValue('exerciseDescription'));
2548
        $this->updateAttempts($form->getSubmitValue('exerciseAttempts'));
2549
        $this->updateFeedbackType($form->getSubmitValue('exerciseFeedbackType'));
2550
        $this->updateType($form->getSubmitValue('exerciseType'));
2551
2552
        // If direct feedback then force to One per page
2553
        if (EXERCISE_FEEDBACK_TYPE_DIRECT == $form->getSubmitValue('exerciseFeedbackType')) {
2554
            $this->updateType(ONE_PER_PAGE);
2555
        }
2556
2557
        $this->setRandom($form->getSubmitValue('randomQuestions'));
2558
        $this->updateRandomAnswers($form->getSubmitValue('randomAnswers'));
2559
        $this->updateResultsDisabled($form->getSubmitValue('results_disabled'));
2560
        $this->updateExpiredTime($form->getSubmitValue('enabletimercontroltotalminutes'));
2561
        $this->updatePropagateNegative($form->getSubmitValue('propagate_neg'));
2562
        $this->updateSaveCorrectAnswers($form->getSubmitValue('save_correct_answers'));
2563
        $this->updateRandomByCat($form->getSubmitValue('randomByCat'));
2564
        $this->updateTextWhenFinished($form->getSubmitValue('text_when_finished'));
2565
        $this->updateDisplayCategoryName($form->getSubmitValue('display_category_name'));
2566
        $this->updateReviewAnswers($form->getSubmitValue('review_answers'));
2567
        $this->updatePassPercentage($form->getSubmitValue('pass_percentage'));
2568
        $this->updateCategories($form->getSubmitValue('category'));
2569
        $this->updateEndButton($form->getSubmitValue('end_button'));
2570
        $this->setOnSuccessMessage($form->getSubmitValue('on_success_message'));
2571
        $this->setOnFailedMessage($form->getSubmitValue('on_failed_message'));
2572
        $this->updateEmailNotificationTemplate($form->getSubmitValue('email_notification_template'));
2573
        $this->setEmailNotificationTemplateToUser($form->getSubmitValue('email_notification_template_to_user'));
2574
        $this->setNotifyUserByEmail($form->getSubmitValue('notify_user_by_email'));
2575
        $this->setModelType($form->getSubmitValue('model_type'));
2576
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2577
        $this->setHideQuestionTitle($form->getSubmitValue('hide_question_title'));
2578
        $this->sessionId = api_get_session_id();
2579
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2580
        $this->setScoreTypeModel($form->getSubmitValue('score_type_model'));
2581
        $this->setGlobalCategoryId($form->getSubmitValue('global_category_id'));
2582
        $this->setShowPreviousButton($form->getSubmitValue('show_previous_button'));
2583
        $this->setNotifications($form->getSubmitValue('notifications'));
2584
        $this->setExerciseCategoryId($form->getSubmitValue('exercise_category_id'));
2585
        $this->setPageResultConfiguration($form->getSubmitValues());
2586
        $this->preventBackwards = (int) $form->getSubmitValue('prevent_backwards');
2587
2588
        $this->start_time = null;
2589
        if (1 == $form->getSubmitValue('activate_start_date_check')) {
2590
            $start_time = $form->getSubmitValue('start_time');
2591
            $this->start_time = api_get_utc_datetime($start_time);
2592
        }
2593
2594
        $this->end_time = null;
2595
        if (1 == $form->getSubmitValue('activate_end_date_check')) {
2596
            $end_time = $form->getSubmitValue('end_time');
2597
            $this->end_time = api_get_utc_datetime($end_time);
2598
        }
2599
2600
        $this->expired_time = 0;
2601
        if (1 == $form->getSubmitValue('enabletimercontrol')) {
2602
            $expired_total_time = $form->getSubmitValue('enabletimercontroltotalminutes');
2603
            if (0 == $this->expired_time) {
2604
                $this->expired_time = $expired_total_time;
2605
            }
2606
        }
2607
2608
        $this->random_answers = 0;
2609
        if (1 == $form->getSubmitValue('randomAnswers')) {
2610
            $this->random_answers = 1;
2611
        }
2612
2613
        // Update title in all LPs that have this quiz added
2614
        if (1 == $form->getSubmitValue('update_title_in_lps')) {
2615
            $courseId = api_get_course_int_id();
2616
            $table = Database::get_course_table(TABLE_LP_ITEM);
2617
            $sql = "SELECT * FROM $table
2618
                    WHERE
2619
                        c_id = $courseId AND
2620
                        item_type = 'quiz' AND
2621
                        path = '".$this->getId()."'
2622
                    ";
2623
            $result = Database::query($sql);
2624
            $items = Database::store_result($result);
2625
            if (!empty($items)) {
2626
                foreach ($items as $item) {
2627
                    $itemId = $item['iid'];
2628
                    $sql = "UPDATE $table SET title = '".$this->title."'
2629
                            WHERE iid = $itemId AND c_id = $courseId ";
2630
                    Database::query($sql);
2631
                }
2632
            }
2633
        }
2634
2635
        $iId = $this->save();
2636
        if (!empty($iId)) {
2637
            $values = $form->getSubmitValues();
2638
            $values['item_id'] = $iId;
2639
            $extraFieldValue = new ExtraFieldValue('exercise');
2640
            $extraFieldValue->saveFieldValues($values);
2641
2642
            Skill::saveSkills($form, ITEM_TYPE_EXERCISE, $iId);
2643
        }
2644
    }
2645
2646
    public function search_engine_save()
2647
    {
2648
        if (1 != $_POST['index_document']) {
2649
            return;
2650
        }
2651
        $course_id = api_get_course_id();
2652
2653
        require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
2654
2655
        $specific_fields = get_specific_field_list();
2656
        $ic_slide = new IndexableChunk();
2657
2658
        $all_specific_terms = '';
2659
        foreach ($specific_fields as $specific_field) {
2660
            if (isset($_REQUEST[$specific_field['code']])) {
2661
                $sterms = trim($_REQUEST[$specific_field['code']]);
2662
                if (!empty($sterms)) {
2663
                    $all_specific_terms .= ' '.$sterms;
2664
                    $sterms = explode(',', $sterms);
2665
                    foreach ($sterms as $sterm) {
2666
                        $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2667
                        add_specific_field_value($specific_field['id'], $course_id, TOOL_QUIZ, $this->getId(), $sterm);
2668
                    }
2669
                }
2670
            }
2671
        }
2672
2673
        // build the chunk to index
2674
        $ic_slide->addValue('title', $this->exercise);
2675
        $ic_slide->addCourseId($course_id);
2676
        $ic_slide->addToolId(TOOL_QUIZ);
2677
        $xapian_data = [
2678
            SE_COURSE_ID => $course_id,
2679
            SE_TOOL_ID => TOOL_QUIZ,
2680
            SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->getId()],
2681
            SE_USER => (int) api_get_user_id(),
2682
        ];
2683
        $ic_slide->xapian_data = serialize($xapian_data);
2684
        $exercise_description = $all_specific_terms.' '.$this->description;
2685
        $ic_slide->addValue('content', $exercise_description);
2686
2687
        $di = new ChamiloIndexer();
2688
        isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
2689
        $di->connectDb(null, null, $lang);
2690
        $di->addChunk($ic_slide);
2691
2692
        //index and return search engine document id
2693
        $did = $di->index();
2694
        if ($did) {
2695
            // save it to db
2696
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2697
            $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2698
			    VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2699
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId(), $did);
2700
            Database::query($sql);
2701
        }
2702
    }
2703
2704
    public function search_engine_edit()
2705
    {
2706
        // update search enchine and its values table if enabled
2707
        if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
2708
            $course_id = api_get_course_id();
2709
2710
            // actually, it consists on delete terms from db,
2711
            // insert new ones, create a new search engine document, and remove the old one
2712
            // get search_did
2713
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2714
            $sql = 'SELECT * FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s LIMIT 1';
2715
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2716
            $res = Database::query($sql);
2717
2718
            if (Database::num_rows($res) > 0) {
2719
                require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
2720
2721
                $se_ref = Database::fetch_array($res);
2722
                $specific_fields = get_specific_field_list();
2723
                $ic_slide = new IndexableChunk();
2724
2725
                $all_specific_terms = '';
2726
                foreach ($specific_fields as $specific_field) {
2727
                    delete_all_specific_field_value($course_id, $specific_field['id'], TOOL_QUIZ, $this->getId());
2728
                    if (isset($_REQUEST[$specific_field['code']])) {
2729
                        $sterms = trim($_REQUEST[$specific_field['code']]);
2730
                        $all_specific_terms .= ' '.$sterms;
2731
                        $sterms = explode(',', $sterms);
2732
                        foreach ($sterms as $sterm) {
2733
                            $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2734
                            add_specific_field_value($specific_field['id'], $course_id, TOOL_QUIZ, $this->getId(), $sterm);
2735
                        }
2736
                    }
2737
                }
2738
2739
                // build the chunk to index
2740
                $ic_slide->addValue('title', $this->exercise);
2741
                $ic_slide->addCourseId($course_id);
2742
                $ic_slide->addToolId(TOOL_QUIZ);
2743
                $xapian_data = [
2744
                    SE_COURSE_ID => $course_id,
2745
                    SE_TOOL_ID => TOOL_QUIZ,
2746
                    SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->getId()],
2747
                    SE_USER => (int) api_get_user_id(),
2748
                ];
2749
                $ic_slide->xapian_data = serialize($xapian_data);
2750
                $exercise_description = $all_specific_terms.' '.$this->description;
2751
                $ic_slide->addValue('content', $exercise_description);
2752
2753
                $di = new ChamiloIndexer();
2754
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
2755
                $di->connectDb(null, null, $lang);
2756
                $di->remove_document($se_ref['search_did']);
2757
                $di->addChunk($ic_slide);
2758
2759
                //index and return search engine document id
2760
                $did = $di->index();
2761
                if ($did) {
2762
                    // save it to db
2763
                    $sql = 'DELETE FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=\'%s\'';
2764
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2765
                    Database::query($sql);
2766
                    $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2767
                        VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2768
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId(), $did);
2769
                    Database::query($sql);
2770
                }
2771
            } else {
2772
                $this->search_engine_save();
2773
            }
2774
        }
2775
    }
2776
2777
    public function search_engine_delete()
2778
    {
2779
        // remove from search engine if enabled
2780
        if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
2781
            $course_id = api_get_course_id();
2782
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2783
            $sql = 'SELECT * FROM %s
2784
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
2785
                    LIMIT 1';
2786
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2787
            $res = Database::query($sql);
2788
            if (Database::num_rows($res) > 0) {
2789
                $row = Database::fetch_array($res);
2790
                $di = new ChamiloIndexer();
2791
                $di->remove_document($row['search_did']);
2792
                unset($di);
2793
                $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2794
                foreach ($this->questionList as $question_i) {
2795
                    $sql = 'SELECT type FROM %s WHERE id=%s';
2796
                    $sql = sprintf($sql, $tbl_quiz_question, $question_i);
2797
                    $qres = Database::query($sql);
2798
                    if (Database::num_rows($qres) > 0) {
2799
                        $qrow = Database::fetch_array($qres);
2800
                        $objQuestion = Question::getInstance($qrow['type']);
2801
                        $objQuestion = Question::read((int) $question_i);
2802
                        $objQuestion->search_engine_edit($this->getId(), false, true);
2803
                        unset($objQuestion);
2804
                    }
2805
                }
2806
            }
2807
            $sql = 'DELETE FROM %s
2808
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
2809
                    LIMIT 1';
2810
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->getId());
2811
            Database::query($sql);
2812
2813
            // remove terms from db
2814
            require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
2815
            delete_all_values_for_item($course_id, TOOL_QUIZ, $this->getId());
2816
        }
2817
    }
2818
2819
    public function selectExpiredTime()
2820
    {
2821
        return $this->expired_time;
2822
    }
2823
2824
    /**
2825
     * Cleans the student's results only for the Exercise tool (Not from the LP)
2826
     * The LP results are NOT deleted by default, otherwise put $cleanLpTests = true
2827
     * Works with exercises in sessions.
2828
     *
2829
     * @param bool   $cleanLpTests
2830
     * @param string $cleanResultBeforeDate
2831
     *
2832
     * @return int quantity of user's exercises deleted
2833
     */
2834
    public function cleanResults($cleanLpTests = false, $cleanResultBeforeDate = null)
2835
    {
2836
        $sessionId = api_get_session_id();
2837
        $table_track_e_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2838
        $table_track_e_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
2839
2840
        $sql_where = '  AND
2841
                        orig_lp_id = 0 AND
2842
                        orig_lp_item_id = 0';
2843
2844
        // if we want to delete results from LP too
2845
        if ($cleanLpTests) {
2846
            $sql_where = '';
2847
        }
2848
2849
        // if we want to delete attempts before date $cleanResultBeforeDate
2850
        // $cleanResultBeforeDate must be a valid UTC-0 date yyyy-mm-dd
2851
2852
        if (!empty($cleanResultBeforeDate)) {
2853
            $cleanResultBeforeDate = Database::escape_string($cleanResultBeforeDate);
2854
            if (api_is_valid_date($cleanResultBeforeDate)) {
2855
                $sql_where .= "  AND exe_date <= '$cleanResultBeforeDate' ";
2856
            } else {
2857
                return 0;
2858
            }
2859
        }
2860
2861
        $sql = "SELECT exe_id
2862
            FROM $table_track_e_exercises
2863
            WHERE
2864
                c_id = ".api_get_course_int_id().' AND
2865
                exe_exo_id = '.$this->getId().' AND
2866
                session_id = '.$sessionId.' '.
2867
                $sql_where;
2868
2869
        $result = Database::query($sql);
2870
        $exe_list = Database::store_result($result);
2871
2872
        // deleting TRACK_E_ATTEMPT table
2873
        // check if exe in learning path or not
2874
        $i = 0;
2875
        if (is_array($exe_list) && count($exe_list) > 0) {
2876
            foreach ($exe_list as $item) {
2877
                $sql = "DELETE FROM $table_track_e_attempt
2878
                        WHERE exe_id = '".$item['exe_id']."'";
2879
                Database::query($sql);
2880
                $i++;
2881
            }
2882
        }
2883
2884
        // delete TRACK_E_EXERCISES table
2885
        $sql = "DELETE FROM $table_track_e_exercises
2886
                WHERE
2887
                  c_id = ".api_get_course_int_id().' AND
2888
                  exe_exo_id = '.$this->getId()." $sql_where AND
2889
                  session_id = ".$sessionId;
2890
        Database::query($sql);
2891
2892
        $this->generateStats($this->getId(), api_get_course_info(), $sessionId);
2893
2894
        Event::addEvent(
2895
            LOG_EXERCISE_RESULT_DELETE,
2896
            LOG_EXERCISE_ID,
2897
            $this->getId(),
2898
            null,
2899
            null,
2900
            api_get_course_int_id(),
2901
            $sessionId
2902
        );
2903
2904
        return $i;
2905
    }
2906
2907
    /**
2908
     * Copies an exercise (duplicate all questions and answers).
2909
     */
2910
    public function copyExercise()
2911
    {
2912
        $exerciseObject = $this;
2913
        $categories = $exerciseObject->getCategoriesInExercise();
2914
        // Get all questions no matter the order/category settings
2915
        $questionList = $exerciseObject->getQuestionOrderedList();
2916
        // Force the creation of a new exercise
2917
        $exerciseObject->updateTitle($exerciseObject->selectTitle().' - '.get_lang('Copy'));
2918
        // Hides the new exercise
2919
        $exerciseObject->updateStatus(false);
2920
        $exerciseObject->updateId(0);
2921
        $exerciseObject->sessionId = api_get_session_id();
2922
        $exerciseObject->save();
2923
        $newId = $exerciseObject->getId();
2924
        if ($newId && !empty($questionList)) {
2925
            // Question creation
2926
            foreach ($questionList as $oldQuestionId) {
2927
                $oldQuestionObj = Question::read($oldQuestionId);
2928
                $newQuestionId = $oldQuestionObj->duplicate();
2929
                if ($newQuestionId) {
2930
                    $newQuestionObj = Question::read($newQuestionId);
2931
                    if (isset($newQuestionObj) && $newQuestionObj) {
2932
                        $newQuestionObj->addToList($newId);
2933
                        if (!empty($oldQuestionObj->category)) {
2934
                            $newQuestionObj->saveCategory($oldQuestionObj->category);
2935
                        }
2936
2937
                        // This should be moved to the duplicate function
2938
                        $newAnswerObj = new Answer($oldQuestionId);
2939
                        $newAnswerObj->read();
2940
                        $newAnswerObj->duplicate($newQuestionObj);
2941
                    }
2942
                }
2943
            }
2944
            if (!empty($categories)) {
2945
                $newCategoryList = [];
2946
                foreach ($categories as $category) {
2947
                    $newCategoryList[$category['category_id']] = $category['count_questions'];
2948
                }
2949
                $exerciseObject->save_categories_in_exercise($newCategoryList);
2950
            }
2951
        }
2952
    }
2953
2954
    /**
2955
     * Changes the exercise status.
2956
     *
2957
     * @param string $status - exercise status
2958
     */
2959
    public function updateStatus($status)
2960
    {
2961
        $this->active = $status;
2962
    }
2963
2964
    /**
2965
     * @param int    $lp_id
2966
     * @param int    $lp_item_id
2967
     * @param int    $lp_item_view_id
2968
     * @param string $status
2969
     *
2970
     * @return array
2971
     */
2972
    public function get_stat_track_exercise_info(
2973
        $lp_id = 0,
2974
        $lp_item_id = 0,
2975
        $lp_item_view_id = 0,
2976
        $status = 'incomplete'
2977
    ) {
2978
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2979
        if (empty($lp_id)) {
2980
            $lp_id = 0;
2981
        }
2982
        if (empty($lp_item_id)) {
2983
            $lp_item_id = 0;
2984
        }
2985
        if (empty($lp_item_view_id)) {
2986
            $lp_item_view_id = 0;
2987
        }
2988
        $condition = ' WHERE exe_exo_id 	= '."'".$this->getId()."'".' AND
2989
					   exe_user_id 			= '."'".api_get_user_id()."'".' AND
2990
					   c_id                 = '.api_get_course_int_id().' AND
2991
					   status 				= '."'".Database::escape_string($status)."'".' AND
2992
					   orig_lp_id 			= '."'".$lp_id."'".' AND
2993
					   orig_lp_item_id 		= '."'".$lp_item_id."'".' AND
2994
                       orig_lp_item_view_id = '."'".$lp_item_view_id."'".' AND
2995
					   session_id 			= '."'".api_get_session_id()."' LIMIT 1"; //Adding limit 1 just in case
2996
2997
        $sql_track = 'SELECT * FROM '.$track_exercises.$condition;
2998
2999
        $result = Database::query($sql_track);
3000
        $new_array = [];
3001
        if (Database::num_rows($result) > 0) {
3002
            $new_array = Database::fetch_array($result, 'ASSOC');
3003
            $new_array['num_exe'] = Database::num_rows($result);
3004
        }
3005
3006
        return $new_array;
3007
    }
3008
3009
    /**
3010
     * Saves a test attempt.
3011
     *
3012
     * @param int   $clock_expired_time   clock_expired_time
3013
     * @param int   $safe_lp_id           int lp id
3014
     * @param int   $safe_lp_item_id      int lp item id
3015
     * @param int   $safe_lp_item_view_id int lp item_view id
3016
     * @param array $questionList
3017
     * @param float $weight
3018
     *
3019
     * @return int
3020
     */
3021
    public function save_stat_track_exercise_info(
3022
        $clock_expired_time = 0,
3023
        $safe_lp_id = 0,
3024
        $safe_lp_item_id = 0,
3025
        $safe_lp_item_view_id = 0,
3026
        $questionList = [],
3027
        $weight = 0
3028
    ) {
3029
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3030
        $safe_lp_id = (int) $safe_lp_id;
3031
        $safe_lp_item_id = (int) $safe_lp_item_id;
3032
        $safe_lp_item_view_id = (int) $safe_lp_item_view_id;
3033
3034
        if (empty($clock_expired_time)) {
3035
            $clock_expired_time = null;
3036
        }
3037
3038
        $questionList = array_map('intval', $questionList);
3039
3040
        $params = [
3041
            'exe_exo_id' => $this->getId(),
3042
            'exe_user_id' => api_get_user_id(),
3043
            'c_id' => api_get_course_int_id(),
3044
            'status' => 'incomplete',
3045
            'session_id' => api_get_session_id(),
3046
            'data_tracking' => implode(',', $questionList),
3047
            'start_date' => api_get_utc_datetime(),
3048
            'orig_lp_id' => $safe_lp_id,
3049
            'orig_lp_item_id' => $safe_lp_item_id,
3050
            'orig_lp_item_view_id' => $safe_lp_item_view_id,
3051
            'max_score' => $weight,
3052
            'user_ip' => Database::escape_string(api_get_real_ip()),
3053
            'exe_date' => api_get_utc_datetime(),
3054
            'score' => 0,
3055
            'steps_counter' => 0,
3056
            'exe_duration' => 0,
3057
            'expired_time_control' => $clock_expired_time,
3058
            'questions_to_check' => '',
3059
        ];
3060
3061
        return Database::insert($track_exercises, $params);
3062
    }
3063
3064
    /**
3065
     * @param int    $question_id
3066
     * @param int    $questionNum
3067
     * @param array  $questions_in_media
3068
     * @param string $currentAnswer
3069
     * @param array  $myRemindList
3070
     *
3071
     * @return string
3072
     */
3073
    public function show_button(
3074
        $question_id,
3075
        $questionNum,
3076
        $questions_in_media = [],
3077
        $currentAnswer = '',
3078
        $myRemindList = []
3079
    ) {
3080
        global $safe_lp_id, $safe_lp_item_id, $safe_lp_item_view_id;
3081
        $nbrQuestions = $this->countQuestionsInExercise();
3082
        $buttonList = [];
3083
        $html = $label = '';
3084
        $hotspotGet = isset($_POST['hotspot']) ? Security::remove_XSS($_POST['hotspot']) : null;
3085
3086
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]) &&
3087
            ONE_PER_PAGE == $this->type
3088
        ) {
3089
            $urlTitle = get_lang('Proceed with the test');
3090
            if ($questionNum == count($this->questionList)) {
3091
                $urlTitle = get_lang('End test');
3092
            }
3093
3094
            $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit_modal.php?'.api_get_cidreq();
3095
            $url .= '&'.http_build_query([
3096
                'learnpath_id' => $safe_lp_id,
3097
                'learnpath_item_id' => $safe_lp_item_id,
3098
                'learnpath_item_view_id' => $safe_lp_item_view_id,
3099
                'hotspot' => $hotspotGet,
3100
                'nbrQuestions' => $nbrQuestions,
3101
                'num' => $questionNum,
3102
                'exerciseType' => $this->type,
3103
                'exerciseId' => $this->getId(),
3104
                'reminder' => empty($myRemindList) ? null : 2,
3105
            ]);
3106
3107
            $params = [
3108
                'class' => 'ajax btn btn-default no-close-button',
3109
                'data-title' => Security::remove_XSS(get_lang('Comment')),
3110
                'data-size' => 'md',
3111
                'id' => "button_$question_id",
3112
            ];
3113
3114
            if (EXERCISE_FEEDBACK_TYPE_POPUP === $this->getFeedbackType()) {
3115
                //$params['data-block-div-after-closing'] = "question_div_$question_id";
3116
                $params['data-block-closing'] = 'true';
3117
                $params['class'] .= ' no-header ';
3118
            }
3119
3120
            $html .= Display::url($urlTitle, $url, $params);
3121
            $html .= '<br />';
3122
            // User
3123
            return $html;
3124
        }
3125
3126
        if (!api_is_allowed_to_session_edit()) {
3127
            return '';
3128
        }
3129
3130
        $isReviewingAnswers = isset($_REQUEST['reminder']) && 2 == $_REQUEST['reminder'];
3131
3132
        // User
3133
                $endReminderValue = false;
3134
        if (!empty($myRemindList) && $isReviewingAnswers) {
3135
                    $endValue = end($myRemindList);
3136
                    if ($endValue == $question_id) {
3137
                        $endReminderValue = true;
3138
                    }
3139
                }
3140
        if ($this->type == ALL_ON_ONE_PAGE || $nbrQuestions == $questionNum || $endReminderValue) {
3141
                    if ($this->review_answers) {
3142
                $label = get_lang('ReviewQuestions');
3143
                        $class = 'btn btn-success';
3144
                    } else {
3145
                $label = get_lang('EndTest');
3146
                        $class = 'btn btn-warning';
3147
                    }
3148
                } else {
3149
            $label = get_lang('NextQuestion');
3150
                    $class = 'btn btn-primary';
3151
                }
3152
                // used to select it with jquery
3153
                $class .= ' question-validate-btn';
3154
        if ($this->type == ONE_PER_PAGE) {
3155
            if ($questionNum != 1 && $this->showPreviousButton()) {
3156
                            $prev_question = $questionNum - 2;
3157
                            $showPreview = true;
3158
                if (!empty($myRemindList) && $isReviewingAnswers) {
3159
                                $beforeId = null;
3160
                                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...
3161
                                    if (isset($myRemindList[$i]) && $myRemindList[$i] == $question_id) {
3162
                                        $beforeId = isset($myRemindList[$i - 1]) ? $myRemindList[$i - 1] : null;
3163
3164
                                        break;
3165
                                    }
3166
                                }
3167
3168
                                if (empty($beforeId)) {
3169
                                    $showPreview = false;
3170
                                } else {
3171
                                    $num = 0;
3172
                                    foreach ($this->questionList as $originalQuestionId) {
3173
                                        if ($originalQuestionId == $beforeId) {
3174
                                            break;
3175
                                        }
3176
                                        $num++;
3177
                                    }
3178
                                    $prev_question = $num;
3179
                                }
3180
                            }
3181
3182
                            if ($showPreview && 0 === $this->getPreventBackwards()) {
3183
                                $buttonList[] = Display::button(
3184
                                    'previous_question_and_save',
3185
                                    get_lang('Previous question'),
3186
                                    [
3187
                                        'type' => 'button',
3188
                                        'class' => 'btn btn-default',
3189
                                        'data-prev' => $prev_question,
3190
                                        'data-question' => $question_id,
3191
                                    ]
3192
                                );
3193
                            }
3194
                        }
3195
3196
                    // Next question
3197
                    if (!empty($questions_in_media)) {
3198
                        $buttonList[] = Display::button(
3199
                            'save_question_list',
3200
                            $label,
3201
                            [
3202
                                'type' => 'button',
3203
                                'class' => $class,
3204
                                'data-list' => implode(',', $questions_in_media),
3205
                            ]
3206
                        );
3207
                    } else {
3208
                        $buttonList[] = Display::button(
3209
                            'save_now',
3210
                            $label,
3211
                            ['type' => 'button', 'class' => $class, 'data-question' => $question_id]
3212
                        );
3213
                    }
3214
            $buttonList[] = '<span id="save_for_now_'.$question_id.'" class="exercise_save_mini_message"></span>';
3215
3216
            $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3217
3218
            return $html;
3219
        }
3220
3221
        if ($this->review_answers) {
3222
            $all_label = get_lang('Review selected questions');
3223
            $class = 'btn btn-success';
3224
        } else {
3225
            $all_label = get_lang('End test');
3226
            $class = 'btn btn-warning';
3227
        }
3228
        // used to select it with jquery
3229
        $class .= ' question-validate-btn';
3230
        $buttonList[] = Display::button(
3231
            'validate_all',
3232
            $all_label,
3233
            ['type' => 'button', 'class' => $class]
3234
        );
3235
        $buttonList[] = Display::span(null, ['id' => 'save_all_response']);
3236
        $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3237
3238
        return $html;
3239
    }
3240
3241
    /**
3242
     * @param int    $timeLeft in seconds
3243
     * @param string $url
3244
     *
3245
     * @return string
3246
     */
3247
    public function showSimpleTimeControl($timeLeft, $url = '')
3248
    {
3249
        $timeLeft = (int) $timeLeft;
3250
3251
        return "<script>
3252
            function openClockWarning() {
3253
                $('#clock_warning').dialog({
3254
                    modal:true,
3255
                    height:320,
3256
                    width:550,
3257
                    closeOnEscape: false,
3258
                    resizable: false,
3259
                    buttons: {
3260
                        '".addslashes(get_lang('Close'))."': function() {
3261
                            $('#clock_warning').dialog('close');
3262
                        }
3263
                    },
3264
                    close: function() {
3265
                        window.location.href = '$url';
3266
                    }
3267
                });
3268
                $('#clock_warning').dialog('open');
3269
                $('#counter_to_redirect').epiclock({
3270
                    mode: $.epiclock.modes.countdown,
3271
                    offset: {seconds: 5},
3272
                    format: 's'
3273
                }).bind('timer', function () {
3274
                    window.location.href = '$url';
3275
                });
3276
            }
3277
3278
            function onExpiredTimeExercise() {
3279
                $('#wrapper-clock').hide();
3280
                $('#expired-message-id').show();
3281
                // Fixes bug #5263
3282
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3283
                openClockWarning();
3284
            }
3285
3286
			$(function() {
3287
				// time in seconds when using minutes there are some seconds lost
3288
                var time_left = parseInt(".$timeLeft.");
3289
                $('#exercise_clock_warning').epiclock({
3290
                    mode: $.epiclock.modes.countdown,
3291
                    offset: {seconds: time_left},
3292
                    format: 'x:i:s',
3293
                    renderer: 'minute'
3294
                }).bind('timer', function () {
3295
                    onExpiredTimeExercise();
3296
                });
3297
	       		$('#submit_save').click(function () {});
3298
	        });
3299
	    </script>";
3300
    }
3301
3302
    /**
3303
     * So the time control will work.
3304
     *
3305
     * @param int $timeLeft
3306
     *
3307
     * @return string
3308
     */
3309
    public function showTimeControlJS($timeLeft)
3310
    {
3311
        $timeLeft = (int) $timeLeft;
3312
        $script = 'redirectExerciseToResult();';
3313
        if (ALL_ON_ONE_PAGE == $this->type) {
3314
            $script = "save_now_all('validate');";
3315
        } elseif (ONE_PER_PAGE == $this->type) {
3316
            $script = 'window.quizTimeEnding = true;
3317
                $(\'[name="save_now"]\').trigger(\'click\');';
3318
        }
3319
3320
        return "<script>
3321
            function openClockWarning() {
3322
                $('#clock_warning').dialog({
3323
                    modal:true,
3324
                    height:320,
3325
                    width:550,
3326
                    closeOnEscape: false,
3327
                    resizable: false,
3328
                    buttons: {
3329
                        '".addslashes(get_lang('End test'))."': function() {
3330
                            $('#clock_warning').dialog('close');
3331
                        }
3332
                    },
3333
                    close: function() {
3334
                        send_form();
3335
                    }
3336
                });
3337
3338
                $('#clock_warning').dialog('open');
3339
                $('#counter_to_redirect').epiclock({
3340
                    mode: $.epiclock.modes.countdown,
3341
                    offset: {seconds: 5},
3342
                    format: 's'
3343
                }).bind('timer', function () {
3344
                    send_form();
3345
                });
3346
            }
3347
3348
            function send_form() {
3349
                if ($('#exercise_form').length) {
3350
                    $script
3351
                } else {
3352
                    // In exercise_reminder.php
3353
                    final_submit();
3354
                }
3355
            }
3356
3357
            function onExpiredTimeExercise() {
3358
                $('#wrapper-clock').hide();
3359
                $('#expired-message-id').show();
3360
                // Fixes bug #5263
3361
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3362
                openClockWarning();
3363
            }
3364
3365
			$(function() {
3366
				// time in seconds when using minutes there are some seconds lost
3367
                var time_left = parseInt(".$timeLeft.");
3368
                $('#exercise_clock_warning').epiclock({
3369
                    mode: $.epiclock.modes.countdown,
3370
                    offset: {seconds: time_left},
3371
                    format: 'x:C:s',
3372
                    renderer: 'minute'
3373
                }).bind('timer', function () {
3374
                    onExpiredTimeExercise();
3375
                });
3376
	       		$('#submit_save').click(function () {});
3377
	        });
3378
	    </script>";
3379
    }
3380
3381
    /**
3382
     * This function was originally found in the exercise_show.php.
3383
     *
3384
     * @param int    $exeId
3385
     * @param int    $questionId
3386
     * @param mixed  $choice                                    the user-selected option
3387
     * @param string $from                                      function is called from 'exercise_show' or
3388
     *                                                          'exercise_result'
3389
     * @param array  $exerciseResultCoordinates                 the hotspot coordinates $hotspot[$question_id] =
3390
     *                                                          coordinates
3391
     * @param bool   $save_results                              save results in the DB or just show the response
3392
     * @param bool   $from_database                             gets information from DB or from the current selection
3393
     * @param bool   $show_result                               show results or not
3394
     * @param int    $propagate_neg
3395
     * @param array  $hotspot_delineation_result
3396
     * @param bool   $showTotalScoreAndUserChoicesInLastAttempt
3397
     * @param bool   $updateResults
3398
     * @param bool   $showHotSpotDelineationTable
3399
     *
3400
     * @todo    reduce parameters of this function
3401
     *
3402
     * @return string html code
3403
     */
3404
    public function manage_answer(
3405
        $exeId,
3406
        $questionId,
3407
        $choice,
3408
        $from = 'exercise_show',
3409
        $exerciseResultCoordinates = [],
3410
        $save_results = true,
3411
        $from_database = false,
3412
        $show_result = true,
3413
        $propagate_neg = 0,
3414
        $hotspot_delineation_result = [],
3415
        $showTotalScoreAndUserChoicesInLastAttempt = true,
3416
        $updateResults = false,
3417
        $showHotSpotDelineationTable = false
3418
    ) {
3419
        $debug = false;
3420
        //needed in order to use in the exercise_attempt() for the time
3421
        global $learnpath_id, $learnpath_item_id;
3422
        require_once api_get_path(LIBRARY_PATH).'geometry.lib.php';
3423
        $em = Database::getManager();
3424
        $feedback_type = $this->getFeedbackType();
3425
        $results_disabled = $this->selectResultsDisabled();
3426
3427
        if ($debug) {
3428
            error_log('<------ manage_answer ------> ');
3429
            error_log('exe_id: '.$exeId);
3430
            error_log('$from:  '.$from);
3431
            error_log('$saved_results: '.(int) $saved_results);
3432
            error_log('$from_database: '.(int) $from_database);
3433
            error_log('$show_result: '.(int) $show_result);
3434
            error_log('$propagate_neg: '.$propagate_neg);
3435
            error_log('$exerciseResultCoordinates: '.print_r($exerciseResultCoordinates, 1));
3436
            error_log('$hotspot_delineation_result: '.print_r($hotspot_delineation_result, 1));
3437
            error_log('$learnpath_id: '.$learnpath_id);
3438
            error_log('$learnpath_item_id: '.$learnpath_item_id);
3439
            error_log('$choice: '.print_r($choice, 1));
3440
        }
3441
3442
        $final_overlap = 0;
3443
        $final_missing = 0;
3444
        $final_excess = 0;
3445
        $overlap_color = 0;
3446
        $missing_color = 0;
3447
        $excess_color = 0;
3448
        $threadhold1 = 0;
3449
        $threadhold2 = 0;
3450
        $threadhold3 = 0;
3451
        $arrques = null;
3452
        $arrans = null;
3453
        $studentChoice = null;
3454
        $expectedAnswer = '';
3455
        $calculatedChoice = '';
3456
        $calculatedStatus = '';
3457
        $questionId = (int) $questionId;
3458
        $exeId = (int) $exeId;
3459
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3460
        $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
3461
        $studentChoiceDegree = null;
3462
3463
        // Creates a temporary Question object
3464
        $course_id = $this->course_id;
3465
        $objQuestionTmp = Question::read($questionId, $this->course);
3466
3467
        if (false === $objQuestionTmp) {
3468
            return false;
3469
        }
3470
3471
        $questionName = $objQuestionTmp->selectTitle();
3472
        $questionWeighting = $objQuestionTmp->selectWeighting();
3473
        $answerType = $objQuestionTmp->selectType();
3474
        $quesId = $objQuestionTmp->selectId();
3475
        $extra = $objQuestionTmp->extra;
3476
        $next = 1; //not for now
3477
        $totalWeighting = 0;
3478
        $totalScore = 0;
3479
3480
        // Extra information of the question
3481
        if ((
3482
            MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
3483
            MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
3484
            )
3485
            && !empty($extra)
3486
        ) {
3487
            $extra = explode(':', $extra);
3488
            if ($debug) {
3489
                error_log(print_r($extra, 1));
3490
            }
3491
            // Fixes problems with negatives values using intval
3492
            $true_score = (float) (trim($extra[0]));
3493
            $false_score = (float) (trim($extra[1]));
3494
            $doubt_score = (float) (trim($extra[2]));
3495
        }
3496
3497
        // Construction of the Answer object
3498
        $objAnswerTmp = new Answer($questionId, $course_id);
3499
        $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
3500
3501
        if ($debug) {
3502
            error_log('Count of answers: '.$nbrAnswers);
3503
            error_log('$answerType: '.$answerType);
3504
        }
3505
3506
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
3507
            $choiceTmp = $choice;
3508
            $choice = isset($choiceTmp['choice']) ? $choiceTmp['choice'] : '';
3509
            $choiceDegreeCertainty = isset($choiceTmp['choiceDegreeCertainty']) ? $choiceTmp['choiceDegreeCertainty'] : '';
3510
        }
3511
3512
        if (FREE_ANSWER == $answerType ||
3513
            ORAL_EXPRESSION == $answerType ||
3514
            CALCULATED_ANSWER == $answerType ||
3515
            ANNOTATION == $answerType
3516
        ) {
3517
            $nbrAnswers = 1;
3518
        }
3519
3520
        $generatedFile = '';
3521
        if (ORAL_EXPRESSION == $answerType) {
3522
            $exe_info = Event::get_exercise_results_by_attempt($exeId);
3523
            $exe_info = isset($exe_info[$exeId]) ? $exe_info[$exeId] : null;
3524
3525
            $objQuestionTmp->initFile(
3526
                api_get_session_id(),
3527
                isset($exe_info['exe_user_id']) ? $exe_info['exe_user_id'] : api_get_user_id(),
3528
                isset($exe_info['exe_exo_id']) ? $exe_info['exe_exo_id'] : $this->getId(),
3529
                isset($exe_info['exe_id']) ? $exe_info['exe_id'] : $exeId
3530
            );
3531
3532
            // Probably this attempt came in an exercise all question by page
3533
            if (0 == $feedback_type) {
3534
                $objQuestionTmp->replaceWithRealExe($exeId);
3535
            }
3536
            $generatedFile = $objQuestionTmp->getFileUrl();
3537
        }
3538
3539
        $user_answer = '';
3540
        // Get answer list for matching
3541
        $sql = "SELECT id_auto, id, answer
3542
                FROM $table_ans
3543
                WHERE c_id = $course_id AND question_id = $questionId";
3544
        $res_answer = Database::query($sql);
3545
3546
        $answerMatching = [];
3547
        while ($real_answer = Database::fetch_array($res_answer)) {
3548
            $answerMatching[$real_answer['id_auto']] = $real_answer['answer'];
3549
        }
3550
3551
        $real_answers = [];
3552
        $quiz_question_options = Question::readQuestionOption(
3553
            $questionId,
3554
            $course_id
3555
        );
3556
3557
        $organs_at_risk_hit = 0;
3558
        $questionScore = 0;
3559
        $orderedHotSpots = [];
3560
        if (HOT_SPOT == $answerType || ANNOTATION == $answerType) {
3561
            $orderedHotSpots = $em->getRepository('ChamiloCoreBundle:TrackEHotspot')->findBy(
3562
                [
3563
                    'hotspotQuestionId' => $questionId,
3564
                    'course' => $course_id,
3565
                    'hotspotExeId' => $exeId,
3566
                ],
3567
                ['hotspotAnswerId' => 'ASC']
3568
            );
3569
        }
3570
3571
        if ($debug) {
3572
            error_log('-- Start answer loop --');
3573
        }
3574
3575
        $answerDestination = null;
3576
        $userAnsweredQuestion = false;
3577
        $correctAnswerId = [];
3578
        for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
3579
            $answer = $objAnswerTmp->selectAnswer($answerId);
3580
            $answerComment = $objAnswerTmp->selectComment($answerId);
3581
            $answerCorrect = $objAnswerTmp->isCorrect($answerId);
3582
            $answerWeighting = (float) $objAnswerTmp->selectWeighting($answerId);
3583
            $answerAutoId = $objAnswerTmp->selectAutoId($answerId);
3584
            $answerIid = isset($objAnswerTmp->iid[$answerId]) ? (int) $objAnswerTmp->iid[$answerId] : 0;
3585
3586
            if ($debug) {
3587
                error_log("c_quiz_answer.id_auto: $answerAutoId ");
3588
                error_log("Answer marked as correct in db (0/1)?: $answerCorrect ");
3589
            }
3590
3591
            // Delineation
3592
            $delineation_cord = $objAnswerTmp->selectHotspotCoordinates(1);
3593
            $answer_delineation_destination = $objAnswerTmp->selectDestination(1);
3594
3595
            switch ($answerType) {
3596
                case UNIQUE_ANSWER:
3597
                case UNIQUE_ANSWER_IMAGE:
3598
                case UNIQUE_ANSWER_NO_OPTION:
3599
                case READING_COMPREHENSION:
3600
                    if ($from_database) {
3601
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3602
                                WHERE
3603
                                    exe_id = $exeId AND
3604
                                    question_id = $questionId";
3605
                        $result = Database::query($sql);
3606
                        $choice = Database::result($result, 0, 'answer');
3607
3608
                        if (false === $userAnsweredQuestion) {
3609
                            $userAnsweredQuestion = !empty($choice);
3610
                        }
3611
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3612
                        if ($studentChoice) {
3613
                            $questionScore += $answerWeighting;
3614
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3615
                            $correctAnswerId[] = $answerId;
3616
                        }
3617
                    } else {
3618
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3619
                        if ($studentChoice) {
3620
                            $questionScore += $answerWeighting;
3621
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3622
                            $correctAnswerId[] = $answerId;
3623
                        }
3624
                    }
3625
3626
                    break;
3627
                case MULTIPLE_ANSWER_TRUE_FALSE:
3628
                    if ($from_database) {
3629
                        $choice = [];
3630
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3631
                                WHERE
3632
                                    exe_id = $exeId AND
3633
                                    question_id = ".$questionId;
3634
3635
                        $result = Database::query($sql);
3636
                        while ($row = Database::fetch_array($result)) {
3637
                            $values = explode(':', $row['answer']);
3638
                            $my_answer_id = isset($values[0]) ? $values[0] : '';
3639
                            $option = isset($values[1]) ? $values[1] : '';
3640
                            $choice[$my_answer_id] = $option;
3641
                        }
3642
                    }
3643
3644
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3645
                    if (!empty($studentChoice)) {
3646
                        $correctAnswerId[] = $answerAutoId;
3647
                        if ($studentChoice == $answerCorrect) {
3648
                            $questionScore += $true_score;
3649
                        } else {
3650
                            if ("Don't know" == $quiz_question_options[$studentChoice]['name'] ||
3651
                                'DoubtScore' == $quiz_question_options[$studentChoice]['name']
3652
                            ) {
3653
                                $questionScore += $doubt_score;
3654
                            } else {
3655
                                $questionScore += $false_score;
3656
                            }
3657
                        }
3658
                    } else {
3659
                        // If no result then the user just hit don't know
3660
                        $studentChoice = 3;
3661
                        $questionScore += $doubt_score;
3662
                    }
3663
                    $totalScore = $questionScore;
3664
3665
                    break;
3666
                case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
3667
                    if ($from_database) {
3668
                        $choice = [];
3669
                        $choiceDegreeCertainty = [];
3670
                        $sql = "SELECT answer
3671
                            FROM $TBL_TRACK_ATTEMPT
3672
                            WHERE
3673
                            exe_id = $exeId AND question_id = $questionId";
3674
3675
                        $result = Database::query($sql);
3676
                        while ($row = Database::fetch_array($result)) {
3677
                            $ind = $row['answer'];
3678
                            $values = explode(':', $ind);
3679
                            $myAnswerId = $values[0];
3680
                            $option = $values[1];
3681
                            $percent = $values[2];
3682
                            $choice[$myAnswerId] = $option;
3683
                            $choiceDegreeCertainty[$myAnswerId] = $percent;
3684
                        }
3685
                    }
3686
3687
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3688
                    $studentChoiceDegree = isset($choiceDegreeCertainty[$answerAutoId]) ? $choiceDegreeCertainty[$answerAutoId] : null;
3689
3690
                    // student score update
3691
                    if (!empty($studentChoice)) {
3692
                        if ($studentChoice == $answerCorrect) {
3693
                            // correct answer and student is Unsure or PrettySur
3694
                            if (isset($quiz_question_options[$studentChoiceDegree]) &&
3695
                                $quiz_question_options[$studentChoiceDegree]['position'] >= 3 &&
3696
                                $quiz_question_options[$studentChoiceDegree]['position'] < 9
3697
                            ) {
3698
                                $questionScore += $true_score;
3699
                            } else {
3700
                                // student ignore correct answer
3701
                                $questionScore += $doubt_score;
3702
                            }
3703
                        } else {
3704
                            // false answer and student is Unsure or PrettySur
3705
                            if ($quiz_question_options[$studentChoiceDegree]['position'] >= 3
3706
                                && $quiz_question_options[$studentChoiceDegree]['position'] < 9) {
3707
                                $questionScore += $false_score;
3708
                            } else {
3709
                                // student ignore correct answer
3710
                                $questionScore += $doubt_score;
3711
                            }
3712
                        }
3713
                    }
3714
                    $totalScore = $questionScore;
3715
3716
                    break;
3717
                case MULTIPLE_ANSWER:
3718
                    if ($from_database) {
3719
                        $choice = [];
3720
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3721
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3722
                        $resultans = Database::query($sql);
3723
                        while ($row = Database::fetch_array($resultans)) {
3724
                            $choice[$row['answer']] = 1;
3725
                        }
3726
3727
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3728
                        $real_answers[$answerId] = (bool) $studentChoice;
3729
3730
                        if ($studentChoice) {
3731
                            $questionScore += $answerWeighting;
3732
                        }
3733
                    } else {
3734
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3735
                        $real_answers[$answerId] = (bool) $studentChoice;
3736
3737
                        if (isset($studentChoice)) {
3738
                            $correctAnswerId[] = $answerAutoId;
3739
                            $questionScore += $answerWeighting;
3740
                        }
3741
                    }
3742
                    $totalScore += $answerWeighting;
3743
3744
                    break;
3745
                case GLOBAL_MULTIPLE_ANSWER:
3746
                    if ($from_database) {
3747
                        $choice = [];
3748
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3749
                                WHERE exe_id = $exeId AND question_id = $questionId ";
3750
                        $resultans = Database::query($sql);
3751
                        while ($row = Database::fetch_array($resultans)) {
3752
                            $choice[$row['answer']] = 1;
3753
                        }
3754
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3755
                        $real_answers[$answerId] = (bool) $studentChoice;
3756
                        if ($studentChoice) {
3757
                            $questionScore += $answerWeighting;
3758
                        }
3759
                    } else {
3760
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3761
                        if (isset($studentChoice)) {
3762
                            $questionScore += $answerWeighting;
3763
                        }
3764
                        $real_answers[$answerId] = (bool) $studentChoice;
3765
                    }
3766
                    $totalScore += $answerWeighting;
3767
                    if ($debug) {
3768
                        error_log("studentChoice: $studentChoice");
3769
                    }
3770
3771
                    break;
3772
                case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
3773
                    if ($from_database) {
3774
                        $choice = [];
3775
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3776
                                WHERE exe_id = $exeId AND question_id = $questionId";
3777
                        $resultans = Database::query($sql);
3778
                        while ($row = Database::fetch_array($resultans)) {
3779
                            $result = explode(':', $row['answer']);
3780
                            if (isset($result[0])) {
3781
                                $my_answer_id = isset($result[0]) ? $result[0] : '';
3782
                                $option = isset($result[1]) ? $result[1] : '';
3783
                                $choice[$my_answer_id] = $option;
3784
                            }
3785
                        }
3786
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3787
3788
                        $real_answers[$answerId] = false;
3789
                        if ($answerCorrect == $studentChoice) {
3790
                            $real_answers[$answerId] = true;
3791
                        }
3792
                    } else {
3793
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
3794
                        $real_answers[$answerId] = false;
3795
                        if ($answerCorrect == $studentChoice) {
3796
                            $real_answers[$answerId] = true;
3797
                        }
3798
                    }
3799
3800
                    break;
3801
                case MULTIPLE_ANSWER_COMBINATION:
3802
                    if ($from_database) {
3803
                        $choice = [];
3804
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3805
                                WHERE exe_id = $exeId AND question_id = $questionId";
3806
                        $resultans = Database::query($sql);
3807
                        while ($row = Database::fetch_array($resultans)) {
3808
                            $choice[$row['answer']] = 1;
3809
                        }
3810
3811
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3812
                        if (1 == $answerCorrect) {
3813
                            $real_answers[$answerId] = false;
3814
                            if ($studentChoice) {
3815
                                $real_answers[$answerId] = true;
3816
                            }
3817
                        } else {
3818
                            $real_answers[$answerId] = true;
3819
                            if ($studentChoice) {
3820
                                $real_answers[$answerId] = false;
3821
                            }
3822
                        }
3823
                    } else {
3824
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3825
                        if (1 == $answerCorrect) {
3826
                            $real_answers[$answerId] = false;
3827
                            if ($studentChoice) {
3828
                                $real_answers[$answerId] = true;
3829
                            }
3830
                        } else {
3831
                            $real_answers[$answerId] = true;
3832
                            if ($studentChoice) {
3833
                                $real_answers[$answerId] = false;
3834
                            }
3835
                        }
3836
                    }
3837
3838
                    break;
3839
                case FILL_IN_BLANKS:
3840
                    $str = '';
3841
                    $answerFromDatabase = '';
3842
                    if ($from_database) {
3843
                        $sql = "SELECT answer
3844
                                FROM $TBL_TRACK_ATTEMPT
3845
                                WHERE
3846
                                    exe_id = $exeId AND
3847
                                    question_id= $questionId ";
3848
                        $result = Database::query($sql);
3849
                        $str = $answerFromDatabase = Database::result($result, 0, 'answer');
3850
                    }
3851
3852
                    // if ($saved_results == false && strpos($answerFromDatabase, 'font color') !== false) {
3853
                    if (false) {
3854
                        // the question is encoded like this
3855
                        // [A] B [C] D [E] F::10,10,10@1
3856
                        // number 1 before the "@" means that is a switchable fill in blank question
3857
                        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
3858
                        // means that is a normal fill blank question
3859
                        // first we explode the "::"
3860
                        $pre_array = explode('::', $answer);
3861
3862
                        // is switchable fill blank or not
3863
                        $last = count($pre_array) - 1;
3864
                        $is_set_switchable = explode('@', $pre_array[$last]);
3865
                        $switchable_answer_set = false;
3866
                        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
3867
                            $switchable_answer_set = true;
3868
                        }
3869
                        $answer = '';
3870
                        for ($k = 0; $k < $last; $k++) {
3871
                            $answer .= $pre_array[$k];
3872
                        }
3873
                        // splits weightings that are joined with a comma
3874
                        $answerWeighting = explode(',', $is_set_switchable[0]);
3875
                        // we save the answer because it will be modified
3876
                        $temp = $answer;
3877
                        $answer = '';
3878
                        $j = 0;
3879
                        //initialise answer tags
3880
                        $user_tags = $correct_tags = $real_text = [];
3881
                        // the loop will stop at the end of the text
3882
                        while (1) {
3883
                            // quits the loop if there are no more blanks (detect '[')
3884
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
3885
                                // adds the end of the text
3886
                                $answer = $temp;
3887
                                $real_text[] = $answer;
3888
3889
                                break; //no more "blanks", quit the loop
3890
                            }
3891
                            // adds the piece of text that is before the blank
3892
                            //and ends with '[' into a general storage array
3893
                            $real_text[] = api_substr($temp, 0, $pos + 1);
3894
                            $answer .= api_substr($temp, 0, $pos + 1);
3895
                            //take the string remaining (after the last "[" we found)
3896
                            $temp = api_substr($temp, $pos + 1);
3897
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
3898
                            if (false === ($pos = api_strpos($temp, ']'))) {
3899
                                // adds the end of the text
3900
                                $answer .= $temp;
3901
3902
                                break;
3903
                            }
3904
                            if ($from_database) {
3905
                                $str = $answerFromDatabase;
3906
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
3907
                                $str = str_replace('\r\n', '', $str);
3908
3909
                                $choice = $arr[1];
3910
                                if (isset($choice[$j])) {
3911
                                    $tmp = api_strrpos($choice[$j], ' / ');
3912
                                    $choice[$j] = api_substr($choice[$j], 0, $tmp);
3913
                                    $choice[$j] = trim($choice[$j]);
3914
                                    // Needed to let characters ' and " to work as part of an answer
3915
                                    $choice[$j] = stripslashes($choice[$j]);
3916
                                } else {
3917
                                    $choice[$j] = null;
3918
                                }
3919
                            } else {
3920
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
3921
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
3922
                            }
3923
3924
                            $user_tags[] = $choice[$j];
3925
                            // Put the contents of the [] answer tag into correct_tags[]
3926
                            $correct_tags[] = api_substr($temp, 0, $pos);
3927
                            $j++;
3928
                            $temp = api_substr($temp, $pos + 1);
3929
                        }
3930
                        $answer = '';
3931
                        $real_correct_tags = $correct_tags;
3932
                        $chosen_list = [];
3933
3934
                        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...
3935
                            if (0 == $i) {
3936
                                $answer .= $real_text[0];
3937
                            }
3938
                            if (!$switchable_answer_set) {
3939
                                // Needed to parse ' and " characters
3940
                                $user_tags[$i] = stripslashes($user_tags[$i]);
3941
                                if ($correct_tags[$i] == $user_tags[$i]) {
3942
                                    // gives the related weighting to the student
3943
                                    $questionScore += $answerWeighting[$i];
3944
                                    // increments total score
3945
                                    $totalScore += $answerWeighting[$i];
3946
                                    // adds the word in green at the end of the string
3947
                                    $answer .= $correct_tags[$i];
3948
                                } elseif (!empty($user_tags[$i])) {
3949
                                    // else if the word entered by the student IS NOT the same as
3950
                                    // the one defined by the professor
3951
                                    // adds the word in red at the end of the string, and strikes it
3952
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
3953
                                } else {
3954
                                    // adds a tabulation if no word has been typed by the student
3955
                                    $answer .= ''; // remove &nbsp; that causes issue
3956
                                }
3957
                            } else {
3958
                                // switchable fill in the blanks
3959
                                if (in_array($user_tags[$i], $correct_tags)) {
3960
                                    $chosen_list[] = $user_tags[$i];
3961
                                    $correct_tags = array_diff($correct_tags, $chosen_list);
3962
                                    // gives the related weighting to the student
3963
                                    $questionScore += $answerWeighting[$i];
3964
                                    // increments total score
3965
                                    $totalScore += $answerWeighting[$i];
3966
                                    // adds the word in green at the end of the string
3967
                                    $answer .= $user_tags[$i];
3968
                                } elseif (!empty($user_tags[$i])) {
3969
                                    // else if the word entered by the student IS NOT the same
3970
                                    // as the one defined by the professor
3971
                                    // adds the word in red at the end of the string, and strikes it
3972
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
3973
                                } else {
3974
                                    // adds a tabulation if no word has been typed by the student
3975
                                    $answer .= ''; // remove &nbsp; that causes issue
3976
                                }
3977
                            }
3978
3979
                            // adds the correct word, followed by ] to close the blank
3980
                            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
3981
                            if (isset($real_text[$i + 1])) {
3982
                                $answer .= $real_text[$i + 1];
3983
                            }
3984
                        }
3985
                    } else {
3986
                        // insert the student result in the track_e_attempt table, field answer
3987
                        // $answer is the answer like in the c_quiz_answer table for the question
3988
                        // student data are choice[]
3989
                        $listCorrectAnswers = FillBlanks::getAnswerInfo($answer);
3990
                        $switchableAnswerSet = $listCorrectAnswers['switchable'];
3991
                        $answerWeighting = $listCorrectAnswers['weighting'];
3992
                        // user choices is an array $choice
3993
3994
                        // get existing user data in n the BDD
3995
                        if ($from_database) {
3996
                            $listStudentResults = FillBlanks::getAnswerInfo(
3997
                                $answerFromDatabase,
3998
                                true
3999
                            );
4000
                            $choice = $listStudentResults['student_answer'];
4001
                        }
4002
4003
                        // loop other all blanks words
4004
                        if (!$switchableAnswerSet) {
4005
                            // not switchable answer, must be in the same place than teacher order
4006
                            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...
4007
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
4008
                                $correctAnswer = $listCorrectAnswers['words'][$i];
4009
4010
                                if ($debug) {
4011
                                    error_log("Student answer: $i");
4012
                                    error_log($studentAnswer);
4013
                                }
4014
4015
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4016
                                // Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
4017
                                // ENT_QUOTES is used in order to transform ' to &#039;
4018
                                if (!$from_database) {
4019
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4020
                                    if ($debug) {
4021
                                        error_log('Student answer cleaned:');
4022
                                        error_log($studentAnswer);
4023
                                    }
4024
                                }
4025
4026
                                $isAnswerCorrect = 0;
4027
                                if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
4028
                                    // gives the related weighting to the student
4029
                                    $questionScore += $answerWeighting[$i];
4030
                                    // increments total score
4031
                                    $totalScore += $answerWeighting[$i];
4032
                                    $isAnswerCorrect = 1;
4033
                                }
4034
                                if ($debug) {
4035
                                    error_log("isAnswerCorrect $i: $isAnswerCorrect");
4036
                                }
4037
4038
                                $studentAnswerToShow = $studentAnswer;
4039
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4040
                                if ($debug) {
4041
                                    error_log("Fill in blank type: $type");
4042
                                }
4043
                                if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4044
                                    $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4045
                                    if ('' != $studentAnswer) {
4046
                                        foreach ($listMenu as $item) {
4047
                                            if (sha1($item) == $studentAnswer) {
4048
                                                $studentAnswerToShow = $item;
4049
                                            }
4050
                                        }
4051
                                    }
4052
                                }
4053
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4054
                                $listCorrectAnswers['student_score'][$i] = $isAnswerCorrect;
4055
                            }
4056
                        } else {
4057
                            // switchable answer
4058
                            $listStudentAnswerTemp = $choice;
4059
                            $listTeacherAnswerTemp = $listCorrectAnswers['words'];
4060
4061
                            // for every teacher answer, check if there is a student answer
4062
                            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...
4063
                                $studentAnswer = trim($listStudentAnswerTemp[$i]);
4064
                                $studentAnswerToShow = $studentAnswer;
4065
4066
                                if (empty($studentAnswer)) {
4067
                                    continue;
4068
                                }
4069
4070
                                if ($debug) {
4071
                                    error_log("Student answer: $i");
4072
                                    error_log($studentAnswer);
4073
                                }
4074
4075
                                if (!$from_database) {
4076
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4077
                                    if ($debug) {
4078
                                        error_log("Student answer cleaned:");
4079
                                        error_log($studentAnswer);
4080
                                    }
4081
                                }
4082
4083
                                $found = false;
4084
                                for ($j = 0; $j < count($listTeacherAnswerTemp); $j++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

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

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

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4085
                                    $correctAnswer = $listTeacherAnswerTemp[$j];
4086
4087
                                    if (!$found) {
4088
                                        if (FillBlanks::isStudentAnswerGood(
4089
                                            $studentAnswer,
4090
                                            $correctAnswer,
4091
                                            $from_database
4092
                                        )) {
4093
                                            $questionScore += $answerWeighting[$i];
4094
                                            $totalScore += $answerWeighting[$i];
4095
                                            $listTeacherAnswerTemp[$j] = '';
4096
                                            $found = true;
4097
                                        }
4098
                                    }
4099
4100
                                    $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4101
                                    if (FillBlanks::FILL_THE_BLANK_MENU == $type) {
4102
                                        $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4103
                                        if (!empty($studentAnswer)) {
4104
                                            foreach ($listMenu as $key => $item) {
4105
                                                if ($key == $correctAnswer) {
4106
                                                    $studentAnswerToShow = $item;
4107
                                                    break;
4108
                                                }
4109
                                            }
4110
                                        }
4111
                                    }
4112
                                }
4113
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4114
                                $listCorrectAnswers['student_score'][$i] = $found ? 1 : 0;
4115
                            }
4116
                        }
4117
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
4118
                    }
4119
4120
                    break;
4121
                case CALCULATED_ANSWER:
4122
                    $calculatedAnswerList = Session::read('calculatedAnswerId');
4123
                    if (!empty($calculatedAnswerList)) {
4124
                        $answer = $objAnswerTmp->selectAnswer($calculatedAnswerList[$questionId]);
4125
                        $preArray = explode('@@', $answer);
4126
                        $last = count($preArray) - 1;
4127
                        $answer = '';
4128
                        for ($k = 0; $k < $last; $k++) {
4129
                            $answer .= $preArray[$k];
4130
                        }
4131
                        $answerWeighting = [$answerWeighting];
4132
                        // we save the answer because it will be modified
4133
                        $temp = $answer;
4134
                        $answer = '';
4135
                        $j = 0;
4136
                        // initialise answer tags
4137
                        $userTags = $correctTags = $realText = [];
4138
                        // the loop will stop at the end of the text
4139
                        while (1) {
4140
                            // quits the loop if there are no more blanks (detect '[')
4141
                            if (false == $temp || false === ($pos = api_strpos($temp, '['))) {
4142
                                // adds the end of the text
4143
                                $answer = $temp;
4144
                                $realText[] = $answer;
4145
4146
                                break; //no more "blanks", quit the loop
4147
                            }
4148
                            // adds the piece of text that is before the blank
4149
                            // and ends with '[' into a general storage array
4150
                            $realText[] = api_substr($temp, 0, $pos + 1);
4151
                            $answer .= api_substr($temp, 0, $pos + 1);
4152
                            // take the string remaining (after the last "[" we found)
4153
                            $temp = api_substr($temp, $pos + 1);
4154
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4155
                            if (false === ($pos = api_strpos($temp, ']'))) {
4156
                                // adds the end of the text
4157
                                $answer .= $temp;
4158
4159
                                break;
4160
                            }
4161
4162
                            if ($from_database) {
4163
                                $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4164
                                        WHERE
4165
                                            exe_id = $exeId AND
4166
                                            question_id = $questionId ";
4167
                                $result = Database::query($sql);
4168
                                $str = Database::result($result, 0, 'answer');
4169
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4170
                                $str = str_replace('\r\n', '', $str);
4171
                                $choice = $arr[1];
4172
                                if (isset($choice[$j])) {
4173
                                    $tmp = api_strrpos($choice[$j], ' / ');
4174
                                    if ($tmp) {
4175
                                        $choice[$j] = api_substr($choice[$j], 0, $tmp);
4176
                                    } else {
4177
                                        $tmp = ltrim($tmp, '[');
4178
                                        $tmp = rtrim($tmp, ']');
4179
                                    }
4180
                                    $choice[$j] = trim($choice[$j]);
4181
                                    // Needed to let characters ' and " to work as part of an answer
4182
                                    $choice[$j] = stripslashes($choice[$j]);
4183
                                } else {
4184
                                    $choice[$j] = null;
4185
                                }
4186
                            } else {
4187
                                // This value is the user input not escaped while correct answer is escaped by ckeditor
4188
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4189
                            }
4190
                            $userTags[] = $choice[$j];
4191
                            // put the contents of the [] answer tag into correct_tags[]
4192
                            $correctTags[] = api_substr($temp, 0, $pos);
4193
                            $j++;
4194
                            $temp = api_substr($temp, $pos + 1);
4195
                        }
4196
                        $answer = '';
4197
                        $realCorrectTags = $correctTags;
4198
                        $calculatedStatus = Display::label(get_lang('Incorrect'), 'danger');
4199
                        $expectedAnswer = '';
4200
                        $calculatedChoice = '';
4201
4202
                        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...
4203
                            if (0 == $i) {
4204
                                $answer .= $realText[0];
4205
                            }
4206
                            // Needed to parse ' and " characters
4207
                            $userTags[$i] = stripslashes($userTags[$i]);
4208
                            if ($correctTags[$i] == $userTags[$i]) {
4209
                                // gives the related weighting to the student
4210
                                $questionScore += $answerWeighting[$i];
4211
                                // increments total score
4212
                                $totalScore += $answerWeighting[$i];
4213
                                // adds the word in green at the end of the string
4214
                                $answer .= $correctTags[$i];
4215
                                $calculatedChoice = $correctTags[$i];
4216
                            } elseif (!empty($userTags[$i])) {
4217
                                // else if the word entered by the student IS NOT the same as
4218
                                // the one defined by the professor
4219
                                // adds the word in red at the end of the string, and strikes it
4220
                                $answer .= '<font color="red"><s>'.$userTags[$i].'</s></font>';
4221
                                $calculatedChoice = $userTags[$i];
4222
                            } else {
4223
                                // adds a tabulation if no word has been typed by the student
4224
                                $answer .= ''; // remove &nbsp; that causes issue
4225
                            }
4226
                            // adds the correct word, followed by ] to close the blank
4227
                            if (EXERCISE_FEEDBACK_TYPE_EXAM != $this->results_disabled) {
4228
                                $answer .= ' / <font color="green"><b>'.$realCorrectTags[$i].'</b></font>';
4229
                                $calculatedStatus = Display::label(get_lang('Correct'), 'success');
4230
                                $expectedAnswer = $realCorrectTags[$i];
4231
                            }
4232
                            $answer .= ']';
4233
                            if (isset($realText[$i + 1])) {
4234
                                $answer .= $realText[$i + 1];
4235
                            }
4236
                        }
4237
                    } else {
4238
                        if ($from_database) {
4239
                            $sql = "SELECT *
4240
                                    FROM $TBL_TRACK_ATTEMPT
4241
                                    WHERE
4242
                                        exe_id = $exeId AND
4243
                                        question_id = $questionId ";
4244
                            $result = Database::query($sql);
4245
                            $resultData = Database::fetch_array($result, 'ASSOC');
4246
                            $answer = $resultData['answer'];
4247
                            $questionScore = $resultData['marks'];
4248
                        }
4249
                    }
4250
4251
                    break;
4252
                case FREE_ANSWER:
4253
                    if ($from_database) {
4254
                        $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
4255
                                 WHERE
4256
                                    exe_id = $exeId AND
4257
                                    question_id= ".$questionId;
4258
                        $result = Database::query($sql);
4259
                        $data = Database::fetch_array($result);
4260
4261
                        $choice = $data['answer'];
4262
                        $choice = str_replace('\r\n', '', $choice);
4263
                        $choice = stripslashes($choice);
4264
                        $questionScore = $data['marks'];
4265
4266
                        if (-1 == $questionScore) {
4267
                            $totalScore += 0;
4268
                        } else {
4269
                            $totalScore += $questionScore;
4270
                        }
4271
                        if ('' == $questionScore) {
4272
                            $questionScore = 0;
4273
                        }
4274
                        $arrques = $questionName;
4275
                        $arrans = $choice;
4276
                    } else {
4277
                        $studentChoice = $choice;
4278
                        if ($studentChoice) {
4279
                            //Fixing negative puntation see #2193
4280
                            $questionScore = 0;
4281
                            $totalScore += 0;
4282
                        }
4283
                    }
4284
4285
                    break;
4286
                case ORAL_EXPRESSION:
4287
                    if ($from_database) {
4288
                        $query = "SELECT answer, marks
4289
                                  FROM $TBL_TRACK_ATTEMPT
4290
                                  WHERE
4291
                                        exe_id = $exeId AND
4292
                                        question_id = $questionId
4293
                                 ";
4294
                        $resq = Database::query($query);
4295
                        $row = Database::fetch_assoc($resq);
4296
                        $choice = $row['answer'];
4297
                        $choice = str_replace('\r\n', '', $choice);
4298
                        $choice = stripslashes($choice);
4299
                        $questionScore = $row['marks'];
4300
                        if (-1 == $questionScore) {
4301
                            $totalScore += 0;
4302
                        } else {
4303
                            $totalScore += $questionScore;
4304
                        }
4305
                        $arrques = $questionName;
4306
                        $arrans = $choice;
4307
                    } else {
4308
                        $studentChoice = $choice;
4309
                        if ($studentChoice) {
4310
                            //Fixing negative puntation see #2193
4311
                            $questionScore = 0;
4312
                            $totalScore += 0;
4313
                        }
4314
                    }
4315
4316
                    break;
4317
                case DRAGGABLE:
4318
                case MATCHING_DRAGGABLE:
4319
                case MATCHING:
4320
                    if ($from_database) {
4321
                        $sql = "SELECT id, answer, id_auto
4322
                                FROM $table_ans
4323
                                WHERE
4324
                                    c_id = $course_id AND
4325
                                    question_id = $questionId AND
4326
                                    correct = 0
4327
                                ";
4328
                        $result = Database::query($sql);
4329
                        // Getting the real answer
4330
                        $real_list = [];
4331
                        while ($realAnswer = Database::fetch_array($result)) {
4332
                            $real_list[$realAnswer['id_auto']] = $realAnswer['answer'];
4333
                        }
4334
4335
                        $sql = "SELECT id, answer, correct, id_auto, ponderation
4336
                                FROM $table_ans
4337
                                WHERE
4338
                                    c_id = $course_id AND
4339
                                    question_id = $questionId AND
4340
                                    correct <> 0
4341
                                ORDER BY id_auto";
4342
                        $result = Database::query($sql);
4343
                        $options = [];
4344
                        while ($row = Database::fetch_array($result, 'ASSOC')) {
4345
                            $options[] = $row;
4346
                        }
4347
4348
                        $questionScore = 0;
4349
                        $counterAnswer = 1;
4350
                        foreach ($options as $a_answers) {
4351
                            $i_answer_id = $a_answers['id']; //3
4352
                            $s_answer_label = $a_answers['answer']; // your daddy - your mother
4353
                            $i_answer_correct_answer = $a_answers['correct']; //1 - 2
4354
                            $i_answer_id_auto = $a_answers['id_auto']; // 3 - 4
4355
4356
                            $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4357
                                    WHERE
4358
                                        exe_id = '$exeId' AND
4359
                                        question_id = '$questionId' AND
4360
                                        position = '$i_answer_id_auto'";
4361
                            $result = Database::query($sql);
4362
                            $s_user_answer = 0;
4363
                            if (Database::num_rows($result) > 0) {
4364
                                //  rich - good looking
4365
                                $s_user_answer = Database::result($result, 0, 0);
4366
                            }
4367
                            $i_answerWeighting = $a_answers['ponderation'];
4368
                            $user_answer = '';
4369
                            $status = Display::label(get_lang('Incorrect'), 'danger');
4370
4371
                            if (!empty($s_user_answer)) {
4372
                                if (DRAGGABLE == $answerType) {
4373
                                    if ($s_user_answer == $i_answer_correct_answer) {
4374
                                        $questionScore += $i_answerWeighting;
4375
                                        $totalScore += $i_answerWeighting;
4376
                                        $user_answer = Display::label(get_lang('Correct'), 'success');
4377
                                        if ($this->showExpectedChoice()) {
4378
                                            $user_answer = $answerMatching[$i_answer_id_auto];
4379
                                        }
4380
                                        $status = Display::label(get_lang('Correct'), 'success');
4381
                                    } else {
4382
                                        $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4383
                                        if ($this->showExpectedChoice()) {
4384
                                            $data = $options[$real_list[$s_user_answer] - 1];
4385
                                            $user_answer = $data['answer'];
4386
                                        }
4387
                                    }
4388
                                } else {
4389
                                    if ($s_user_answer == $i_answer_correct_answer) {
4390
                                        $questionScore += $i_answerWeighting;
4391
                                        $totalScore += $i_answerWeighting;
4392
                                        $status = Display::label(get_lang('Correct'), 'success');
4393
4394
                                        // Try with id
4395
                                        if (isset($real_list[$i_answer_id])) {
4396
                                            $user_answer = Display::span(
4397
                                                $real_list[$i_answer_id],
4398
                                                ['style' => 'color: #008000; font-weight: bold;']
4399
                                            );
4400
                                        }
4401
4402
                                        // Try with $i_answer_id_auto
4403
                                        if (empty($user_answer)) {
4404
                                            if (isset($real_list[$i_answer_id_auto])) {
4405
                                                $user_answer = Display::span(
4406
                                                    $real_list[$i_answer_id_auto],
4407
                                                    ['style' => 'color: #008000; font-weight: bold;']
4408
                                                );
4409
                                            }
4410
                                        }
4411
4412
                                        if (isset($real_list[$i_answer_correct_answer])) {
4413
                                            $user_answer = Display::span(
4414
                                                $real_list[$i_answer_correct_answer],
4415
                                                ['style' => 'color: #008000; font-weight: bold;']
4416
                                            );
4417
                                        }
4418
                                    } else {
4419
                                        $user_answer = Display::span(
4420
                                            $real_list[$s_user_answer],
4421
                                            ['style' => 'color: #FF0000; text-decoration: line-through;']
4422
                                        );
4423
                                        if ($this->showExpectedChoice()) {
4424
                                            if (isset($real_list[$s_user_answer])) {
4425
                                                $user_answer = Display::span($real_list[$s_user_answer]);
4426
                                            }
4427
                                        }
4428
                                    }
4429
                                }
4430
                            } elseif (DRAGGABLE == $answerType) {
4431
                                $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4432
                                if ($this->showExpectedChoice()) {
4433
                                    $user_answer = '';
4434
                                }
4435
                            } else {
4436
                                $user_answer = Display::span(
4437
                                    get_lang('Incorrect').' &nbsp;',
4438
                                    ['style' => 'color: #FF0000; text-decoration: line-through;']
4439
                                );
4440
                                if ($this->showExpectedChoice()) {
4441
                                    $user_answer = '';
4442
                                }
4443
                            }
4444
4445
                            if ($show_result) {
4446
                                if (false === $this->showExpectedChoice() &&
4447
                                    false === $showTotalScoreAndUserChoicesInLastAttempt
4448
                                ) {
4449
                                    $user_answer = '';
4450
                                }
4451
                                switch ($answerType) {
4452
                                    case MATCHING:
4453
                                    case MATCHING_DRAGGABLE:
4454
                                        echo '<tr>';
4455
                                        if (!in_array(
4456
                                            $this->results_disabled,
4457
                                            [
4458
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4459
                                            ]
4460
                                        )
4461
                                        ) {
4462
                                            echo '<td>'.$s_answer_label.'</td>';
4463
                                            echo '<td>'.$user_answer.'</td>';
4464
                                        } else {
4465
                                            echo '<td>'.$s_answer_label.'</td>';
4466
                                            $status = Display::label(get_lang('Correct'), 'success');
4467
                                        }
4468
4469
                                        if ($this->showExpectedChoice()) {
4470
                                            if ($this->showExpectedChoiceColumn()) {
4471
                                                echo '<td>';
4472
                                                if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4473
                                                    if (isset($real_list[$i_answer_correct_answer]) &&
4474
                                                        true == $showTotalScoreAndUserChoicesInLastAttempt
4475
                                                    ) {
4476
                                                        echo Display::span(
4477
                                                            $real_list[$i_answer_correct_answer]
4478
                                                        );
4479
                                                    }
4480
                                                }
4481
                                                echo '</td>';
4482
                                            }
4483
                                            echo '<td>'.$status.'</td>';
4484
                                        } else {
4485
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4486
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4487
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4488
                                                ) {
4489
                                                    if ($this->showExpectedChoiceColumn()) {
4490
                                                        echo '<td>';
4491
                                                        echo Display::span(
4492
                                                            $real_list[$i_answer_correct_answer],
4493
                                                            ['style' => 'color: #008000; font-weight: bold;']
4494
                                                        );
4495
                                                        echo '</td>';
4496
                                                    }
4497
                                                }
4498
                                            }
4499
                                        }
4500
                                        echo '</tr>';
4501
4502
                                        break;
4503
                                    case DRAGGABLE:
4504
                                        if (false == $showTotalScoreAndUserChoicesInLastAttempt) {
4505
                                            $s_answer_label = '';
4506
                                        }
4507
                                        echo '<tr>';
4508
                                        if ($this->showExpectedChoice()) {
4509
                                            if (!in_array($this->results_disabled, [
4510
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4511
                                                //RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4512
                                            ])
4513
                                            ) {
4514
                                                echo '<td>'.$user_answer.'</td>';
4515
                                            } else {
4516
                                                $status = Display::label(get_lang('Correct'), 'success');
4517
                                            }
4518
                                            echo '<td>'.$s_answer_label.'</td>';
4519
                                            echo '<td>'.$status.'</td>';
4520
                                        } else {
4521
                                            echo '<td>'.$s_answer_label.'</td>';
4522
                                            echo '<td>'.$user_answer.'</td>';
4523
                                            echo '<td>';
4524
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4525
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4526
                                                    true === $showTotalScoreAndUserChoicesInLastAttempt
4527
                                                ) {
4528
                                                    echo Display::span(
4529
                                                        $real_list[$i_answer_correct_answer],
4530
                                                        ['style' => 'color: #008000; font-weight: bold;']
4531
                                                    );
4532
                                                }
4533
                                            }
4534
                                            echo '</td>';
4535
                                        }
4536
                                        echo '</tr>';
4537
4538
                                        break;
4539
                                }
4540
                            }
4541
                            $counterAnswer++;
4542
                        }
4543
4544
                        break 2; // break the switch and the "for" condition
4545
                    } else {
4546
                        if ($answerCorrect) {
4547
                            if (isset($choice[$answerAutoId]) &&
4548
                                $answerCorrect == $choice[$answerAutoId]
4549
                            ) {
4550
                                $correctAnswerId[] = $answerAutoId;
4551
                                $questionScore += $answerWeighting;
4552
                                $totalScore += $answerWeighting;
4553
                                $user_answer = Display::span($answerMatching[$choice[$answerAutoId]]);
4554
                            } else {
4555
                                if (isset($answerMatching[$choice[$answerAutoId]])) {
4556
                                    $user_answer = Display::span(
4557
                                        $answerMatching[$choice[$answerAutoId]],
4558
                                        ['style' => 'color: #FF0000; text-decoration: line-through;']
4559
                                    );
4560
                                }
4561
                            }
4562
                            $matching[$answerAutoId] = $choice[$answerAutoId];
4563
                        }
4564
                    }
4565
4566
                    break;
4567
                case HOT_SPOT:
4568
                    if ($from_database) {
4569
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4570
                        // Check auto id
4571
                        $sql = "SELECT hotspot_correct
4572
                                FROM $TBL_TRACK_HOTSPOT
4573
                                WHERE
4574
                                    hotspot_exe_id = $exeId AND
4575
                                    hotspot_question_id= $questionId AND
4576
                                    hotspot_answer_id = $answerAutoId
4577
                                ORDER BY hotspot_id ASC";
4578
                        $result = Database::query($sql);
4579
                        if (Database::num_rows($result)) {
4580
                            $studentChoice = Database::result(
4581
                                $result,
4582
                                0,
4583
                                'hotspot_correct'
4584
                            );
4585
4586
                            if ($studentChoice) {
4587
                                $questionScore += $answerWeighting;
4588
                                $totalScore += $answerWeighting;
4589
                            }
4590
                        } else {
4591
                            // If answer.id is different:
4592
                            $sql = "SELECT hotspot_correct
4593
                                FROM $TBL_TRACK_HOTSPOT
4594
                                WHERE
4595
                                    hotspot_exe_id = $exeId AND
4596
                                    hotspot_question_id= $questionId AND
4597
                                    hotspot_answer_id = ".(int) $answerId.'
4598
                                ORDER BY hotspot_id ASC';
4599
                            $result = Database::query($sql);
4600
4601
                            if (Database::num_rows($result)) {
4602
                                $studentChoice = Database::result(
4603
                                    $result,
4604
                                    0,
4605
                                    'hotspot_correct'
4606
                                );
4607
4608
                                if ($studentChoice) {
4609
                                    $questionScore += $answerWeighting;
4610
                                    $totalScore += $answerWeighting;
4611
                                }
4612
                            } else {
4613
                                // check answer.iid
4614
                                if (!empty($answerIid)) {
4615
                                    $sql = "SELECT hotspot_correct
4616
                                            FROM $TBL_TRACK_HOTSPOT
4617
                                            WHERE
4618
                                                hotspot_exe_id = $exeId AND
4619
                                                hotspot_question_id= $questionId AND
4620
                                                hotspot_answer_id = $answerIid
4621
                                            ORDER BY hotspot_id ASC";
4622
                                    $result = Database::query($sql);
4623
4624
                                    $studentChoice = Database::result(
4625
                                        $result,
4626
                                        0,
4627
                                        'hotspot_correct'
4628
                                    );
4629
4630
                                    if ($studentChoice) {
4631
                                        $questionScore += $answerWeighting;
4632
                                        $totalScore += $answerWeighting;
4633
                                    }
4634
                                }
4635
                            }
4636
                        }
4637
                    } else {
4638
                        if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
4639
                            $choice[$answerAutoId] = 0;
4640
                            $choice[$answerIid] = 0;
4641
                        } else {
4642
                            $studentChoice = $choice[$answerAutoId];
4643
                            if (empty($studentChoice)) {
4644
                                $studentChoice = $choice[$answerIid];
4645
                            }
4646
                            $choiceIsValid = false;
4647
                            if (!empty($studentChoice)) {
4648
                                $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
4649
                                $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
4650
                                $choicePoint = Geometry::decodePoint($studentChoice);
4651
4652
                                switch ($hotspotType) {
4653
                                    case 'square':
4654
                                        $hotspotProperties = Geometry::decodeSquare($hotspotCoordinates);
4655
                                        $choiceIsValid = Geometry::pointIsInSquare($hotspotProperties, $choicePoint);
4656
4657
                                        break;
4658
                                    case 'circle':
4659
                                        $hotspotProperties = Geometry::decodeEllipse($hotspotCoordinates);
4660
                                        $choiceIsValid = Geometry::pointIsInEllipse($hotspotProperties, $choicePoint);
4661
4662
                                        break;
4663
                                    case 'poly':
4664
                                        $hotspotProperties = Geometry::decodePolygon($hotspotCoordinates);
4665
                                        $choiceIsValid = Geometry::pointIsInPolygon($hotspotProperties, $choicePoint);
4666
4667
                                        break;
4668
                                }
4669
                            }
4670
4671
                            $choice[$answerAutoId] = 0;
4672
                            if ($choiceIsValid) {
4673
                                $questionScore += $answerWeighting;
4674
                                $totalScore += $answerWeighting;
4675
                                $choice[$answerAutoId] = 1;
4676
                                $choice[$answerIid] = 1;
4677
                            }
4678
                        }
4679
                    }
4680
4681
                    break;
4682
                case HOT_SPOT_ORDER:
4683
                    // @todo never added to chamilo
4684
                    // for hotspot with fixed order
4685
                    $studentChoice = $choice['order'][$answerId];
4686
                    if ($studentChoice == $answerId) {
4687
                        $questionScore += $answerWeighting;
4688
                        $totalScore += $answerWeighting;
4689
                        $studentChoice = true;
4690
                    } else {
4691
                        $studentChoice = false;
4692
                    }
4693
4694
                    break;
4695
                case HOT_SPOT_DELINEATION:
4696
                    // for hotspot with delineation
4697
                    if ($from_database) {
4698
                        // getting the user answer
4699
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4700
                        $query = "SELECT hotspot_correct, hotspot_coordinate
4701
                                    FROM $TBL_TRACK_HOTSPOT
4702
                                    WHERE
4703
                                        hotspot_exe_id = $exeId AND
4704
                                        hotspot_question_id= $questionId AND
4705
                                        hotspot_answer_id = '1'";
4706
                        // By default we take 1 because it's a delineation
4707
                        $resq = Database::query($query);
4708
                        $row = Database::fetch_array($resq, 'ASSOC');
4709
4710
                        $choice = $row['hotspot_correct'];
4711
                        $user_answer = $row['hotspot_coordinate'];
4712
4713
                        // THIS is very important otherwise the poly_compile will throw an error!!
4714
                        // round-up the coordinates
4715
                        $coords = explode('/', $user_answer);
4716
                        $coords = array_filter($coords);
4717
                        $user_array = '';
4718
                        foreach ($coords as $coord) {
4719
                            list($x, $y) = explode(';', $coord);
4720
                            $user_array .= round($x).';'.round($y).'/';
4721
                        }
4722
                        $user_array = substr($user_array, 0, -1) ?: '';
4723
                    } else {
4724
                        if (!empty($studentChoice)) {
4725
                            $correctAnswerId[] = $answerAutoId;
4726
                            $newquestionList[] = $questionId;
4727
                        }
4728
4729
                        if (1 === $answerId) {
4730
                            $studentChoice = $choice[$answerId];
4731
                            $questionScore += $answerWeighting;
4732
                        }
4733
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
4734
                            $user_array = $_SESSION['exerciseResultCoordinates'][$questionId];
4735
                        }
4736
                    }
4737
                    $_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
4738
                    $_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
4739
4740
                    break;
4741
                case ANNOTATION:
4742
                    if ($from_database) {
4743
                        $sql = "SELECT answer, marks
4744
                                FROM $TBL_TRACK_ATTEMPT
4745
                                WHERE
4746
                                  exe_id = $exeId AND
4747
                                  question_id = $questionId ";
4748
                        $resq = Database::query($sql);
4749
                        $data = Database::fetch_array($resq);
4750
4751
                        $questionScore = empty($data['marks']) ? 0 : $data['marks'];
4752
                        $arrques = $questionName;
4753
4754
                        break;
4755
                    }
4756
                    $studentChoice = $choice;
4757
                    if ($studentChoice) {
4758
                        $questionScore = 0;
4759
                    }
4760
4761
                    break;
4762
            }
4763
4764
            if ($show_result) {
4765
                if ('exercise_result' === $from) {
4766
                    // Display answers (if not matching type, or if the answer is correct)
4767
                    if (!in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE]) ||
4768
                        $answerCorrect
4769
                    ) {
4770
                        if (in_array(
4771
                            $answerType,
4772
                            [
4773
                                UNIQUE_ANSWER,
4774
                                UNIQUE_ANSWER_IMAGE,
4775
                                UNIQUE_ANSWER_NO_OPTION,
4776
                                MULTIPLE_ANSWER,
4777
                                MULTIPLE_ANSWER_COMBINATION,
4778
                                GLOBAL_MULTIPLE_ANSWER,
4779
                                READING_COMPREHENSION,
4780
                            ]
4781
                        )) {
4782
                            ExerciseShowFunctions::display_unique_or_multiple_answer(
4783
                                $this,
4784
                                $feedback_type,
4785
                                $answerType,
4786
                                $studentChoice,
4787
                                $answer,
4788
                                $answerComment,
4789
                                $answerCorrect,
4790
                                0,
4791
                                0,
4792
                                0,
4793
                                $results_disabled,
4794
                                $showTotalScoreAndUserChoicesInLastAttempt,
4795
                                $this->export
4796
                            );
4797
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
4798
                            ExerciseShowFunctions::display_multiple_answer_true_false(
4799
                                $this,
4800
                                $feedback_type,
4801
                                $answerType,
4802
                                $studentChoice,
4803
                                $answer,
4804
                                $answerComment,
4805
                                $answerCorrect,
4806
                                0,
4807
                                $questionId,
4808
                                0,
4809
                                $results_disabled,
4810
                                $showTotalScoreAndUserChoicesInLastAttempt
4811
                            );
4812
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
4813
                            ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
4814
                                $this,
4815
                                $feedback_type,
4816
                                $studentChoice,
4817
                                $studentChoiceDegree,
4818
                                $answer,
4819
                                $answerComment,
4820
                                $answerCorrect,
4821
                                $questionId,
4822
                                $results_disabled
4823
                            );
4824
                        } elseif (MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType) {
4825
                            ExerciseShowFunctions::display_multiple_answer_combination_true_false(
4826
                                $this,
4827
                                $feedback_type,
4828
                                $answerType,
4829
                                $studentChoice,
4830
                                $answer,
4831
                                $answerComment,
4832
                                $answerCorrect,
4833
                                0,
4834
                                0,
4835
                                0,
4836
                                $results_disabled,
4837
                                $showTotalScoreAndUserChoicesInLastAttempt
4838
                            );
4839
                        } elseif (FILL_IN_BLANKS == $answerType) {
4840
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
4841
                                $this,
4842
                                $feedback_type,
4843
                                $answer,
4844
                                0,
4845
                                0,
4846
                                $results_disabled,
4847
                                '',
4848
                                $showTotalScoreAndUserChoicesInLastAttempt
4849
                            );
4850
                        } elseif (CALCULATED_ANSWER == $answerType) {
4851
                            ExerciseShowFunctions::display_calculated_answer(
4852
                                $this,
4853
                                $feedback_type,
4854
                                $answer,
4855
                                0,
4856
                                0,
4857
                                $results_disabled,
4858
                                $showTotalScoreAndUserChoicesInLastAttempt,
4859
                                $expectedAnswer,
4860
                                $calculatedChoice,
4861
                                $calculatedStatus
4862
                            );
4863
                        } elseif (FREE_ANSWER == $answerType) {
4864
                            ExerciseShowFunctions::display_free_answer(
4865
                                $feedback_type,
4866
                                $choice,
4867
                                $exeId,
4868
                                $questionId,
4869
                                $questionScore,
4870
                                $results_disabled
4871
                            );
4872
                        } elseif (ORAL_EXPRESSION == $answerType) {
4873
                            // to store the details of open questions in an array to be used in mail
4874
                            /** @var OralExpression $objQuestionTmp */
4875
                            ExerciseShowFunctions::display_oral_expression_answer(
4876
                                $feedback_type,
4877
                                $choice,
4878
                                0,
4879
                                0,
4880
                                $objQuestionTmp->getFileUrl(true),
4881
                                $results_disabled,
4882
                                $questionScore
4883
                            );
4884
                        } elseif (HOT_SPOT == $answerType) {
4885
                            $correctAnswerId = 0;
4886
                            /** @var TrackEHotspot $hotspot */
4887
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
4888
                                if ($hotspot->getHotspotAnswerId() == $answerAutoId) {
4889
                                    break;
4890
                                }
4891
                            }
4892
4893
                            // force to show whether the choice is correct or not
4894
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
4895
                            ExerciseShowFunctions::display_hotspot_answer(
4896
                                $feedback_type,
4897
                                ++$correctAnswerId,
4898
                                $answer,
4899
                                $studentChoice,
4900
                                $answerComment,
4901
                                $results_disabled,
4902
                                $correctAnswerId,
4903
                                $showTotalScoreAndUserChoicesInLastAttempt
4904
                            );
4905
                        } elseif (HOT_SPOT_ORDER == $answerType) {
4906
                            ExerciseShowFunctions::display_hotspot_order_answer(
4907
                                $feedback_type,
4908
                                $answerId,
4909
                                $answer,
4910
                                $studentChoice,
4911
                                $answerComment
4912
                            );
4913
                        } elseif (HOT_SPOT_DELINEATION == $answerType) {
4914
                            $user_answer = $_SESSION['exerciseResultCoordinates'][$questionId];
4915
4916
                            // Round-up the coordinates
4917
                            $coords = explode('/', $user_answer);
4918
                            $coords = array_filter($coords);
4919
                            $user_array = '';
4920
                            foreach ($coords as $coord) {
4921
                                if (!empty($coord)) {
4922
                                    $parts = explode(';', $coord);
4923
                                    if (!empty($parts)) {
4924
                                        $user_array .= round($parts[0]).';'.round($parts[1]).'/';
4925
                                    }
4926
                                }
4927
                            }
4928
                            $user_array = substr($user_array, 0, -1) ?: '';
4929
                            if ($next) {
4930
                                $user_answer = $user_array;
4931
                                // We compare only the delineation not the other points
4932
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
4933
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
4934
4935
                                // Calculating the area
4936
                                $poly_user = convert_coordinates($user_answer, '/');
4937
                                $poly_answer = convert_coordinates($answer_question, '|');
4938
                                $max_coord = poly_get_max($poly_user, $poly_answer);
4939
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
4940
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
4941
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
4942
4943
                                $overlap = $poly_results['both'];
4944
                                $poly_answer_area = $poly_results['s1'];
4945
                                $poly_user_area = $poly_results['s2'];
4946
                                $missing = $poly_results['s1Only'];
4947
                                $excess = $poly_results['s2Only'];
4948
4949
                                // //this is an area in pixels
4950
                                if ($debug > 0) {
4951
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
4952
                                }
4953
4954
                                if ($overlap < 1) {
4955
                                    // Shortcut to avoid complicated calculations
4956
                                    $final_overlap = 0;
4957
                                    $final_missing = 100;
4958
                                    $final_excess = 100;
4959
                                } else {
4960
                                    // the final overlap is the percentage of the initial polygon
4961
                                    // that is overlapped by the user's polygon
4962
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
4963
                                    if ($debug > 1) {
4964
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap, 0);
4965
                                    }
4966
                                    // the final missing area is the percentage of the initial polygon
4967
                                    // that is not overlapped by the user's polygon
4968
                                    $final_missing = 100 - $final_overlap;
4969
                                    if ($debug > 1) {
4970
                                        error_log(__LINE__.' - Final missing is '.$final_missing, 0);
4971
                                    }
4972
                                    // the final excess area is the percentage of the initial polygon's size
4973
                                    // that is covered by the user's polygon outside of the initial polygon
4974
                                    $final_excess = round((((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100);
4975
                                    if ($debug > 1) {
4976
                                        error_log(__LINE__.' - Final excess is '.$final_excess, 0);
4977
                                    }
4978
                                }
4979
4980
                                // Checking the destination parameters parsing the "@@"
4981
                                $destination_items = explode('@@', $answerDestination);
4982
                                $threadhold_total = $destination_items[0];
4983
                                $threadhold_items = explode(';', $threadhold_total);
4984
                                $threadhold1 = $threadhold_items[0]; // overlap
4985
                                $threadhold2 = $threadhold_items[1]; // excess
4986
                                $threadhold3 = $threadhold_items[2]; // missing
4987
4988
                                // if is delineation
4989
                                if (1 === $answerId) {
4990
                                    //setting colors
4991
                                    if ($final_overlap >= $threadhold1) {
4992
                                        $overlap_color = true;
4993
                                    }
4994
                                    if ($final_excess <= $threadhold2) {
4995
                                        $excess_color = true;
4996
                                    }
4997
                                    if ($final_missing <= $threadhold3) {
4998
                                        $missing_color = true;
4999
                                    }
5000
5001
                                    // if pass
5002
                                    if ($final_overlap >= $threadhold1 &&
5003
                                        $final_missing <= $threadhold3 &&
5004
                                        $final_excess <= $threadhold2
5005
                                    ) {
5006
                                        $next = 1; //go to the oars
5007
                                        $result_comment = get_lang('Acceptable');
5008
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5009
                                    } else {
5010
                                        $next = 0;
5011
                                        $result_comment = get_lang('Unacceptable');
5012
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5013
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5014
                                        // checking the destination parameters parsing the "@@"
5015
                                        $destination_items = explode('@@', $answerDestination);
5016
                                    }
5017
                                } elseif ($answerId > 1) {
5018
                                    if ('noerror' == $objAnswerTmp->selectHotspotType($answerId)) {
5019
                                        if ($debug > 0) {
5020
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5021
                                        }
5022
                                        //type no error shouldn't be treated
5023
                                        $next = 1;
5024
5025
                                        continue;
5026
                                    }
5027
                                    if ($debug > 0) {
5028
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5029
                                    }
5030
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5031
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5032
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5033
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5034
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5035
5036
                                    if (false == $overlap) {
5037
                                        //all good, no overlap
5038
                                        $next = 1;
5039
5040
                                        continue;
5041
                                    } else {
5042
                                        if ($debug > 0) {
5043
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5044
                                        }
5045
                                        $organs_at_risk_hit++;
5046
                                        //show the feedback
5047
                                        $next = 0;
5048
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5049
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5050
5051
                                        $destination_items = explode('@@', $answerDestination);
5052
                                        $try_hotspot = $destination_items[1];
5053
                                        $lp_hotspot = $destination_items[2];
5054
                                        $select_question_hotspot = $destination_items[3];
5055
                                        $url_hotspot = $destination_items[4];
5056
                                    }
5057
                                }
5058
                            } else {
5059
                                // the first delineation feedback
5060
                                if ($debug > 0) {
5061
                                    error_log(__LINE__.' first', 0);
5062
                                }
5063
                            }
5064
                        } elseif (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
5065
                            echo '<tr>';
5066
                            echo Display::tag('td', $answerMatching[$answerId]);
5067
                            echo Display::tag(
5068
                                'td',
5069
                                "$user_answer / ".Display::tag(
5070
                                    'strong',
5071
                                    $answerMatching[$answerCorrect],
5072
                                    ['style' => 'color: #008000; font-weight: bold;']
5073
                                )
5074
                            );
5075
                            echo '</tr>';
5076
                        } elseif (ANNOTATION == $answerType) {
5077
                            ExerciseShowFunctions::displayAnnotationAnswer(
5078
                                $feedback_type,
5079
                                $exeId,
5080
                                $questionId,
5081
                                $questionScore,
5082
                                $results_disabled
5083
                            );
5084
                        }
5085
                    }
5086
                } else {
5087
                    if ($debug) {
5088
                        error_log('Showing questions $from '.$from);
5089
                    }
5090
5091
                    switch ($answerType) {
5092
                        case UNIQUE_ANSWER:
5093
                        case UNIQUE_ANSWER_IMAGE:
5094
                        case UNIQUE_ANSWER_NO_OPTION:
5095
                        case MULTIPLE_ANSWER:
5096
                        case GLOBAL_MULTIPLE_ANSWER:
5097
                        case MULTIPLE_ANSWER_COMBINATION:
5098
                        case READING_COMPREHENSION:
5099
                            if (1 == $answerId) {
5100
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5101
                                    $this,
5102
                                    $feedback_type,
5103
                                    $answerType,
5104
                                    $studentChoice,
5105
                                    $answer,
5106
                                    $answerComment,
5107
                                    $answerCorrect,
5108
                                    $exeId,
5109
                                    $questionId,
5110
                                    $answerId,
5111
                                    $results_disabled,
5112
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5113
                                    $this->export
5114
                                );
5115
                            } else {
5116
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5117
                                    $this,
5118
                                    $feedback_type,
5119
                                    $answerType,
5120
                                    $studentChoice,
5121
                                    $answer,
5122
                                    $answerComment,
5123
                                    $answerCorrect,
5124
                                    $exeId,
5125
                                    $questionId,
5126
                                    '',
5127
                                    $results_disabled,
5128
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5129
                                    $this->export
5130
                                );
5131
                            }
5132
5133
                            break;
5134
                        case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
5135
                            if (1 == $answerId) {
5136
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5137
                                    $this,
5138
                                    $feedback_type,
5139
                                    $answerType,
5140
                                    $studentChoice,
5141
                                    $answer,
5142
                                    $answerComment,
5143
                                    $answerCorrect,
5144
                                    $exeId,
5145
                                    $questionId,
5146
                                    $answerId,
5147
                                    $results_disabled,
5148
                                    $showTotalScoreAndUserChoicesInLastAttempt
5149
                                );
5150
                            } else {
5151
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5152
                                    $this,
5153
                                    $feedback_type,
5154
                                    $answerType,
5155
                                    $studentChoice,
5156
                                    $answer,
5157
                                    $answerComment,
5158
                                    $answerCorrect,
5159
                                    $exeId,
5160
                                    $questionId,
5161
                                    '',
5162
                                    $results_disabled,
5163
                                    $showTotalScoreAndUserChoicesInLastAttempt
5164
                                );
5165
                            }
5166
5167
                            break;
5168
                        case MULTIPLE_ANSWER_TRUE_FALSE:
5169
                            if (1 == $answerId) {
5170
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5171
                                    $this,
5172
                                    $feedback_type,
5173
                                    $answerType,
5174
                                    $studentChoice,
5175
                                    $answer,
5176
                                    $answerComment,
5177
                                    $answerCorrect,
5178
                                    $exeId,
5179
                                    $questionId,
5180
                                    $answerId,
5181
                                    $results_disabled,
5182
                                    $showTotalScoreAndUserChoicesInLastAttempt
5183
                                );
5184
                            } else {
5185
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5186
                                    $this,
5187
                                    $feedback_type,
5188
                                    $answerType,
5189
                                    $studentChoice,
5190
                                    $answer,
5191
                                    $answerComment,
5192
                                    $answerCorrect,
5193
                                    $exeId,
5194
                                    $questionId,
5195
                                    '',
5196
                                    $results_disabled,
5197
                                    $showTotalScoreAndUserChoicesInLastAttempt
5198
                                );
5199
                            }
5200
5201
                            break;
5202
                        case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
5203
                            if (1 == $answerId) {
5204
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5205
                                    $this,
5206
                                    $feedback_type,
5207
                                    $studentChoice,
5208
                                    $studentChoiceDegree,
5209
                                    $answer,
5210
                                    $answerComment,
5211
                                    $answerCorrect,
5212
                                    $questionId,
5213
                                    $results_disabled
5214
                                );
5215
                            } else {
5216
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5217
                                    $this,
5218
                                    $feedback_type,
5219
                                    $studentChoice,
5220
                                    $studentChoiceDegree,
5221
                                    $answer,
5222
                                    $answerComment,
5223
                                    $answerCorrect,
5224
                                    $questionId,
5225
                                    $results_disabled
5226
                                );
5227
                            }
5228
5229
                            break;
5230
                        case FILL_IN_BLANKS:
5231
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5232
                                $this,
5233
                                $feedback_type,
5234
                                $answer,
5235
                                $exeId,
5236
                                $questionId,
5237
                                $results_disabled,
5238
                                $str,
5239
                                $showTotalScoreAndUserChoicesInLastAttempt
5240
                            );
5241
5242
                            break;
5243
                        case CALCULATED_ANSWER:
5244
                            ExerciseShowFunctions::display_calculated_answer(
5245
                                $this,
5246
                                $feedback_type,
5247
                                $answer,
5248
                                $exeId,
5249
                                $questionId,
5250
                                $results_disabled,
5251
                                '',
5252
                                $showTotalScoreAndUserChoicesInLastAttempt
5253
                            );
5254
5255
                            break;
5256
                        case FREE_ANSWER:
5257
                            echo ExerciseShowFunctions::display_free_answer(
5258
                                $feedback_type,
5259
                                $choice,
5260
                                $exeId,
5261
                                $questionId,
5262
                                $questionScore,
5263
                                $results_disabled
5264
                            );
5265
5266
                            break;
5267
                        case ORAL_EXPRESSION:
5268
                            echo '<tr>
5269
                                <td valign="top">'.
5270
                                ExerciseShowFunctions::display_oral_expression_answer(
5271
                                    $feedback_type,
5272
                                    $choice,
5273
                                    $exeId,
5274
                                    $questionId,
5275
                                    $objQuestionTmp->getFileUrl(),
5276
                                    $results_disabled,
5277
                                    $questionScore
5278
                                ).'</td>
5279
                                </tr>
5280
                                </table>';
5281
5282
                            break;
5283
                        case HOT_SPOT:
5284
                            ExerciseShowFunctions::display_hotspot_answer(
5285
                                $feedback_type,
5286
                                $answerId,
5287
                                $answer,
5288
                                $studentChoice,
5289
                                $answerComment,
5290
                                $results_disabled,
5291
                                $answerId,
5292
                                $showTotalScoreAndUserChoicesInLastAttempt
5293
                            );
5294
5295
                            break;
5296
                        case HOT_SPOT_DELINEATION:
5297
                            $user_answer = $user_array;
5298
                            if ($next) {
5299
                                $user_answer = $user_array;
5300
                                // we compare only the delineation not the other points
5301
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5302
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5303
5304
                                // calculating the area
5305
                                $poly_user = convert_coordinates($user_answer, '/');
5306
                                $poly_answer = convert_coordinates($answer_question, '|');
5307
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5308
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5309
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5310
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5311
5312
                                $overlap = $poly_results['both'];
5313
                                $poly_answer_area = $poly_results['s1'];
5314
                                $poly_user_area = $poly_results['s2'];
5315
                                $missing = $poly_results['s1Only'];
5316
                                $excess = $poly_results['s2Only'];
5317
                                if ($debug > 0) {
5318
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5319
                                }
5320
                                if ($overlap < 1) {
5321
                                    //shortcut to avoid complicated calculations
5322
                                    $final_overlap = 0;
5323
                                    $final_missing = 100;
5324
                                    $final_excess = 100;
5325
                                } else {
5326
                                    // the final overlap is the percentage of the initial polygon
5327
                                    // that is overlapped by the user's polygon
5328
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5329
5330
                                    // the final missing area is the percentage of the initial polygon that
5331
                                    // is not overlapped by the user's polygon
5332
                                    $final_missing = 100 - $final_overlap;
5333
                                    // the final excess area is the percentage of the initial polygon's size that is
5334
                                    // covered by the user's polygon outside of the initial polygon
5335
                                    $final_excess = round((((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100);
5336
5337
                                    if ($debug > 1) {
5338
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap);
5339
                                        error_log(__LINE__.' - Final excess is '.$final_excess);
5340
                                        error_log(__LINE__.' - Final missing is '.$final_missing);
5341
                                    }
5342
                                }
5343
5344
                                // Checking the destination parameters parsing the "@@"
5345
                                $destination_items = explode('@@', $answerDestination);
5346
                                $threadhold_total = $destination_items[0];
5347
                                $threadhold_items = explode(';', $threadhold_total);
5348
                                $threadhold1 = $threadhold_items[0]; // overlap
5349
                                $threadhold2 = $threadhold_items[1]; // excess
5350
                                $threadhold3 = $threadhold_items[2]; //missing
5351
                                // if is delineation
5352
                                if (1 === $answerId) {
5353
                                    //setting colors
5354
                                    if ($final_overlap >= $threadhold1) {
5355
                                        $overlap_color = true;
5356
                                    }
5357
                                    if ($final_excess <= $threadhold2) {
5358
                                        $excess_color = true;
5359
                                    }
5360
                                    if ($final_missing <= $threadhold3) {
5361
                                        $missing_color = true;
5362
                                    }
5363
5364
                                    // if pass
5365
                                    if ($final_overlap >= $threadhold1 &&
5366
                                        $final_missing <= $threadhold3 &&
5367
                                        $final_excess <= $threadhold2
5368
                                    ) {
5369
                                        $next = 1; //go to the oars
5370
                                        $result_comment = get_lang('Acceptable');
5371
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5372
                                    } else {
5373
                                        $next = 0;
5374
                                        $result_comment = get_lang('Unacceptable');
5375
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5376
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5377
                                        //checking the destination parameters parsing the "@@"
5378
                                        $destination_items = explode('@@', $answerDestination);
5379
                                    }
5380
                                } elseif ($answerId > 1) {
5381
                                    if ('noerror' === $objAnswerTmp->selectHotspotType($answerId)) {
5382
                                        if ($debug > 0) {
5383
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5384
                                        }
5385
                                        //type no error shouldn't be treated
5386
                                        $next = 1;
5387
5388
                                        break;
5389
                                    }
5390
                                    if ($debug > 0) {
5391
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5392
                                    }
5393
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5394
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5395
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5396
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5397
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5398
5399
                                    if (false == $overlap) {
5400
                                        //all good, no overlap
5401
                                        $next = 1;
5402
5403
                                        break;
5404
                                    } else {
5405
                                        if ($debug > 0) {
5406
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5407
                                        }
5408
                                        $organs_at_risk_hit++;
5409
                                        //show the feedback
5410
                                        $next = 0;
5411
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5412
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5413
5414
                                        $destination_items = explode('@@', $answerDestination);
5415
                                        $try_hotspot = $destination_items[1];
5416
                                        $lp_hotspot = $destination_items[2];
5417
                                        $select_question_hotspot = $destination_items[3];
5418
                                        $url_hotspot = $destination_items[4];
5419
                                    }
5420
                                }
5421
                            }
5422
5423
                            break;
5424
                        case HOT_SPOT_ORDER:
5425
                            ExerciseShowFunctions::display_hotspot_order_answer(
5426
                                $feedback_type,
5427
                                $answerId,
5428
                                $answer,
5429
                                $studentChoice,
5430
                                $answerComment
5431
                            );
5432
5433
                            break;
5434
                        case DRAGGABLE:
5435
                        case MATCHING_DRAGGABLE:
5436
                        case MATCHING:
5437
                            echo '<tr>';
5438
                            echo Display::tag('td', $answerMatching[$answerId]);
5439
                            echo Display::tag(
5440
                                'td',
5441
                                "$user_answer / ".Display::tag(
5442
                                    'strong',
5443
                                    $answerMatching[$answerCorrect],
5444
                                    ['style' => 'color: #008000; font-weight: bold;']
5445
                                )
5446
                            );
5447
                            echo '</tr>';
5448
5449
                            break;
5450
                        case ANNOTATION:
5451
                            ExerciseShowFunctions::displayAnnotationAnswer(
5452
                                $feedback_type,
5453
                                $exeId,
5454
                                $questionId,
5455
                                $questionScore,
5456
                                $results_disabled
5457
                            );
5458
5459
                            break;
5460
                    }
5461
                }
5462
            }
5463
        } // end for that loops over all answers of the current question
5464
5465
        if ($debug) {
5466
            error_log('-- end answer loop --');
5467
        }
5468
5469
        $final_answer = true;
5470
5471
        foreach ($real_answers as $my_answer) {
5472
            if (!$my_answer) {
5473
                $final_answer = false;
5474
            }
5475
        }
5476
5477
        //we add the total score after dealing with the answers
5478
        if (MULTIPLE_ANSWER_COMBINATION == $answerType ||
5479
            MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
5480
        ) {
5481
            if ($final_answer) {
5482
                //getting only the first score where we save the weight of all the question
5483
                $answerWeighting = $objAnswerTmp->selectWeighting(1);
5484
                $questionScore += $answerWeighting;
5485
            }
5486
        }
5487
5488
        $extra_data = [
5489
            'final_overlap' => $final_overlap,
5490
            'final_missing' => $final_missing,
5491
            'final_excess' => $final_excess,
5492
            'overlap_color' => $overlap_color,
5493
            'missing_color' => $missing_color,
5494
            'excess_color' => $excess_color,
5495
            'threadhold1' => $threadhold1,
5496
            'threadhold2' => $threadhold2,
5497
            'threadhold3' => $threadhold3,
5498
        ];
5499
5500
        if ('exercise_result' === $from) {
5501
            // if answer is hotspot. To the difference of exercise_show.php,
5502
            //  we use the results from the session (from_db=0)
5503
            // TODO Change this, because it is wrong to show the user
5504
            //  some results that haven't been stored in the database yet
5505
            if (HOT_SPOT == $answerType || HOT_SPOT_ORDER == $answerType || HOT_SPOT_DELINEATION == $answerType) {
5506
                if ($debug) {
5507
                    error_log('$from AND this is a hotspot kind of question ');
5508
                }
5509
                if (HOT_SPOT_DELINEATION === $answerType) {
5510
                    if ($showHotSpotDelineationTable) {
5511
                        if (!is_numeric($final_overlap)) {
5512
                            $final_overlap = 0;
5513
                        }
5514
                        if (!is_numeric($final_missing)) {
5515
                            $final_missing = 0;
5516
                        }
5517
                        if (!is_numeric($final_excess)) {
5518
                            $final_excess = 0;
5519
                        }
5520
5521
                        if ($final_overlap > 100) {
5522
                            $final_overlap = 100;
5523
                        }
5524
5525
                        $table_resume = '<table class="data_table">
5526
                                <tr class="row_odd" >
5527
                                    <td></td>
5528
                                    <td ><b>'.get_lang('Requirements').'</b></td>
5529
                                    <td><b>'.get_lang('Your answer').'</b></td>
5530
                                </tr>
5531
                                <tr class="row_even">
5532
                                    <td><b>'.get_lang('Overlapping areaping area').'</b></td>
5533
                                    <td>'.get_lang('Minimumimum').' '.$threadhold1.'</td>
5534
                                    <td class="text-right '.($overlap_color ? 'text-success' : 'text-danger').'">'
5535
                            .$final_overlap < 0 ? 0 : (int) $final_overlap.'</td>
5536
                                </tr>
5537
                                <tr>
5538
                                    <td><b>'.get_lang('Excessive areaive area').'</b></td>
5539
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold2.'</td>
5540
                                    <td class="text-right '.($excess_color ? 'text-success' : 'text-danger').'">'
5541
                            .$final_excess < 0 ? 0 : (int) $final_excess.'</td>
5542
                                </tr>
5543
                                <tr class="row_even">
5544
                                    <td><b>'.get_lang('Missing area area').'</b></td>
5545
                                    <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold3.'</td>
5546
                                    <td class="text-right '.($missing_color ? 'text-success' : 'text-danger').'">'
5547
                            .$final_missing < 0 ? 0 : (int) $final_missing.'</td>
5548
                                </tr>
5549
                            </table>';
5550
                        if (0 == $next) {
5551
                            /*$try = $try_hotspot;
5552
                            $lp = $lp_hotspot;
5553
                            $destinationid = $select_question_hotspot;
5554
                            $url = $url_hotspot;*/
5555
                        } else {
5556
                            $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
5557
                            $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
5558
                        }
5559
5560
                        $message = '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>
5561
                                    <p style="text-align:center">';
5562
                        $message .= '<p>'.get_lang('Your delineation :').'</p>';
5563
                        $message .= $table_resume;
5564
                        $message .= '<br />'.get_lang('Your result is :').' '.$result_comment.'<br />';
5565
                        if ($organs_at_risk_hit > 0) {
5566
                            $message .= '<p><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
5567
                        }
5568
                        $message .= '<p>'.$comment.'</p>';
5569
                        echo $message;
5570
5571
                        $_SESSION['hotspot_delineation_result'][$this->selectId()][$questionId][0] = $message;
5572
                        $_SESSION['hotspot_delineation_result'][$this->selectId()][$questionId][1] = $_SESSION['exerciseResultCoordinates'][$questionId];
5573
                    } else {
5574
                        echo $hotspot_delineation_result[0];
5575
                    }
5576
5577
                    // Save the score attempts
5578
                    if (1) {
5579
                        //getting the answer 1 or 0 comes from exercise_submit_modal.php
5580
                        $final_answer = isset($hotspot_delineation_result[1]) ? $hotspot_delineation_result[1] : '';
5581
                        if (0 == $final_answer) {
5582
                            $questionScore = 0;
5583
                        }
5584
                        // we always insert the answer_id 1 = delineation
5585
                        Event::saveQuestionAttempt($questionScore, 1, $quesId, $exeId, 0);
5586
                        //in delineation mode, get the answer from $hotspot_delineation_result[1]
5587
                        $hotspotValue = isset($hotspot_delineation_result[1]) ? 1 === (int) $hotspot_delineation_result[1] ? 1 : 0 : 0;
5588
                        Event::saveExerciseAttemptHotspot(
5589
                            $exeId,
5590
                            $quesId,
5591
                            1,
5592
                            $hotspotValue,
5593
                            $exerciseResultCoordinates[$quesId]
5594
                        );
5595
                    } else {
5596
                        if (0 == $final_answer) {
5597
                            $questionScore = 0;
5598
                            $answer = 0;
5599
                            Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0);
5600
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5601
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5602
                                    Event::saveExerciseAttemptHotspot(
5603
                                        $exeId,
5604
                                        $quesId,
5605
                                        $idx,
5606
                                        0,
5607
                                        $val
5608
                                    );
5609
                                }
5610
                            }
5611
                        } else {
5612
                            Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0);
5613
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5614
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5615
                                    $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
5616
                                    Event::saveExerciseAttemptHotspot(
5617
                                        $exeId,
5618
                                        $quesId,
5619
                                        $idx,
5620
                                        $hotspotValue,
5621
                                        $val
5622
                                    );
5623
                                }
5624
                            }
5625
                        }
5626
                    }
5627
                }
5628
            }
5629
5630
            $relPath = api_get_path(WEB_CODE_PATH);
5631
5632
            if (HOT_SPOT == $answerType || HOT_SPOT_ORDER == $answerType) {
5633
                // We made an extra table for the answers
5634
                if ($show_result) {
5635
                    echo '</table></td></tr>';
5636
                    echo '
5637
                        <tr>
5638
                            <td colspan="2">
5639
                                <p><em>'.get_lang('Image zones')."</em></p>
5640
                                <div id=\"hotspot-solution-$questionId\"></div>
5641
                                <script>
5642
                                    $(function() {
5643
                                        new HotspotQuestion({
5644
                                            questionId: $questionId,
5645
                                            exerciseId: {$this->getId()},
5646
                                            exeId: $exeId,
5647
                                            selector: '#hotspot-solution-$questionId',
5648
                                            for: 'solution',
5649
                                            relPath: '$relPath'
5650
                                        });
5651
                                    });
5652
                                </script>
5653
                            </td>
5654
                        </tr>
5655
                    ";
5656
                }
5657
            } elseif (ANNOTATION == $answerType) {
5658
                if ($show_result) {
5659
                    echo '
5660
                        <p><em>'.get_lang('Annotation').'</em></p>
5661
                        <div id="annotation-canvas-'.$questionId.'"></div>
5662
                        <script>
5663
                            AnnotationQuestion({
5664
                                questionId: parseInt('.$questionId.'),
5665
                                exerciseId: parseInt('.$exeId.'),
5666
                                relPath: \''.$relPath.'\',
5667
                                courseId: parseInt('.$course_id.')
5668
                            });
5669
                        </script>
5670
                    ';
5671
                }
5672
            }
5673
5674
            if ($show_result && $answerType != ANNOTATION) {
5675
                echo '</table>';
5676
            }
5677
        }
5678
        unset($objAnswerTmp);
5679
5680
        $totalWeighting += $questionWeighting;
5681
        // Store results directly in the database
5682
        // For all in one page exercises, the results will be
5683
        // stored by exercise_results.php (using the session)
5684
        if ($save_results) {
5685
            if ($debug) {
5686
                error_log("Save question results $save_results");
5687
                error_log('choice: ');
5688
                error_log(print_r($choice, 1));
5689
            }
5690
5691
            if (empty($choice)) {
5692
                $choice = 0;
5693
            }
5694
            // with certainty degree
5695
            if (empty($choiceDegreeCertainty)) {
5696
                $choiceDegreeCertainty = 0;
5697
            }
5698
            if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
5699
                MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType ||
5700
                MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType
5701
            ) {
5702
                if (0 != $choice) {
5703
                    $reply = array_keys($choice);
5704
                    $countReply = count($reply);
5705
                    for ($i = 0; $i < $countReply; $i++) {
5706
                        $chosenAnswer = $reply[$i];
5707
                        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
5708
                            if (0 != $choiceDegreeCertainty) {
5709
                                $replyDegreeCertainty = array_keys($choiceDegreeCertainty);
5710
                                $answerDegreeCertainty = isset($replyDegreeCertainty[$i]) ? $replyDegreeCertainty[$i] : '';
5711
                                $answerValue = isset($choiceDegreeCertainty[$answerDegreeCertainty]) ? $choiceDegreeCertainty[$answerDegreeCertainty] : '';
5712
                                Event::saveQuestionAttempt(
5713
                                    $questionScore,
5714
                                    $chosenAnswer.':'.$choice[$chosenAnswer].':'.$answerValue,
5715
                                    $quesId,
5716
                                    $exeId,
5717
                                    $i,
5718
                                    $this->getId(),
5719
                                    $updateResults
5720
                                );
5721
                            }
5722
                        } else {
5723
                            Event::saveQuestionAttempt(
5724
                                $questionScore,
5725
                                $chosenAnswer.':'.$choice[$chosenAnswer],
5726
                                $quesId,
5727
                                $exeId,
5728
                                $i,
5729
                                $this->getId(),
5730
                                $updateResults
5731
                            );
5732
                        }
5733
                        if ($debug) {
5734
                            error_log('result =>'.$questionScore.' '.$chosenAnswer.':'.$choice[$chosenAnswer]);
5735
                        }
5736
                    }
5737
                } else {
5738
                    Event::saveQuestionAttempt(
5739
                        $questionScore,
5740
                        0,
5741
                        $quesId,
5742
                        $exeId,
5743
                        0,
5744
                        $this->getId()
5745
                    );
5746
                }
5747
            } elseif (MULTIPLE_ANSWER == $answerType || GLOBAL_MULTIPLE_ANSWER == $answerType) {
5748
                if (0 != $choice) {
5749
                    $reply = array_keys($choice);
5750
                    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...
5751
                        $ans = $reply[$i];
5752
                        Event::saveQuestionAttempt($questionScore, $ans, $quesId, $exeId, $i, $this->getId());
5753
                    }
5754
                } else {
5755
                    Event::saveQuestionAttempt($questionScore, 0, $quesId, $exeId, 0, $this->getId());
5756
                }
5757
            } elseif (MULTIPLE_ANSWER_COMBINATION == $answerType) {
5758
                if (0 != $choice) {
5759
                    $reply = array_keys($choice);
5760
                    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...
5761
                        $ans = $reply[$i];
5762
                        Event::saveQuestionAttempt($questionScore, $ans, $quesId, $exeId, $i, $this->getId());
5763
                    }
5764
                } else {
5765
                    Event::saveQuestionAttempt(
5766
                        $questionScore,
5767
                        0,
5768
                        $quesId,
5769
                        $exeId,
5770
                        0,
5771
                        $this->getId()
5772
                    );
5773
                }
5774
            } elseif (in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE])) {
5775
                if (isset($matching)) {
5776
                    foreach ($matching as $j => $val) {
5777
                        Event::saveQuestionAttempt(
5778
                            $questionScore,
5779
                            $val,
5780
                            $quesId,
5781
                            $exeId,
5782
                            $j,
5783
                            $this->getId()
5784
                        );
5785
                    }
5786
                }
5787
            } elseif (FREE_ANSWER == $answerType) {
5788
                $answer = $choice;
5789
                Event::saveQuestionAttempt(
5790
                    $questionScore,
5791
                    $answer,
5792
                    $quesId,
5793
                    $exeId,
5794
                    0,
5795
                    $this->getId()
5796
                );
5797
            } elseif (ORAL_EXPRESSION == $answerType) {
5798
                $answer = $choice;
5799
                Event::saveQuestionAttempt(
5800
                    $questionScore,
5801
                    $answer,
5802
                    $quesId,
5803
                    $exeId,
5804
                    0,
5805
                    $this->getId(),
5806
                    false,
5807
                    $objQuestionTmp->getAbsoluteFilePath()
5808
                );
5809
            } elseif (
5810
                in_array(
5811
                    $answerType,
5812
                    [UNIQUE_ANSWER, UNIQUE_ANSWER_IMAGE, UNIQUE_ANSWER_NO_OPTION, READING_COMPREHENSION]
5813
                )
5814
            ) {
5815
                $answer = $choice;
5816
                Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0, $this->getId());
5817
            } elseif (HOT_SPOT == $answerType || ANNOTATION == $answerType) {
5818
                $answer = [];
5819
                if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) {
5820
                    if ($debug) {
5821
                        error_log('Checking result coordinates');
5822
                    }
5823
                    Database::delete(
5824
                        Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
5825
                        [
5826
                            'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
5827
                                $exeId,
5828
                                $questionId,
5829
                                api_get_course_int_id(),
5830
                            ],
5831
                        ]
5832
                    );
5833
5834
                    foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
5835
                        $answer[] = $val;
5836
                        $hotspotValue = 1 === (int) $choice[$idx] ? 1 : 0;
5837
                        if ($debug) {
5838
                            error_log('Hotspot value: '.$hotspotValue);
5839
                        }
5840
                        Event::saveExerciseAttemptHotspot(
5841
                            $exeId,
5842
                            $quesId,
5843
                            $idx,
5844
                            $hotspotValue,
5845
                            $val,
5846
                            false,
5847
                            $this->getId()
5848
                        );
5849
                    }
5850
                } else {
5851
                    if ($debug) {
5852
                        error_log('Empty: exerciseResultCoordinates');
5853
                    }
5854
                }
5855
                Event::saveQuestionAttempt($questionScore, implode('|', $answer), $quesId, $exeId, 0, $this->getId());
5856
            } else {
5857
                Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0, $this->getId());
5858
            }
5859
        }
5860
5861
        if (0 == $propagate_neg && $questionScore < 0) {
5862
            $questionScore = 0;
5863
        }
5864
5865
        if ($save_results) {
5866
            $statsTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5867
            $sql = "UPDATE $statsTable SET
5868
                        score = score + ".(float) $questionScore."
5869
                    WHERE exe_id = $exeId";
5870
            Database::query($sql);
5871
        }
5872
5873
        $return = [
5874
            'score' => $questionScore,
5875
            'weight' => $questionWeighting,
5876
            'extra' => $extra_data,
5877
            'open_question' => $arrques,
5878
            'open_answer' => $arrans,
5879
            'answer_type' => $answerType,
5880
            'generated_oral_file' => $generatedFile,
5881
            'user_answered' => $userAnsweredQuestion,
5882
            'correct_answer_id' => $correctAnswerId,
5883
            'answer_destination' => $answerDestination,
5884
        ];
5885
5886
        return $return;
5887
    }
5888
5889
    /**
5890
     * Sends a notification when a user ends an examn.
5891
     *
5892
     * @param string $type                  'start' or 'end' of an exercise
5893
     * @param array  $question_list_answers
5894
     * @param string $origin
5895
     * @param int    $exe_id
5896
     * @param float  $score
5897
     * @param float  $weight
5898
     *
5899
     * @return bool
5900
     */
5901
    public function send_mail_notification_for_exam(
5902
        $type = 'end',
5903
        $question_list_answers,
5904
        $origin,
5905
        $exe_id,
5906
        $score = null,
5907
        $weight = null
5908
    ) {
5909
        $setting = api_get_course_setting('email_alert_manager_on_new_quiz');
5910
5911
        if (empty($setting) && empty($this->getNotifications())) {
5912
            return false;
5913
        }
5914
5915
        $settingFromExercise = $this->getNotifications();
5916
        if (!empty($settingFromExercise)) {
5917
            $setting = $settingFromExercise;
5918
        }
5919
5920
        // Email configuration settings
5921
        $courseCode = api_get_course_id();
5922
        $courseInfo = api_get_course_info($courseCode);
5923
5924
        if (empty($courseInfo)) {
5925
            return false;
5926
        }
5927
5928
        $sessionId = api_get_session_id();
5929
5930
        $sessionData = '';
5931
        if (!empty($sessionId)) {
5932
            $sessionInfo = api_get_session_info($sessionId);
5933
            if (!empty($sessionInfo)) {
5934
                $sessionData = '<tr>'
5935
                    .'<td>'.get_lang('Session name').'</td>'
5936
                    .'<td>'.$sessionInfo['name'].'</td>'
5937
                    .'</tr>';
5938
            }
5939
        }
5940
5941
        $sendStart = false;
5942
        $sendEnd = false;
5943
        $sendEndOpenQuestion = false;
5944
        $sendEndOralQuestion = false;
5945
5946
        foreach ($setting as $option) {
5947
            switch ($option) {
5948
                case 0:
5949
                    return false;
5950
5951
                    break;
5952
                case 1: // End
5953
                    if ('end' == $type) {
5954
                        $sendEnd = true;
5955
                    }
5956
5957
                    break;
5958
                case 2: // start
5959
                    if ('start' == $type) {
5960
                        $sendStart = true;
5961
                    }
5962
5963
                    break;
5964
                case 3: // end + open
5965
                    if ('end' == $type) {
5966
                        $sendEndOpenQuestion = true;
5967
                    }
5968
5969
                    break;
5970
                case 4: // end + oral
5971
                    if ('end' == $type) {
5972
                        $sendEndOralQuestion = true;
5973
                    }
5974
5975
                    break;
5976
            }
5977
        }
5978
5979
        $user_info = api_get_user_info(api_get_user_id());
5980
        $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_show.php?'.
5981
            api_get_cidreq(true, true, 'qualify').'&id='.$exe_id.'&action=qualify';
5982
5983
        if (!empty($sessionId)) {
5984
            $addGeneralCoach = true;
5985
            $setting = api_get_configuration_value('block_quiz_mail_notification_general_coach');
5986
            if (true === $setting) {
5987
                $addGeneralCoach = false;
5988
            }
5989
            $teachers = CourseManager::get_coach_list_from_course_code(
5990
                $courseCode,
5991
                $sessionId,
5992
                $addGeneralCoach
5993
            );
5994
        } else {
5995
            $teachers = CourseManager::get_teacher_list_from_course_code($courseCode);
5996
        }
5997
5998
        if ($sendEndOpenQuestion) {
5999
            $this->sendNotificationForOpenQuestions(
6000
                $question_list_answers,
6001
                $origin,
6002
                $user_info,
6003
                $url,
6004
                $teachers
6005
            );
6006
        }
6007
6008
        if ($sendEndOralQuestion) {
6009
            $this->sendNotificationForOralQuestions(
6010
                $question_list_answers,
6011
                $origin,
6012
                $exe_id,
6013
                $user_info,
6014
                $url,
6015
                $teachers
6016
            );
6017
        }
6018
6019
        if (!$sendEnd && !$sendStart) {
6020
            return false;
6021
        }
6022
6023
        $scoreLabel = '';
6024
        if ($sendEnd &&
6025
            true == api_get_configuration_value('send_score_in_exam_notification_mail_to_manager')
6026
        ) {
6027
            $notificationPercentage = api_get_configuration_value('send_notification_score_in_percentage');
6028
            $scoreLabel = ExerciseLib::show_score($score, $weight, $notificationPercentage, true);
6029
            $scoreLabel = '<tr>
6030
                            <td>'.get_lang('Score')."</td>
6031
                            <td>&nbsp;$scoreLabel</td>
6032
                        </tr>";
6033
        }
6034
6035
        if ($sendEnd) {
6036
            $msg = get_lang('A learner attempted an exercise').'<br /><br />';
6037
        } else {
6038
            $msg = get_lang('Student just started an exercise').'<br /><br />';
6039
        }
6040
6041
        $msg .= get_lang('Attempt details').' : <br /><br />
6042
                    <table>
6043
                        <tr>
6044
                            <td>'.get_lang('Course name').'</td>
6045
                            <td>#course#</td>
6046
                        </tr>
6047
                        '.$sessionData.'
6048
                        <tr>
6049
                            <td>'.get_lang('Test').'</td>
6050
                            <td>&nbsp;#exercise#</td>
6051
                        </tr>
6052
                        <tr>
6053
                            <td>'.get_lang('Learner name').'</td>
6054
                            <td>&nbsp;#student_complete_name#</td>
6055
                        </tr>
6056
                        <tr>
6057
                            <td>'.get_lang('Learner e-mail').'</td>
6058
                            <td>&nbsp;#email#</td>
6059
                        </tr>
6060
                        '.$scoreLabel.'
6061
                    </table>';
6062
6063
        $variables = [
6064
            '#email#' => $user_info['email'],
6065
            '#exercise#' => $this->exercise,
6066
            '#student_complete_name#' => $user_info['complete_name'],
6067
            '#course#' => Display::url(
6068
                $courseInfo['title'],
6069
                $courseInfo['course_public_url'].'?sid='.$sessionId
6070
            ),
6071
        ];
6072
6073
        if ($sendEnd) {
6074
            $msg .= '<br /><a href="#url#">'.get_lang('Click this link to check the answer and/or give feedback').'</a>';
6075
            $variables['#url#'] = $url;
6076
        }
6077
6078
        $content = str_replace(array_keys($variables), array_values($variables), $msg);
6079
6080
        if ($sendEnd) {
6081
            $subject = get_lang('A learner attempted an exercise');
6082
        } else {
6083
            $subject = get_lang('Student just started an exercise');
6084
        }
6085
6086
        if (!empty($teachers)) {
6087
            foreach ($teachers as $user_id => $teacher_data) {
6088
                MessageManager::send_message_simple(
6089
                    $user_id,
6090
                    $subject,
6091
                    $content
6092
                );
6093
            }
6094
        }
6095
    }
6096
6097
    /**
6098
     * @param array $user_data         result of api_get_user_info()
6099
     * @param array $trackExerciseInfo result of get_stat_track_exercise_info
6100
     * @param bool  $saveUserResult
6101
     *
6102
     * @return string
6103
     */
6104
    public function showExerciseResultHeader(
6105
        $user_data,
6106
        $trackExerciseInfo,
6107
        $saveUserResult
6108
    ) {
6109
        if (api_get_configuration_value('hide_user_info_in_quiz_result')) {
6110
            return '';
6111
        }
6112
6113
        $start_date = null;
6114
6115
        if (isset($trackExerciseInfo['start_date'])) {
6116
            $start_date = api_convert_and_format_date($trackExerciseInfo['start_date']);
6117
        }
6118
        $duration = isset($trackExerciseInfo['duration_formatted']) ? $trackExerciseInfo['duration_formatted'] : null;
6119
        $ip = isset($trackExerciseInfo['user_ip']) ? $trackExerciseInfo['user_ip'] : null;
6120
6121
        if (!empty($user_data)) {
6122
            $userFullName = $user_data['complete_name'];
6123
            if (api_is_teacher() || api_is_platform_admin(true, true)) {
6124
                $userFullName = '<a href="'.$user_data['profile_url'].'" title="'.get_lang('GoToStudentDetails').'">'.
6125
                    $user_data['complete_name'].'</a>';
6126
            }
6127
6128
            $data = [
6129
                'name_url' => $userFullName,
6130
                'complete_name' => $user_data['complete_name'],
6131
                'username' => $user_data['username'],
6132
                'avatar' => $user_data['avatar_medium'],
6133
                'url' => $user_data['profile_url'],
6134
            ];
6135
6136
            if (!empty($user_data['official_code'])) {
6137
                $data['code'] = $user_data['official_code'];
6138
            }
6139
        }
6140
        // Description can be very long and is generally meant to explain
6141
        //   rules *before* the exam. Leaving here to make display easier if
6142
        //   necessary
6143
        /*
6144
        if (!empty($this->description)) {
6145
            $array[] = array('title' => get_lang("Description"), 'content' => $this->description);
6146
        }
6147
        */
6148
        if (!empty($start_date)) {
6149
            $data['start_date'] = $start_date;
6150
        }
6151
6152
        if (!empty($duration)) {
6153
            $data['duration'] = $duration;
6154
        }
6155
6156
        if (!empty($ip)) {
6157
            $data['ip'] = $ip;
6158
        }
6159
6160
        if (api_get_configuration_value('save_titles_as_html')) {
6161
            $data['title'] = $this->get_formated_title().get_lang('Result');
6162
        } else {
6163
            $data['title'] = PHP_EOL.$this->exercise.' : '.get_lang('Result');
6164
        }
6165
6166
        $questionsCount = count(explode(',', $trackExerciseInfo['data_tracking']));
6167
        $savedAnswersCount = $this->countUserAnswersSavedInExercise($trackExerciseInfo['exe_id']);
6168
6169
        $data['number_of_answers'] = $questionsCount;
6170
        $data['number_of_answers_saved'] = $savedAnswersCount;
6171
6172
        if (false !== api_get_configuration_value('quiz_confirm_saved_answers')) {
6173
            $em = Database::getManager();
6174
6175
            if ($saveUserResult) {
6176
                $trackConfirmation = new TrackEExerciseConfirmation();
6177
                $trackConfirmation
6178
                    ->setUserId($trackExerciseInfo['exe_user_id'])
6179
                    ->setQuizId($trackExerciseInfo['exe_exo_id'])
6180
                    ->setAttemptId($trackExerciseInfo['exe_id'])
6181
                    ->setQuestionsCount($questionsCount)
6182
                    ->setSavedAnswersCount($savedAnswersCount)
6183
                    ->setCourseId($trackExerciseInfo['c_id'])
6184
                    ->setSessionId($trackExerciseInfo['session_id'])
6185
                    ->setCreatedAt(api_get_utc_datetime(null, false, true));
6186
6187
                $em->persist($trackConfirmation);
6188
                $em->flush();
6189
            } else {
6190
                $trackConfirmation = $em
6191
                    ->getRepository('ChamiloCoreBundle:TrackEExerciseConfirmation')
6192
                    ->findOneBy(
6193
                        [
6194
                            'attemptId' => $trackExerciseInfo['exe_id'],
6195
                            'quizId' => $trackExerciseInfo['exe_exo_id'],
6196
                            'courseId' => $trackExerciseInfo['c_id'],
6197
                            'sessionId' => $trackExerciseInfo['session_id'],
6198
                        ]
6199
                    );
6200
            }
6201
6202
            $data['track_confirmation'] = $trackConfirmation;
6203
        }
6204
6205
        $tpl = new Template(null, false, false, false, false, false, false);
6206
        $tpl->assign('data', $data);
6207
        $layoutTemplate = $tpl->get_template('exercise/partials/result_exercise.tpl');
6208
6209
        return $tpl->fetch($layoutTemplate);
6210
    }
6211
6212
    /**
6213
     * Returns the exercise result.
6214
     *
6215
     * @param 	int		attempt id
6216
     *
6217
     * @return array
6218
     */
6219
    public function get_exercise_result($exe_id)
6220
    {
6221
        $result = [];
6222
        $track_exercise_info = ExerciseLib::get_exercise_track_exercise_info($exe_id);
6223
6224
        if (!empty($track_exercise_info)) {
6225
            $totalScore = 0;
6226
            $objExercise = new self();
6227
            $objExercise->read($track_exercise_info['exe_exo_id']);
6228
            if (!empty($track_exercise_info['data_tracking'])) {
6229
                $question_list = explode(',', $track_exercise_info['data_tracking']);
6230
            }
6231
            foreach ($question_list as $questionId) {
6232
                $question_result = $objExercise->manage_answer(
6233
                    $exe_id,
6234
                    $questionId,
6235
                    '',
6236
                    'exercise_show',
6237
                    [],
6238
                    false,
6239
                    true,
6240
                    false,
6241
                    $objExercise->selectPropagateNeg()
6242
                );
6243
                $totalScore += $question_result['score'];
6244
            }
6245
6246
            if (0 == $objExercise->selectPropagateNeg() && $totalScore < 0) {
6247
                $totalScore = 0;
6248
            }
6249
            $result = [
6250
                'score' => $totalScore,
6251
                'weight' => $track_exercise_info['max_score'],
6252
            ];
6253
        }
6254
6255
        return $result;
6256
    }
6257
6258
    /**
6259
     * Checks if the exercise is visible due a lot of conditions
6260
     * visibility, time limits, student attempts
6261
     * Return associative array
6262
     * value : true if exercise visible
6263
     * message : HTML formatted message
6264
     * rawMessage : text message.
6265
     *
6266
     * @param int  $lpId
6267
     * @param int  $lpItemId
6268
     * @param int  $lpItemViewId
6269
     * @param bool $filterByAdmin
6270
     *
6271
     * @return array
6272
     */
6273
    public function is_visible(
6274
        $lpId = 0,
6275
        $lpItemId = 0,
6276
        $lpItemViewId = 0,
6277
        $filterByAdmin = true
6278
    ) {
6279
        // 1. By default the exercise is visible
6280
        $isVisible = true;
6281
        $message = null;
6282
6283
        // 1.1 Admins and teachers can access to the exercise
6284
        if ($filterByAdmin) {
6285
            if (api_is_platform_admin() || api_is_course_admin() || api_is_course_tutor()) {
6286
                return ['value' => true, 'message' => ''];
6287
            }
6288
        }
6289
6290
        // Deleted exercise.
6291
        if (-1 == $this->active) {
6292
            return [
6293
                'value' => false,
6294
                'message' => Display::return_message(
6295
                    get_lang('TestNotFound'),
6296
                    'warning',
6297
                    false
6298
                ),
6299
                'rawMessage' => get_lang('TestNotFound'),
6300
            ];
6301
        }
6302
6303
        $repo = Container::getExerciseRepository();
6304
        $exercise = $repo->find($this->iId);
6305
6306
        if (null === $exercise) {
6307
            return [];
6308
        }
6309
6310
        $link = $exercise->getFirstResourceLinkFromCourseSession(api_get_course_entity($this->course_id));
6311
6312
        if ($link->isDraft()) {
6313
            $this->active = 0;
6314
        }
6315
6316
        // 2. If the exercise is not active.
6317
        if (empty($lpId)) {
6318
            // 2.1 LP is OFF
6319
            if (0 == $this->active) {
6320
                return [
6321
                    'value' => false,
6322
                    'message' => Display::return_message(
6323
                        get_lang('TestNotFound'),
6324
                        'warning',
6325
                        false
6326
                    ),
6327
                    'rawMessage' => get_lang('TestNotFound'),
6328
                ];
6329
            }
6330
        } else {
6331
            $lp = Container::getLpRepository()->find($lpId);
6332
            // 2.1 LP is loaded
6333
            if ($lp && 0 == $this->active &&
6334
                !learnpath::is_lp_visible_for_student($lp, api_get_user_id())
6335
            ) {
6336
                return [
6337
                    'value' => false,
6338
                    'message' => Display::return_message(
6339
                        get_lang('TestNotFound'),
6340
                        'warning',
6341
                        false
6342
                    ),
6343
                    'rawMessage' => get_lang('TestNotFound'),
6344
                ];
6345
            }
6346
        }
6347
6348
        // 3. We check if the time limits are on
6349
        $limitTimeExists = false;
6350
        if (!empty($this->start_time) || !empty($this->end_time)) {
6351
            $limitTimeExists = true;
6352
        }
6353
6354
        if ($limitTimeExists) {
6355
            $timeNow = time();
6356
            $existsStartDate = false;
6357
            $nowIsAfterStartDate = true;
6358
            $existsEndDate = false;
6359
            $nowIsBeforeEndDate = true;
6360
6361
            if (!empty($this->start_time)) {
6362
                $existsStartDate = true;
6363
            }
6364
6365
            if (!empty($this->end_time)) {
6366
                $existsEndDate = true;
6367
            }
6368
6369
            // check if we are before-or-after end-or-start date
6370
            if ($existsStartDate && $timeNow < api_strtotime($this->start_time, 'UTC')) {
6371
                $nowIsAfterStartDate = false;
6372
            }
6373
6374
            if ($existsEndDate & $timeNow >= api_strtotime($this->end_time, 'UTC')) {
6375
                $nowIsBeforeEndDate = false;
6376
            }
6377
6378
            // lets check all cases
6379
            if ($existsStartDate && !$existsEndDate) {
6380
                // exists start date and dont exists end date
6381
                if ($nowIsAfterStartDate) {
6382
                    // after start date, no end date
6383
                    $isVisible = true;
6384
                    $message = sprintf(
6385
                        get_lang('TestAvailableSinceX'),
6386
                        api_convert_and_format_date($this->start_time)
6387
                    );
6388
                } else {
6389
                    // before start date, no end date
6390
                    $isVisible = false;
6391
                    $message = sprintf(
6392
                        get_lang('TestAvailableFromX'),
6393
                        api_convert_and_format_date($this->start_time)
6394
                    );
6395
                }
6396
            } elseif (!$existsStartDate && $existsEndDate) {
6397
                // doesnt exist start date, exists end date
6398
                if ($nowIsBeforeEndDate) {
6399
                    // before end date, no start date
6400
                    $isVisible = true;
6401
                    $message = sprintf(
6402
                        get_lang('TestAvailableUntilX'),
6403
                        api_convert_and_format_date($this->end_time)
6404
                    );
6405
                } else {
6406
                    // after end date, no start date
6407
                    $isVisible = false;
6408
                    $message = sprintf(
6409
                        get_lang('TestAvailableUntilX'),
6410
                        api_convert_and_format_date($this->end_time)
6411
                    );
6412
                }
6413
            } elseif ($existsStartDate && $existsEndDate) {
6414
                // exists start date and end date
6415
                if ($nowIsAfterStartDate) {
6416
                    if ($nowIsBeforeEndDate) {
6417
                        // after start date and before end date
6418
                        $isVisible = true;
6419
                        $message = sprintf(
6420
                            get_lang('TestIsActivatedFromXToY'),
6421
                            api_convert_and_format_date($this->start_time),
6422
                            api_convert_and_format_date($this->end_time)
6423
                        );
6424
                    } else {
6425
                        // after start date and after end date
6426
                        $isVisible = false;
6427
                        $message = sprintf(
6428
                            get_lang('TestWasActivatedFromXToY'),
6429
                            api_convert_and_format_date($this->start_time),
6430
                            api_convert_and_format_date($this->end_time)
6431
                        );
6432
                    }
6433
                } else {
6434
                    if ($nowIsBeforeEndDate) {
6435
                        // before start date and before end date
6436
                        $isVisible = false;
6437
                        $message = sprintf(
6438
                            get_lang('TestWillBeActivatedFromXToY'),
6439
                            api_convert_and_format_date($this->start_time),
6440
                            api_convert_and_format_date($this->end_time)
6441
                        );
6442
                    }
6443
                    // case before start date and after end date is impossible
6444
                }
6445
            } elseif (!$existsStartDate && !$existsEndDate) {
6446
                // doesnt exist start date nor end date
6447
                $isVisible = true;
6448
                $message = '';
6449
            }
6450
        }
6451
6452
        // 4. We check if the student have attempts
6453
        if ($isVisible) {
6454
            $exerciseAttempts = $this->selectAttempts();
6455
6456
            if ($exerciseAttempts > 0) {
6457
                $attemptCount = Event::get_attempt_count_not_finished(
6458
                    api_get_user_id(),
6459
                    $this->getId(),
6460
                    $lpId,
6461
                    $lpItemId,
6462
                    $lpItemViewId
6463
                );
6464
6465
                if ($attemptCount >= $exerciseAttempts) {
6466
                    $message = sprintf(
6467
                        get_lang('Reachedmax. 20 characters, e.g. <i>INNOV21</i>Attempts'),
6468
                        $this->name,
6469
                        $exerciseAttempts
6470
                    );
6471
                    $isVisible = false;
6472
                }
6473
            }
6474
        }
6475
6476
        $rawMessage = '';
6477
        if (!empty($message)) {
6478
            $rawMessage = $message;
6479
            $message = Display::return_message($message, 'warning', false);
6480
        }
6481
6482
        return [
6483
            'value' => $isVisible,
6484
            'message' => $message,
6485
            'rawMessage' => $rawMessage,
6486
        ];
6487
    }
6488
6489
    /**
6490
     * @return bool
6491
     */
6492
    public function added_in_lp()
6493
    {
6494
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
6495
        $sql = "SELECT max_score FROM $TBL_LP_ITEM
6496
                WHERE
6497
                    c_id = {$this->course_id} AND
6498
                    item_type = '".TOOL_QUIZ."' AND
6499
                    path = '{$this->getId()}'";
6500
        $result = Database::query($sql);
6501
        if (Database::num_rows($result) > 0) {
6502
            return true;
6503
        }
6504
6505
        return false;
6506
    }
6507
6508
    /**
6509
     * Returns an array with this form.
6510
     *
6511
     * @example
6512
     * <code>
6513
     * array (size=3)
6514
     * 999 =>
6515
     * array (size=3)
6516
     * 0 => int 3422
6517
     * 1 => int 3423
6518
     * 2 => int 3424
6519
     * 100 =>
6520
     * array (size=2)
6521
     * 0 => int 3469
6522
     * 1 => int 3470
6523
     * 101 =>
6524
     * array (size=1)
6525
     * 0 => int 3482
6526
     * </code>
6527
     * The array inside the key 999 means the question list that belongs to the media id = 999,
6528
     * this case is special because 999 means "no media".
6529
     *
6530
     * @return array
6531
     */
6532
    public function getMediaList()
6533
    {
6534
        return $this->mediaList;
6535
    }
6536
6537
    /**
6538
     * Is media question activated?
6539
     *
6540
     * @return bool
6541
     */
6542
    public function mediaIsActivated()
6543
    {
6544
        $mediaQuestions = $this->getMediaList();
6545
        $active = false;
6546
        if (isset($mediaQuestions) && !empty($mediaQuestions)) {
6547
            $media_count = count($mediaQuestions);
6548
            if ($media_count > 1) {
6549
                return true;
6550
            } elseif (1 == $media_count) {
6551
                if (isset($mediaQuestions[999])) {
6552
                    return false;
6553
                } else {
6554
                    return true;
6555
                }
6556
            }
6557
        }
6558
6559
        return $active;
6560
    }
6561
6562
    /**
6563
     * Gets question list from the exercise.
6564
     *
6565
     * @return array
6566
     */
6567
    public function getQuestionList()
6568
    {
6569
        return $this->questionList;
6570
    }
6571
6572
    /**
6573
     * Question list with medias compressed like this.
6574
     *
6575
     * @example
6576
     * <code>
6577
     * array(
6578
     *      question_id_1,
6579
     *      question_id_2,
6580
     *      media_id, <- this media id contains question ids
6581
     *      question_id_3,
6582
     * )
6583
     * </code>
6584
     *
6585
     * @return array
6586
     */
6587
    public function getQuestionListWithMediasCompressed()
6588
    {
6589
        return $this->questionList;
6590
    }
6591
6592
    /**
6593
     * Question list with medias uncompressed like this.
6594
     *
6595
     * @example
6596
     * <code>
6597
     * array(
6598
     *      question_id,
6599
     *      question_id,
6600
     *      question_id, <- belongs to a media id
6601
     *      question_id, <- belongs to a media id
6602
     *      question_id,
6603
     * )
6604
     * </code>
6605
     *
6606
     * @return array
6607
     */
6608
    public function getQuestionListWithMediasUncompressed()
6609
    {
6610
        return $this->questionListUncompressed;
6611
    }
6612
6613
    /**
6614
     * Sets the question list when the exercise->read() is executed.
6615
     *
6616
     * @param bool $adminView Whether to view the set the list of *all* questions or just the normal student view
6617
     */
6618
    public function setQuestionList($adminView = false)
6619
    {
6620
        // Getting question list.
6621
        $questionList = $this->selectQuestionList(true, $adminView);
6622
        $this->setMediaList($questionList);
6623
        $this->questionList = $this->transformQuestionListWithMedias(
6624
            $questionList,
6625
            false
6626
        );
6627
        $this->questionListUncompressed = $this->transformQuestionListWithMedias(
6628
            $questionList,
6629
            true
6630
        );
6631
    }
6632
6633
    /**
6634
     * @params array question list
6635
     * @params bool expand or not question list (true show all questions,
6636
     * false show media question id instead of the question ids)
6637
     */
6638
    public function transformQuestionListWithMedias(
6639
        $question_list,
6640
        $expand_media_questions = false
6641
    ) {
6642
        $new_question_list = [];
6643
        if (!empty($question_list)) {
6644
            $media_questions = $this->getMediaList();
6645
            $media_active = $this->mediaIsActivated($media_questions);
6646
6647
            if ($media_active) {
6648
                $counter = 1;
6649
                foreach ($question_list as $question_id) {
6650
                    $add_question = true;
6651
                    foreach ($media_questions as $media_id => $question_list_in_media) {
6652
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
6653
                            $add_question = false;
6654
                            if (!in_array($media_id, $new_question_list)) {
6655
                                $new_question_list[$counter] = $media_id;
6656
                                $counter++;
6657
                            }
6658
6659
                            break;
6660
                        }
6661
                    }
6662
                    if ($add_question) {
6663
                        $new_question_list[$counter] = $question_id;
6664
                        $counter++;
6665
                    }
6666
                }
6667
                if ($expand_media_questions) {
6668
                    $media_key_list = array_keys($media_questions);
6669
                    foreach ($new_question_list as &$question_id) {
6670
                        if (in_array($question_id, $media_key_list)) {
6671
                            $question_id = $media_questions[$question_id];
6672
                        }
6673
                    }
6674
                    $new_question_list = array_flatten($new_question_list);
6675
                }
6676
            } else {
6677
                $new_question_list = $question_list;
6678
            }
6679
        }
6680
6681
        return $new_question_list;
6682
    }
6683
6684
    /**
6685
     * Get question list depend on the random settings.
6686
     *
6687
     * @return array
6688
     */
6689
    public function get_validated_question_list()
6690
    {
6691
        $isRandomByCategory = $this->isRandomByCat();
6692
        if (0 == $isRandomByCategory) {
6693
            if ($this->isRandom()) {
6694
                return $this->getRandomList();
6695
            }
6696
6697
            return $this->selectQuestionList();
6698
        }
6699
6700
        if ($this->isRandom()) {
6701
            // USE question categories
6702
            // get questions by category for this exercise
6703
            // we have to choice $objExercise->random question in each array values of $tabCategoryQuestions
6704
            // key of $tabCategoryQuestions are the categopy id (0 for not in a category)
6705
            // value is the array of question id of this category
6706
            $questionList = [];
6707
            $tabCategoryQuestions = TestCategory::getQuestionsByCat($this->getId());
6708
            $isRandomByCategory = $this->getRandomByCategory();
6709
            // We sort categories based on the term between [] in the head
6710
            // of the category's description
6711
            /* examples of categories :
6712
             * [biologie] Maitriser les mecanismes de base de la genetique
6713
             * [biologie] Relier les moyens de depenses et les agents infectieux
6714
             * [biologie] Savoir ou est produite l'enrgie dans les cellules et sous quelle forme
6715
             * [chimie] Classer les molles suivant leur pouvoir oxydant ou reacteur
6716
             * [chimie] Connaître la denition de la theoie acide/base selon Brönsted
6717
             * [chimie] Connaître les charges des particules
6718
             * We want that in the order of the groups defined by the term
6719
             * between brackets at the beginning of the category title
6720
            */
6721
            // If test option is Grouped By Categories
6722
            if (2 == $isRandomByCategory) {
6723
                $tabCategoryQuestions = TestCategory::sortTabByBracketLabel($tabCategoryQuestions);
6724
            }
6725
            foreach ($tabCategoryQuestions as $tabquestion) {
6726
                $number_of_random_question = $this->random;
6727
                if (-1 == $this->random) {
6728
                    $number_of_random_question = count($this->questionList);
6729
                }
6730
                $questionList = array_merge(
6731
                    $questionList,
6732
                    TestCategory::getNElementsFromArray(
6733
                        $tabquestion,
6734
                        $number_of_random_question
6735
                    )
6736
                );
6737
            }
6738
            // shuffle the question list if test is not grouped by categories
6739
            if (1 == $isRandomByCategory) {
6740
                shuffle($questionList); // or not
6741
            }
6742
6743
            return $questionList;
6744
        }
6745
6746
        // Problem, random by category has been selected and
6747
        // we have no $this->isRandom number of question selected
6748
        // Should not happened
6749
6750
        return [];
6751
    }
6752
6753
    public function get_question_list($expand_media_questions = false)
6754
    {
6755
        $question_list = $this->get_validated_question_list();
6756
6757
        return $this->transform_question_list_with_medias($question_list, $expand_media_questions);
6758
    }
6759
6760
    public function transform_question_list_with_medias($question_list, $expand_media_questions = false)
6761
    {
6762
        $new_question_list = [];
6763
        if (!empty($question_list)) {
6764
            $media_questions = $this->getMediaList();
6765
            $media_active = $this->mediaIsActivated($media_questions);
6766
6767
            if ($media_active) {
6768
                $counter = 1;
6769
                foreach ($question_list as $question_id) {
6770
                    $add_question = true;
6771
                    foreach ($media_questions as $media_id => $question_list_in_media) {
6772
                        if (999 != $media_id && in_array($question_id, $question_list_in_media)) {
6773
                            $add_question = false;
6774
                            if (!in_array($media_id, $new_question_list)) {
6775
                                $new_question_list[$counter] = $media_id;
6776
                                $counter++;
6777
                            }
6778
6779
                            break;
6780
                        }
6781
                    }
6782
                    if ($add_question) {
6783
                        $new_question_list[$counter] = $question_id;
6784
                        $counter++;
6785
                    }
6786
                }
6787
                if ($expand_media_questions) {
6788
                    $media_key_list = array_keys($media_questions);
6789
                    foreach ($new_question_list as &$question_id) {
6790
                        if (in_array($question_id, $media_key_list)) {
6791
                            $question_id = $media_questions[$question_id];
6792
                        }
6793
                    }
6794
                    $new_question_list = array_flatten($new_question_list);
6795
                }
6796
            } else {
6797
                $new_question_list = $question_list;
6798
            }
6799
        }
6800
6801
        return $new_question_list;
6802
    }
6803
6804
    /**
6805
     * @param int $exe_id
6806
     *
6807
     * @return array
6808
     */
6809
    public function get_stat_track_exercise_info_by_exe_id($exe_id)
6810
    {
6811
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6812
        $exe_id = (int) $exe_id;
6813
        $sql_track = "SELECT * FROM $table WHERE exe_id = $exe_id ";
6814
        $result = Database::query($sql_track);
6815
        $new_array = [];
6816
        if (Database::num_rows($result) > 0) {
6817
            $new_array = Database::fetch_array($result, 'ASSOC');
6818
            $start_date = api_get_utc_datetime($new_array['start_date'], true);
6819
            $end_date = api_get_utc_datetime($new_array['exe_date'], true);
6820
            $new_array['duration_formatted'] = '';
6821
            if (!empty($new_array['exe_duration']) && !empty($start_date) && !empty($end_date)) {
6822
                $time = api_format_time($new_array['exe_duration'], 'js');
6823
                $new_array['duration_formatted'] = $time;
6824
            }
6825
        }
6826
6827
        return $new_array;
6828
    }
6829
6830
    /**
6831
     * @param int $exeId
6832
     *
6833
     * @return bool
6834
     */
6835
    public function removeAllQuestionToRemind($exeId)
6836
    {
6837
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6838
        $exeId = (int) $exeId;
6839
        if (empty($exeId)) {
6840
            return false;
6841
        }
6842
        $sql = "UPDATE $table
6843
                SET questions_to_check = ''
6844
                WHERE exe_id = $exeId ";
6845
        Database::query($sql);
6846
6847
        return true;
6848
    }
6849
6850
    /**
6851
     * @param int   $exeId
6852
     * @param array $questionList
6853
     *
6854
     * @return bool
6855
     */
6856
    public function addAllQuestionToRemind($exeId, $questionList = [])
6857
    {
6858
        $exeId = (int) $exeId;
6859
        if (empty($questionList)) {
6860
            return false;
6861
        }
6862
6863
        $questionListToString = implode(',', $questionList);
6864
        $questionListToString = Database::escape_string($questionListToString);
6865
6866
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6867
        $sql = "UPDATE $table
6868
                SET questions_to_check = '$questionListToString'
6869
                WHERE exe_id = $exeId";
6870
        Database::query($sql);
6871
6872
        return true;
6873
    }
6874
6875
    /**
6876
     * @param int    $exe_id
6877
     * @param int    $question_id
6878
     * @param string $action
6879
     */
6880
    public function editQuestionToRemind($exe_id, $question_id, $action = 'add')
6881
    {
6882
        $exercise_info = self::get_stat_track_exercise_info_by_exe_id($exe_id);
6883
        $question_id = (int) $question_id;
6884
        $exe_id = (int) $exe_id;
6885
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6886
        if ($exercise_info) {
6887
            if (empty($exercise_info['questions_to_check'])) {
6888
                if ('add' == $action) {
6889
                    $sql = "UPDATE $track_exercises
6890
                            SET questions_to_check = '$question_id'
6891
                            WHERE exe_id = $exe_id ";
6892
                    Database::query($sql);
6893
                }
6894
            } else {
6895
                $remind_list = explode(',', $exercise_info['questions_to_check']);
6896
                $remind_list_string = '';
6897
                if ('add' == $action) {
6898
                    if (!in_array($question_id, $remind_list)) {
6899
                        $newRemindList = [];
6900
                        $remind_list[] = $question_id;
6901
                        $questionListInSession = Session::read('questionList');
6902
                        if (!empty($questionListInSession)) {
6903
                            foreach ($questionListInSession as $originalQuestionId) {
6904
                                if (in_array($originalQuestionId, $remind_list)) {
6905
                                    $newRemindList[] = $originalQuestionId;
6906
                                }
6907
                            }
6908
                        }
6909
                        $remind_list_string = implode(',', $newRemindList);
6910
                    }
6911
                } elseif ('delete' == $action) {
6912
                    if (!empty($remind_list)) {
6913
                        if (in_array($question_id, $remind_list)) {
6914
                            $remind_list = array_flip($remind_list);
6915
                            unset($remind_list[$question_id]);
6916
                            $remind_list = array_flip($remind_list);
6917
6918
                            if (!empty($remind_list)) {
6919
                                sort($remind_list);
6920
                                array_filter($remind_list);
6921
                                $remind_list_string = implode(',', $remind_list);
6922
                            }
6923
                        }
6924
                    }
6925
                }
6926
                $value = Database::escape_string($remind_list_string);
6927
                $sql = "UPDATE $track_exercises
6928
                        SET questions_to_check = '$value'
6929
                        WHERE exe_id = $exe_id ";
6930
                Database::query($sql);
6931
            }
6932
        }
6933
    }
6934
6935
    /**
6936
     * @param string $answer
6937
     */
6938
    public function fill_in_blank_answer_to_array($answer)
6939
    {
6940
        api_preg_match_all('/\[[^]]+\]/', $answer, $teacher_answer_list);
6941
6942
        return $teacher_answer_list[0];
6943
    }
6944
6945
    /**
6946
     * @param string $answer
6947
     *
6948
     * @return string
6949
     */
6950
    public function fill_in_blank_answer_to_string($answer)
6951
    {
6952
        $teacher_answer_list = $this->fill_in_blank_answer_to_array($answer);
6953
        $result = '';
6954
        if (!empty($teacher_answer_list)) {
6955
            $i = 0;
6956
            foreach ($teacher_answer_list as $teacher_item) {
6957
                $value = null;
6958
                //Cleaning student answer list
6959
                $value = strip_tags($teacher_item);
6960
                $value = api_substr($value, 1, api_strlen($value) - 2);
6961
                $value = explode('/', $value);
6962
                if (!empty($value[0])) {
6963
                    $value = trim($value[0]);
6964
                    $value = str_replace('&nbsp;', '', $value);
6965
                    $result .= $value;
6966
                }
6967
            }
6968
        }
6969
6970
        return $result;
6971
    }
6972
6973
    /**
6974
     * @return string
6975
     */
6976
    public function returnTimeLeftDiv()
6977
    {
6978
        $html = '<div id="clock_warning" style="display:none">';
6979
        $html .= Display::return_message(
6980
            get_lang('Time limit reached'),
6981
            'warning'
6982
        );
6983
        $html .= ' ';
6984
        $html .= sprintf(
6985
            get_lang('Just a moment, please. You will be redirected in %s seconds...'),
6986
            '<span id="counter_to_redirect" class="red_alert"></span>'
6987
        );
6988
        $html .= '</div>';
6989
        $html .= '<div id="exercise_clock_warning" class="count_down"></div>';
6990
6991
        return $html;
6992
    }
6993
6994
    /**
6995
     * Get categories added in the exercise--category matrix.
6996
     *
6997
     * @return array
6998
     */
6999
    public function getCategoriesInExercise()
7000
    {
7001
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7002
        if (!empty($this->getId())) {
7003
            $sql = "SELECT * FROM $table
7004
                    WHERE exercise_id = {$this->getId()} AND c_id = {$this->course_id} ";
7005
            $result = Database::query($sql);
7006
            $list = [];
7007
            if (Database::num_rows($result)) {
7008
                while ($row = Database::fetch_array($result, 'ASSOC')) {
7009
                    $list[$row['category_id']] = $row;
7010
                }
7011
7012
                return $list;
7013
            }
7014
        }
7015
7016
        return [];
7017
    }
7018
7019
    /**
7020
     * Get total number of question that will be parsed when using the category/exercise.
7021
     *
7022
     * @return int
7023
     */
7024
    public function getNumberQuestionExerciseCategory()
7025
    {
7026
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7027
        if (!empty($this->getId())) {
7028
            $sql = "SELECT SUM(count_questions) count_questions
7029
                    FROM $table
7030
                    WHERE exercise_id = {$this->getId()} AND c_id = {$this->course_id}";
7031
            $result = Database::query($sql);
7032
            if (Database::num_rows($result)) {
7033
                $row = Database::fetch_array($result);
7034
7035
                return (int) $row['count_questions'];
7036
            }
7037
        }
7038
7039
        return 0;
7040
    }
7041
7042
    /**
7043
     * Save categories in the TABLE_QUIZ_REL_CATEGORY table.
7044
     *
7045
     * @param array $categories
7046
     */
7047
    public function save_categories_in_exercise($categories)
7048
    {
7049
        if (!empty($categories) && !empty($this->getId())) {
7050
            $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7051
            $sql = "DELETE FROM $table
7052
                    WHERE exercise_id = {$this->getId()} AND c_id = {$this->course_id}";
7053
            Database::query($sql);
7054
            if (!empty($categories)) {
7055
                foreach ($categories as $categoryId => $countQuestions) {
7056
                    $params = [
7057
                        'c_id' => $this->course_id,
7058
                        'exercise_id' => $this->getId(),
7059
                        'category_id' => $categoryId,
7060
                        'count_questions' => $countQuestions,
7061
                    ];
7062
                    Database::insert($table, $params);
7063
                }
7064
            }
7065
        }
7066
    }
7067
7068
    /**
7069
     * @param array  $questionList
7070
     * @param int    $currentQuestion
7071
     * @param array  $conditions
7072
     * @param string $link
7073
     *
7074
     * @return string
7075
     */
7076
    public function progressExercisePaginationBar(
7077
        $questionList,
7078
        $currentQuestion,
7079
        $conditions,
7080
        $link
7081
    ) {
7082
        $mediaQuestions = $this->getMediaList();
7083
7084
        $html = '<div class="exercise_pagination pagination pagination-mini"><ul>';
7085
        $counter = 0;
7086
        $nextValue = 0;
7087
        $wasMedia = false;
7088
        $before = 0;
7089
        $counterNoMedias = 0;
7090
        foreach ($questionList as $questionId) {
7091
            $isCurrent = $currentQuestion == $counterNoMedias + 1 ? true : false;
7092
7093
            if (!empty($nextValue)) {
7094
                if ($wasMedia) {
7095
                    $nextValue = $nextValue - $before + 1;
7096
                }
7097
            }
7098
7099
            if (isset($mediaQuestions) && isset($mediaQuestions[$questionId])) {
7100
                $fixedValue = $counterNoMedias;
7101
7102
                $html .= Display::progressPaginationBar(
7103
                    $nextValue,
7104
                    $mediaQuestions[$questionId],
7105
                    $currentQuestion,
7106
                    $fixedValue,
7107
                    $conditions,
7108
                    $link,
7109
                    true,
7110
                    true
7111
                );
7112
7113
                $counter += count($mediaQuestions[$questionId]) - 1;
7114
                $before = count($questionList);
7115
                $wasMedia = true;
7116
                $nextValue += count($questionList);
7117
            } else {
7118
                $html .= Display::parsePaginationItem(
7119
                    $questionId,
7120
                    $isCurrent,
7121
                    $conditions,
7122
                    $link,
7123
                    $counter
7124
                );
7125
                $counter++;
7126
                $nextValue++;
7127
                $wasMedia = false;
7128
            }
7129
            $counterNoMedias++;
7130
        }
7131
        $html .= '</ul></div>';
7132
7133
        return $html;
7134
    }
7135
7136
    /**
7137
     *  Shows a list of numbers that represents the question to answer in a exercise.
7138
     *
7139
     * @param array  $categories
7140
     * @param int    $current
7141
     * @param array  $conditions
7142
     * @param string $link
7143
     *
7144
     * @return string
7145
     */
7146
    public function progressExercisePaginationBarWithCategories(
7147
        $categories,
7148
        $current,
7149
        $conditions = [],
7150
        $link = null
7151
    ) {
7152
        $html = null;
7153
        $counterNoMedias = 0;
7154
        $nextValue = 0;
7155
        $wasMedia = false;
7156
        $before = 0;
7157
7158
        if (!empty($categories)) {
7159
            $selectionType = $this->getQuestionSelectionType();
7160
            $useRootAsCategoryTitle = false;
7161
7162
            // Grouping questions per parent category see BT#6540
7163
            if (in_array(
7164
                $selectionType,
7165
                [
7166
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED,
7167
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM,
7168
                ]
7169
            )) {
7170
                $useRootAsCategoryTitle = true;
7171
            }
7172
7173
            // If the exercise is set to only show the titles of the categories
7174
            // at the root of the tree, then pre-order the categories tree by
7175
            // removing children and summing their questions into the parent
7176
            // categories
7177
            if ($useRootAsCategoryTitle) {
7178
                // The new categories list starts empty
7179
                $newCategoryList = [];
7180
                foreach ($categories as $category) {
7181
                    $rootElement = $category['root'];
7182
7183
                    if (isset($category['parent_info'])) {
7184
                        $rootElement = $category['parent_info']['id'];
7185
                    }
7186
7187
                    //$rootElement = $category['id'];
7188
                    // If the current category's ancestor was never seen
7189
                    // before, then declare it and assign the current
7190
                    // category to it.
7191
                    if (!isset($newCategoryList[$rootElement])) {
7192
                        $newCategoryList[$rootElement] = $category;
7193
                    } else {
7194
                        // If it was already seen, then merge the previous with
7195
                        // the current category
7196
                        $oldQuestionList = $newCategoryList[$rootElement]['question_list'];
7197
                        $category['question_list'] = array_merge($oldQuestionList, $category['question_list']);
7198
                        $newCategoryList[$rootElement] = $category;
7199
                    }
7200
                }
7201
                // Now use the newly built categories list, with only parents
7202
                $categories = $newCategoryList;
7203
            }
7204
7205
            foreach ($categories as $category) {
7206
                $questionList = $category['question_list'];
7207
                // Check if in this category there questions added in a media
7208
                $mediaQuestionId = $category['media_question'];
7209
                $isMedia = false;
7210
                $fixedValue = null;
7211
7212
                // Media exists!
7213
                if (999 != $mediaQuestionId) {
7214
                    $isMedia = true;
7215
                    $fixedValue = $counterNoMedias;
7216
                }
7217
7218
                //$categoryName = $category['path']; << show the path
7219
                $categoryName = $category['name'];
7220
7221
                if ($useRootAsCategoryTitle) {
7222
                    if (isset($category['parent_info'])) {
7223
                        $categoryName = $category['parent_info']['title'];
7224
                    }
7225
                }
7226
                $html .= '<div class="row">';
7227
                $html .= '<div class="span2">'.$categoryName.'</div>';
7228
                $html .= '<div class="span8">';
7229
7230
                if (!empty($nextValue)) {
7231
                    if ($wasMedia) {
7232
                        $nextValue = $nextValue - $before + 1;
7233
                    }
7234
                }
7235
                $html .= Display::progressPaginationBar(
7236
                    $nextValue,
7237
                    $questionList,
7238
                    $current,
7239
                    $fixedValue,
7240
                    $conditions,
7241
                    $link,
7242
                    $isMedia,
7243
                    true
7244
                );
7245
                $html .= '</div>';
7246
                $html .= '</div>';
7247
7248
                if (999 == $mediaQuestionId) {
7249
                    $counterNoMedias += count($questionList);
7250
                } else {
7251
                    $counterNoMedias++;
7252
                }
7253
7254
                $nextValue += count($questionList);
7255
                $before = count($questionList);
7256
7257
                if (999 != $mediaQuestionId) {
7258
                    $wasMedia = true;
7259
                } else {
7260
                    $wasMedia = false;
7261
                }
7262
            }
7263
        }
7264
7265
        return $html;
7266
    }
7267
7268
    /**
7269
     * Renders a question list.
7270
     *
7271
     * @param array $questionList    (with media questions compressed)
7272
     * @param int   $currentQuestion
7273
     * @param array $exerciseResult
7274
     * @param array $attemptList
7275
     * @param array $remindList
7276
     */
7277
    public function renderQuestionList(
7278
        $questionList,
7279
        $currentQuestion,
7280
        $exerciseResult,
7281
        $attemptList,
7282
        $remindList
7283
    ) {
7284
        $mediaQuestions = $this->getMediaList();
7285
        $i = 0;
7286
7287
        // Normal question list render (medias compressed)
7288
        foreach ($questionList as $questionId) {
7289
            $i++;
7290
            // For sequential exercises
7291
7292
            if (ONE_PER_PAGE == $this->type) {
7293
                // If it is not the right question, goes to the next loop iteration
7294
                if ($currentQuestion != $i) {
7295
                    continue;
7296
                } else {
7297
                    if (!in_array(
7298
                        $this->getFeedbackType(),
7299
                        [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
7300
                    )) {
7301
                        // if the user has already answered this question
7302
                        if (isset($exerciseResult[$questionId])) {
7303
                            echo Display::return_message(
7304
                                get_lang('You already answered the question'),
7305
                                'normal'
7306
                            );
7307
7308
                            break;
7309
                        }
7310
                    }
7311
                }
7312
            }
7313
7314
            // The $questionList contains the media id we check
7315
            // if this questionId is a media question type
7316
            if (isset($mediaQuestions[$questionId]) &&
7317
                999 != $mediaQuestions[$questionId]
7318
            ) {
7319
                // The question belongs to a media
7320
                $mediaQuestionList = $mediaQuestions[$questionId];
7321
                $objQuestionTmp = Question::read($questionId);
7322
7323
                $counter = 1;
7324
                if (MEDIA_QUESTION == $objQuestionTmp->type) {
7325
                    echo $objQuestionTmp->show_media_content();
7326
7327
                    $countQuestionsInsideMedia = count($mediaQuestionList);
7328
7329
                    // Show questions that belongs to a media
7330
                    if (!empty($mediaQuestionList)) {
7331
                        // In order to parse media questions we use letters a, b, c, etc.
7332
                        $letterCounter = 97;
7333
                        foreach ($mediaQuestionList as $questionIdInsideMedia) {
7334
                            $isLastQuestionInMedia = false;
7335
                            if ($counter == $countQuestionsInsideMedia) {
7336
                                $isLastQuestionInMedia = true;
7337
                            }
7338
                            $this->renderQuestion(
7339
                                $questionIdInsideMedia,
7340
                                $attemptList,
7341
                                $remindList,
7342
                                chr($letterCounter),
7343
                                $currentQuestion,
7344
                                $mediaQuestionList,
7345
                                $isLastQuestionInMedia,
7346
                                $questionList
7347
                            );
7348
                            $letterCounter++;
7349
                            $counter++;
7350
                        }
7351
                    }
7352
                } else {
7353
                    $this->renderQuestion(
7354
                        $questionId,
7355
                        $attemptList,
7356
                        $remindList,
7357
                        $i,
7358
                        $currentQuestion,
7359
                        null,
7360
                        null,
7361
                        $questionList
7362
                    );
7363
                    $i++;
7364
                }
7365
            } else {
7366
                // Normal question render.
7367
                $this->renderQuestion(
7368
                    $questionId,
7369
                    $attemptList,
7370
                    $remindList,
7371
                    $i,
7372
                    $currentQuestion,
7373
                    null,
7374
                    null,
7375
                    $questionList
7376
                );
7377
            }
7378
7379
            // For sequential exercises.
7380
            if (ONE_PER_PAGE == $this->type) {
7381
                // quits the loop
7382
                break;
7383
            }
7384
        }
7385
        // end foreach()
7386
7387
        if (ALL_ON_ONE_PAGE == $this->type) {
7388
            $exercise_actions = $this->show_button($questionId, $currentQuestion);
7389
            echo Display::div($exercise_actions, ['class' => 'exercise_actions']);
7390
        }
7391
    }
7392
7393
    /**
7394
     * @param int   $questionId
7395
     * @param array $attemptList
7396
     * @param array $remindList
7397
     * @param int   $i
7398
     * @param int   $current_question
7399
     * @param array $questions_in_media
7400
     * @param bool  $last_question_in_media
7401
     * @param array $realQuestionList
7402
     * @param bool  $generateJS
7403
     */
7404
    public function renderQuestion(
7405
        $questionId,
7406
        $attemptList,
7407
        $remindList,
7408
        $i,
7409
        $current_question,
7410
        $questions_in_media = [],
7411
        $last_question_in_media = false,
7412
        $realQuestionList,
7413
        $generateJS = true
7414
    ) {
7415
        // With this option on the question is loaded via AJAX
7416
        //$generateJS = true;
7417
        //$this->loadQuestionAJAX = true;
7418
7419
        if ($generateJS && $this->loadQuestionAJAX) {
7420
            $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?a=get_question&id='.$questionId.'&'.api_get_cidreq();
7421
            $params = [
7422
                'questionId' => $questionId,
7423
                'attemptList' => $attemptList,
7424
                'remindList' => $remindList,
7425
                'i' => $i,
7426
                'current_question' => $current_question,
7427
                'questions_in_media' => $questions_in_media,
7428
                'last_question_in_media' => $last_question_in_media,
7429
            ];
7430
            $params = json_encode($params);
7431
7432
            $script = '<script>
7433
            $(function(){
7434
                var params = '.$params.';
7435
                $.ajax({
7436
                    type: "GET",
7437
                    data: params,
7438
                    url: "'.$url.'",
7439
                    success: function(return_value) {
7440
                        $("#ajaxquestiondiv'.$questionId.'").html(return_value);
7441
                    }
7442
                });
7443
            });
7444
            </script>
7445
            <div id="ajaxquestiondiv'.$questionId.'"></div>';
7446
            echo $script;
7447
        } else {
7448
            global $origin;
7449
            $question_obj = Question::read($questionId);
7450
            $user_choice = isset($attemptList[$questionId]) ? $attemptList[$questionId] : null;
7451
            $remind_highlight = null;
7452
7453
            // Hides questions when reviewing a ALL_ON_ONE_PAGE exercise
7454
            // see #4542 no_remind_highlight class hide with jquery
7455
            if (ALL_ON_ONE_PAGE == $this->type && isset($_GET['reminder']) && 2 == $_GET['reminder']) {
7456
                $remind_highlight = 'no_remind_highlight';
7457
                if (in_array($question_obj->type, Question::question_type_no_review())) {
7458
                    return null;
7459
                }
7460
            }
7461
7462
            $attributes = ['id' => 'remind_list['.$questionId.']'];
7463
7464
            // Showing the question
7465
            $exercise_actions = null;
7466
            echo '<a id="questionanchor'.$questionId.'"></a><br />';
7467
            echo '<div id="question_div_'.$questionId.'" class="main_question '.$remind_highlight.'" >';
7468
7469
            // Shows the question + possible answers
7470
            $showTitle = 1 == $this->getHideQuestionTitle() ? false : true;
7471
            echo $this->showQuestion(
7472
                $question_obj,
7473
                false,
7474
                $origin,
7475
                $i,
7476
                $showTitle,
7477
                false,
7478
                $user_choice,
7479
                false,
7480
                null,
7481
                false,
7482
                $this->getModelType(),
7483
                $this->categoryMinusOne
7484
            );
7485
7486
            // Button save and continue
7487
            switch ($this->type) {
7488
                case ONE_PER_PAGE:
7489
                    $exercise_actions .= $this->show_button(
7490
                        $questionId,
7491
                        $current_question,
7492
                        null,
7493
                        $remindList
7494
                    );
7495
7496
                    break;
7497
                case ALL_ON_ONE_PAGE:
7498
                    if (api_is_allowed_to_session_edit()) {
7499
                        $button = [
7500
                            Display::button(
7501
                                'save_now',
7502
                                get_lang('Save and continue'),
7503
                                ['type' => 'button', 'class' => 'btn btn-primary', 'data-question' => $questionId]
7504
                            ),
7505
                            '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>',
7506
                        ];
7507
                        $exercise_actions .= Display::div(
7508
                            implode(PHP_EOL, $button),
7509
                            ['class' => 'exercise_save_now_button']
7510
                        );
7511
                    }
7512
7513
                    break;
7514
            }
7515
7516
            if (!empty($questions_in_media)) {
7517
                $count_of_questions_inside_media = count($questions_in_media);
7518
                if ($count_of_questions_inside_media > 1 && api_is_allowed_to_session_edit()) {
7519
                    $button = [
7520
                        Display::button(
7521
                            'save_now',
7522
                            get_lang('Save and continue'),
7523
                            ['type' => 'button', 'class' => 'btn btn-primary', 'data-question' => $questionId]
7524
                        ),
7525
                        '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>&nbsp;',
7526
                    ];
7527
                    $exercise_actions = Display::div(
7528
                        implode(PHP_EOL, $button),
7529
                        ['class' => 'exercise_save_now_button']
7530
                    );
7531
                }
7532
7533
                if ($last_question_in_media && ONE_PER_PAGE == $this->type) {
7534
                    $exercise_actions = $this->show_button($questionId, $current_question, $questions_in_media);
7535
                }
7536
            }
7537
7538
            // Checkbox review answers
7539
            if ($this->review_answers &&
7540
                !in_array($question_obj->type, Question::question_type_no_review())
7541
            ) {
7542
                $remind_question_div = Display::tag(
7543
                    'label',
7544
                    Display::input(
7545
                        'checkbox',
7546
                        'remind_list['.$questionId.']',
7547
                        '',
7548
                        $attributes
7549
                    ).get_lang('Revise question later'),
7550
                    [
7551
                        'class' => 'checkbox',
7552
                        'for' => 'remind_list['.$questionId.']',
7553
                    ]
7554
                );
7555
                $exercise_actions .= Display::div(
7556
                    $remind_question_div,
7557
                    ['class' => 'exercise_save_now_button']
7558
                );
7559
            }
7560
7561
            echo Display::div(' ', ['class' => 'clear']);
7562
7563
            $paginationCounter = null;
7564
            if (ONE_PER_PAGE == $this->type) {
7565
                if (empty($questions_in_media)) {
7566
                    $paginationCounter = Display::paginationIndicator(
7567
                        $current_question,
7568
                        count($realQuestionList)
7569
                    );
7570
                } else {
7571
                    if ($last_question_in_media) {
7572
                        $paginationCounter = Display::paginationIndicator(
7573
                            $current_question,
7574
                            count($realQuestionList)
7575
                        );
7576
                    }
7577
                }
7578
            }
7579
7580
            echo '<div class="row"><div class="pull-right">'.$paginationCounter.'</div></div>';
7581
            echo Display::div($exercise_actions, ['class' => 'form-actions']);
7582
            echo '</div>';
7583
        }
7584
    }
7585
7586
    /**
7587
     * Returns an array of categories details for the questions of the current
7588
     * exercise.
7589
     *
7590
     * @return array
7591
     */
7592
    public function getQuestionWithCategories()
7593
    {
7594
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
7595
        $categoryRelTable = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
7596
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
7597
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
7598
        $sql = "SELECT DISTINCT cat.*
7599
                FROM $TBL_EXERCICE_QUESTION e
7600
                INNER JOIN $TBL_QUESTIONS q
7601
                ON (e.question_id = q.iid AND e.c_id = q.c_id)
7602
                INNER JOIN $categoryRelTable catRel
7603
                ON (catRel.question_id = e.question_id AND catRel.c_id = e.c_id)
7604
                INNER JOIN $categoryTable cat
7605
                ON (cat.id = catRel.category_id AND cat.c_id = e.c_id)
7606
                WHERE
7607
                  e.c_id = {$this->course_id} AND
7608
                  e.exercice_id	= ".(int) ($this->getId());
7609
7610
        $result = Database::query($sql);
7611
        $categoriesInExercise = [];
7612
        if (Database::num_rows($result)) {
7613
            $categoriesInExercise = Database::store_result($result, 'ASSOC');
7614
        }
7615
7616
        return $categoriesInExercise;
7617
    }
7618
7619
    /**
7620
     * Calculate the max_score of the quiz, depending of question inside, and quiz advanced option.
7621
     */
7622
    public function get_max_score()
7623
    {
7624
        $out_max_score = 0;
7625
        // list of question's id !!! the array key start at 1 !!!
7626
        $questionList = $this->selectQuestionList(true);
7627
7628
        // test is randomQuestions - see field random of test
7629
        if ($this->random > 0 && 0 == $this->randomByCat) {
7630
            $numberRandomQuestions = $this->random;
7631
            $questionScoreList = [];
7632
            foreach ($questionList as $questionId) {
7633
                $tmpobj_question = Question::read($questionId);
7634
                if (is_object($tmpobj_question)) {
7635
                    $questionScoreList[] = $tmpobj_question->weighting;
7636
                }
7637
            }
7638
7639
            rsort($questionScoreList);
7640
            // add the first $numberRandomQuestions value of score array to get max_score
7641
            for ($i = 0; $i < min($numberRandomQuestions, count($questionScoreList)); $i++) {
7642
                $out_max_score += $questionScoreList[$i];
7643
            }
7644
        } elseif ($this->random > 0 && $this->randomByCat > 0) {
7645
            // test is random by category
7646
            // get the $numberRandomQuestions best score question of each category
7647
            $numberRandomQuestions = $this->random;
7648
            $tab_categories_scores = [];
7649
            foreach ($questionList as $questionId) {
7650
                $question_category_id = TestCategory::getCategoryForQuestion($questionId);
7651
                if (!is_array($tab_categories_scores[$question_category_id])) {
7652
                    $tab_categories_scores[$question_category_id] = [];
7653
                }
7654
                $tmpobj_question = Question::read($questionId);
7655
                if (is_object($tmpobj_question)) {
7656
                    $tab_categories_scores[$question_category_id][] = $tmpobj_question->weighting;
7657
                }
7658
            }
7659
7660
            // here we've got an array with first key, the category_id, second key, score of question for this cat
7661
            foreach ($tab_categories_scores as $tab_scores) {
7662
                rsort($tab_scores);
7663
                for ($i = 0; $i < min($numberRandomQuestions, count($tab_scores)); $i++) {
7664
                    $out_max_score += $tab_scores[$i];
7665
                }
7666
            }
7667
        } else {
7668
            // standard test, just add each question score
7669
            foreach ($questionList as $questionId) {
7670
                $question = Question::read($questionId, $this->course);
7671
                $out_max_score += $question->weighting;
7672
            }
7673
        }
7674
7675
        return $out_max_score;
7676
    }
7677
7678
    /**
7679
     * @return string
7680
     */
7681
    public function get_formated_title()
7682
    {
7683
        if (api_get_configuration_value('save_titles_as_html')) {
7684
        }
7685
7686
        return api_html_entity_decode($this->selectTitle());
7687
    }
7688
7689
    /**
7690
     * @param string $title
7691
     *
7692
     * @return string
7693
     */
7694
    public static function get_formated_title_variable($title)
7695
    {
7696
        return api_html_entity_decode($title);
7697
    }
7698
7699
    /**
7700
     * @return string
7701
     */
7702
    public function format_title()
7703
    {
7704
        return api_htmlentities($this->title);
7705
    }
7706
7707
    /**
7708
     * @param string $title
7709
     *
7710
     * @return string
7711
     */
7712
    public static function format_title_variable($title)
7713
    {
7714
        return api_htmlentities($title);
7715
    }
7716
7717
    /**
7718
     * @param int $courseId
7719
     * @param int $sessionId
7720
     *
7721
     * @return array exercises
7722
     */
7723
    public function getExercisesByCourseSession($courseId, $sessionId)
7724
    {
7725
        $courseId = (int) $courseId;
7726
        $sessionId = (int) $sessionId;
7727
7728
        $tbl_quiz = Database::get_course_table(TABLE_QUIZ_TEST);
7729
        $sql = "SELECT * FROM $tbl_quiz cq
7730
                WHERE
7731
                    cq.c_id = %s AND
7732
                    (cq.session_id = %s OR cq.session_id = 0) AND
7733
                    cq.active = 0
7734
                ORDER BY cq.id";
7735
        $sql = sprintf($sql, $courseId, $sessionId);
7736
7737
        $result = Database::query($sql);
7738
7739
        $rows = [];
7740
        while ($row = Database::fetch_array($result, 'ASSOC')) {
7741
            $rows[] = $row;
7742
        }
7743
7744
        return $rows;
7745
    }
7746
7747
    /**
7748
     * @param int   $courseId
7749
     * @param int   $sessionId
7750
     * @param array $quizId
7751
     *
7752
     * @return array exercises
7753
     */
7754
    public function getExerciseAndResult($courseId, $sessionId, $quizId = [])
7755
    {
7756
        if (empty($quizId)) {
7757
            return [];
7758
        }
7759
7760
        $sessionId = (int) $sessionId;
7761
        $courseId = (int) $courseId;
7762
7763
        $ids = is_array($quizId) ? $quizId : [$quizId];
7764
        $ids = array_map('intval', $ids);
7765
        $ids = implode(',', $ids);
7766
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7767
        if (0 != $sessionId) {
7768
            $sql = "SELECT * FROM $track_exercises te
7769
              INNER JOIN c_quiz cq
7770
              ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id
7771
              WHERE
7772
              te.id = %s AND
7773
              te.session_id = %s AND
7774
              cq.id IN (%s)
7775
              ORDER BY cq.id";
7776
7777
            $sql = sprintf($sql, $courseId, $sessionId, $ids);
7778
        } else {
7779
            $sql = "SELECT * FROM $track_exercises te
7780
              INNER JOIN c_quiz cq ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id
7781
              WHERE
7782
              te.id = %s AND
7783
              cq.id IN (%s)
7784
              ORDER BY cq.id";
7785
            $sql = sprintf($sql, $courseId, $ids);
7786
        }
7787
        $result = Database::query($sql);
7788
        $rows = [];
7789
        while ($row = Database::fetch_array($result, 'ASSOC')) {
7790
            $rows[] = $row;
7791
        }
7792
7793
        return $rows;
7794
    }
7795
7796
    /**
7797
     * @param $exeId
7798
     * @param $exercise_stat_info
7799
     * @param $remindList
7800
     * @param $currentQuestion
7801
     *
7802
     * @return int|null
7803
     */
7804
    public static function getNextQuestionId(
7805
        $exeId,
7806
        $exercise_stat_info,
7807
        $remindList,
7808
        $currentQuestion
7809
    ) {
7810
        $result = Event::get_exercise_results_by_attempt($exeId, 'incomplete');
7811
7812
        if (isset($result[$exeId])) {
7813
            $result = $result[$exeId];
7814
        } else {
7815
            return null;
7816
        }
7817
7818
        $data_tracking = $exercise_stat_info['data_tracking'];
7819
        $data_tracking = explode(',', $data_tracking);
7820
7821
        // if this is the final question do nothing.
7822
        if ($currentQuestion == count($data_tracking)) {
7823
            return null;
7824
        }
7825
7826
        $currentQuestion--;
7827
7828
        if (!empty($result['question_list'])) {
7829
            $answeredQuestions = [];
7830
            foreach ($result['question_list'] as $question) {
7831
                if (!empty($question['answer'])) {
7832
                    $answeredQuestions[] = $question['question_id'];
7833
                }
7834
            }
7835
7836
            // Checking answered questions
7837
            $counterAnsweredQuestions = 0;
7838
            foreach ($data_tracking as $questionId) {
7839
                if (!in_array($questionId, $answeredQuestions)) {
7840
                    if ($currentQuestion != $counterAnsweredQuestions) {
7841
                        break;
7842
                    }
7843
                }
7844
                $counterAnsweredQuestions++;
7845
            }
7846
7847
            $counterRemindListQuestions = 0;
7848
            // Checking questions saved in the reminder list
7849
            if (!empty($remindList)) {
7850
                foreach ($data_tracking as $questionId) {
7851
                    if (in_array($questionId, $remindList)) {
7852
                        // Skip the current question
7853
                        if ($currentQuestion != $counterRemindListQuestions) {
7854
                            break;
7855
                        }
7856
                    }
7857
                    $counterRemindListQuestions++;
7858
                }
7859
7860
                if ($counterRemindListQuestions < $currentQuestion) {
7861
                    return null;
7862
                }
7863
7864
                if (!empty($counterRemindListQuestions)) {
7865
                    if ($counterRemindListQuestions > $counterAnsweredQuestions) {
7866
                        return $counterAnsweredQuestions;
7867
                    } else {
7868
                        return $counterRemindListQuestions;
7869
                    }
7870
                }
7871
            }
7872
7873
            return $counterAnsweredQuestions;
7874
        }
7875
    }
7876
7877
    /**
7878
     * Gets the position of a questionId in the question list.
7879
     *
7880
     * @param $questionId
7881
     *
7882
     * @return int
7883
     */
7884
    public function getPositionInCompressedQuestionList($questionId)
7885
    {
7886
        $questionList = $this->getQuestionListWithMediasCompressed();
7887
        $mediaQuestions = $this->getMediaList();
7888
        $position = 1;
7889
        foreach ($questionList as $id) {
7890
            if (isset($mediaQuestions[$id]) && in_array($questionId, $mediaQuestions[$id])) {
7891
                $mediaQuestionList = $mediaQuestions[$id];
7892
                if (in_array($questionId, $mediaQuestionList)) {
7893
                    return $position;
7894
                } else {
7895
                    $position++;
7896
                }
7897
            } else {
7898
                if ($id == $questionId) {
7899
                    return $position;
7900
                } else {
7901
                    $position++;
7902
                }
7903
            }
7904
        }
7905
7906
        return 1;
7907
    }
7908
7909
    /**
7910
     * Get the correct answers in all attempts.
7911
     *
7912
     * @param int  $learnPathId
7913
     * @param int  $learnPathItemId
7914
     * @param bool $onlyCorrect
7915
     *
7916
     * @return array
7917
     */
7918
    public function getAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0, $onlyCorrect = true)
7919
    {
7920
        $attempts = Event::getExerciseResultsByUser(
7921
            api_get_user_id(),
7922
            $this->getId(),
7923
            api_get_course_int_id(),
7924
            api_get_session_id(),
7925
            $learnPathId,
7926
            $learnPathItemId,
7927
            'DESC'
7928
        );
7929
7930
        $list = [];
7931
        foreach ($attempts as $attempt) {
7932
            foreach ($attempt['question_list'] as $answers) {
7933
                foreach ($answers as $answer) {
7934
                    $objAnswer = new Answer($answer['question_id']);
7935
                    if ($onlyCorrect) {
7936
                        switch ($objAnswer->getQuestionType()) {
7937
                            case FILL_IN_BLANKS:
7938
                                $isCorrect = FillBlanks::isCorrect($answer['answer']);
7939
7940
                                break;
7941
                            case MATCHING:
7942
                            case DRAGGABLE:
7943
                            case MATCHING_DRAGGABLE:
7944
                                $isCorrect = Matching::isCorrect(
7945
                                    $answer['position'],
7946
                                    $answer['answer'],
7947
                                    $answer['question_id']
7948
                                );
7949
7950
                                break;
7951
                            case ORAL_EXPRESSION:
7952
                                $isCorrect = false;
7953
7954
                                break;
7955
                            default:
7956
                                $isCorrect = $objAnswer->isCorrectByAutoId($answer['answer']);
7957
                        }
7958
                        if ($isCorrect) {
7959
                            $list[$answer['question_id']][] = $answer;
7960
                        }
7961
                    } else {
7962
                        $list[$answer['question_id']][] = $answer;
7963
                    }
7964
                }
7965
            }
7966
7967
            if (false === $onlyCorrect) {
7968
                // Only take latest attempt
7969
                break;
7970
            }
7971
        }
7972
7973
        return $list;
7974
    }
7975
7976
    /**
7977
     * Get the correct answers in all attempts.
7978
     *
7979
     * @param int $learnPathId
7980
     * @param int $learnPathItemId
7981
     *
7982
     * @return array
7983
     */
7984
    public function getCorrectAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0)
7985
    {
7986
        return $this->getAnswersInAllAttempts($learnPathId, $learnPathItemId);
7987
    }
7988
7989
    /**
7990
     * @return bool
7991
     */
7992
    public function showPreviousButton()
7993
    {
7994
        $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
7995
        if (false === $allow) {
7996
            return true;
7997
        }
7998
7999
        return $this->showPreviousButton;
8000
    }
8001
8002
    public function getPreventBackwards()
8003
    {
8004
        $allow = api_get_configuration_value('quiz_prevent_backwards_move');
8005
        if (false === $allow) {
8006
            return 0;
8007
        }
8008
8009
        return (int) $this->preventBackwards;
8010
    }
8011
8012
    /**
8013
     * @return int
8014
     */
8015
    public function getExerciseCategoryId()
8016
    {
8017
        if (empty($this->exerciseCategoryId)) {
8018
            return null;
8019
        }
8020
8021
        return (int) $this->exerciseCategoryId;
8022
    }
8023
8024
    /**
8025
     * @param int $value
8026
     */
8027
    public function setExerciseCategoryId($value)
8028
    {
8029
        if (!empty($value)) {
8030
            $this->exerciseCategoryId = (int) $value;
8031
        }
8032
    }
8033
8034
    /**
8035
     * @param array $values
8036
     *
8037
     * @throws \Doctrine\DBAL\DBALException
8038
     */
8039
    public function setPageResultConfiguration($values)
8040
    {
8041
        $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
8042
        if ($pageConfig) {
8043
            $params = [
8044
                'hide_expected_answer' => isset($values['hide_expected_answer']) ? $values['hide_expected_answer'] : '',
8045
                'hide_question_score' => isset($values['hide_question_score']) ? $values['hide_question_score'] : '',
8046
                'hide_total_score' => isset($values['hide_total_score']) ? $values['hide_total_score'] : '',
8047
            ];
8048
            $type = Type::getType('array');
8049
            $platform = Database::getManager()->getConnection()->getDatabasePlatform();
8050
            $this->pageResultConfiguration = $type->convertToDatabaseValue($params, $platform);
8051
        }
8052
    }
8053
8054
    /**
8055
     * @param array $defaults
8056
     */
8057
    public function setPageResultConfigurationDefaults(&$defaults)
8058
    {
8059
        $configuration = $this->getPageResultConfiguration();
8060
        if (!empty($configuration) && !empty($defaults)) {
8061
            $defaults = array_merge($defaults, $configuration);
8062
        }
8063
    }
8064
8065
    /**
8066
     * @return array
8067
     */
8068
    public function getPageResultConfiguration()
8069
    {
8070
        $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
8071
        if ($pageConfig) {
8072
            /*$params = [
8073
                'hide_expected_answer' => isset($values['hide_expected_answer']) ? $values['hide_expected_answer'] : '',
8074
                'hide_question_score' => isset($values['hide_question_score']) ? $values['hide_question_score'] : '',
8075
                'hide_total_score' => isset($values['hide_total_score']) ? $values['hide_total_score'] : ''
8076
            ];*/
8077
            $type = Type::getType('array');
8078
            $platform = Database::getManager()->getConnection()->getDatabasePlatform();
8079
8080
            return $type->convertToPHPValue($this->pageResultConfiguration, $platform);
8081
        }
8082
8083
        return [];
8084
    }
8085
8086
    /**
8087
     * @param string $attribute
8088
     *
8089
     * @return mixed|null
8090
     */
8091
    public function getPageConfigurationAttribute($attribute)
8092
    {
8093
        $result = $this->getPageResultConfiguration();
8094
8095
        if (!empty($result)) {
8096
            return isset($result[$attribute]) ? $result[$attribute] : null;
8097
        }
8098
8099
        return null;
8100
    }
8101
8102
    /**
8103
     * @param bool $showPreviousButton
8104
     *
8105
     * @return Exercise
8106
     */
8107
    public function setShowPreviousButton($showPreviousButton)
8108
    {
8109
        $this->showPreviousButton = $showPreviousButton;
8110
8111
        return $this;
8112
    }
8113
8114
    /**
8115
     * @param array $notifications
8116
     */
8117
    public function setNotifications($notifications)
8118
    {
8119
        $this->notifications = $notifications;
8120
    }
8121
8122
    /**
8123
     * @return array
8124
     */
8125
    public function getNotifications()
8126
    {
8127
        return $this->notifications;
8128
    }
8129
8130
    /**
8131
     * @return bool
8132
     */
8133
    public function showExpectedChoice()
8134
    {
8135
        return api_get_configuration_value('show_exercise_expected_choice');
8136
    }
8137
8138
    /**
8139
     * @return bool
8140
     */
8141
    public function showExpectedChoiceColumn()
8142
    {
8143
        if (!in_array($this->results_disabled, [
8144
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
8145
        ])
8146
        ) {
8147
            $hide = (int) $this->getPageConfigurationAttribute('hide_expected_answer');
8148
            if (1 === $hide) {
8149
                return false;
8150
            }
8151
8152
            return true;
8153
        }
8154
8155
        return false;
8156
    }
8157
8158
    /**
8159
     * @param string $class
8160
     * @param string $scoreLabel
8161
     * @param string $result
8162
     * @param array
8163
     *
8164
     * @return string
8165
     */
8166
    public function getQuestionRibbon($class, $scoreLabel, $result, $array)
8167
    {
8168
        $hide = (int) $this->getPageConfigurationAttribute('hide_question_score');
8169
        if (1 === $hide) {
8170
            return '';
8171
        }
8172
8173
        if ($this->showExpectedChoice()) {
8174
            $html = null;
8175
            $hideLabel = api_get_configuration_value('exercise_hide_label');
8176
            $label = '<div class="rib rib-'.$class.'">
8177
                        <h3>'.$scoreLabel.'</h3>
8178
                      </div>';
8179
            if (!empty($result)) {
8180
                $label .= '<h4>'.get_lang('Score').': '.$result.'</h4>';
8181
            }
8182
            if (true === $hideLabel) {
8183
                $answerUsed = (int) $array['used'];
8184
                $answerMissing = (int) $array['missing'] - $answerUsed;
8185
                for ($i = 1; $i <= $answerUsed; $i++) {
8186
                    $html .= '<span class="score-img">'.
8187
                        Display::return_icon('attempt-check.png', null, null, ICON_SIZE_SMALL).
8188
                        '</span>';
8189
                }
8190
                for ($i = 1; $i <= $answerMissing; $i++) {
8191
                    $html .= '<span class="score-img">'.
8192
                        Display::return_icon('attempt-nocheck.png', null, null, ICON_SIZE_SMALL).
8193
                        '</span>';
8194
                }
8195
                $label = '<div class="score-title">'.get_lang('Correct answers').': '.$result.'</div>';
8196
                $label .= '<div class="score-limits">';
8197
                $label .= $html;
8198
                $label .= '</div>';
8199
            }
8200
8201
            return '<div class="ribbon">
8202
                '.$label.'
8203
                </div>'
8204
                ;
8205
        } else {
8206
            $html = '<div class="ribbon">
8207
                        <div class="rib rib-'.$class.'">
8208
                            <h3>'.$scoreLabel.'</h3>
8209
                        </div>';
8210
            if (!empty($result)) {
8211
                $html .= '<h4>'.get_lang('Score').': '.$result.'</h4>';
8212
            }
8213
            $html .= '</div>';
8214
8215
            return $html;
8216
        }
8217
    }
8218
8219
    /**
8220
     * @return int
8221
     */
8222
    public function getAutoLaunch()
8223
    {
8224
        return $this->autolaunch;
8225
    }
8226
8227
    /**
8228
     * Clean auto launch settings for all exercise in course/course-session.
8229
     */
8230
    public function enableAutoLaunch()
8231
    {
8232
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8233
        $sql = "UPDATE $table SET autolaunch = 1
8234
                WHERE iid = ".$this->iId;
8235
        Database::query($sql);
8236
    }
8237
8238
    /**
8239
     * Clean auto launch settings for all exercise in course/course-session.
8240
     */
8241
    public function cleanCourseLaunchSettings()
8242
    {
8243
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8244
        $sql = "UPDATE $table SET autolaunch = 0
8245
                WHERE c_id = ".$this->course_id.' AND session_id = '.$this->sessionId;
8246
        Database::query($sql);
8247
    }
8248
8249
    /**
8250
     * Get the title without HTML tags.
8251
     *
8252
     * @return string
8253
     */
8254
    public function getUnformattedTitle()
8255
    {
8256
        return strip_tags(api_html_entity_decode($this->title));
8257
    }
8258
8259
    /**
8260
     * Get the question IDs from quiz_rel_question for the current quiz,
8261
     * using the parameters as the arguments to the SQL's LIMIT clause.
8262
     * Because the exercise_id is known, it also comes with a filter on
8263
     * the session, so sessions are not specified here.
8264
     *
8265
     * @param int $start  At which question do we want to start the list
8266
     * @param int $length Up to how many results we want
8267
     *
8268
     * @return array A list of question IDs
8269
     */
8270
    public function getQuestionForTeacher($start = 0, $length = 10)
8271
    {
8272
        $start = (int) $start;
8273
        if ($start < 0) {
8274
            $start = 0;
8275
        }
8276
8277
        $length = (int) $length;
8278
8279
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8280
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
8281
        $sql = "SELECT DISTINCT e.question_id
8282
                FROM $quizRelQuestion e
8283
                INNER JOIN $question q
8284
                ON (e.question_id = q.iid AND e.c_id = q.c_id)
8285
                WHERE
8286
                    e.c_id = {$this->course_id} AND
8287
                    e.exercice_id = '".$this->getId()."'
8288
                ORDER BY question_order
8289
                LIMIT $start, $length
8290
            ";
8291
        $result = Database::query($sql);
8292
        $questionList = [];
8293
        while ($object = Database::fetch_object($result)) {
8294
            $questionList[] = $object->question_id;
8295
        }
8296
8297
        return $questionList;
8298
    }
8299
8300
    /**
8301
     * @param int   $exerciseId
8302
     * @param array $courseInfo
8303
     * @param int   $sessionId
8304
     *
8305
     * @throws \Doctrine\ORM\OptimisticLockException
8306
     *
8307
     * @return bool
8308
     */
8309
    public function generateStats($exerciseId, $courseInfo, $sessionId)
8310
    {
8311
        $allowStats = api_get_configuration_value('allow_gradebook_stats');
8312
        if (!$allowStats) {
8313
            return false;
8314
        }
8315
8316
        if (empty($courseInfo)) {
8317
            return false;
8318
        }
8319
8320
        $courseId = $courseInfo['real_id'];
8321
8322
        $sessionId = (int) $sessionId;
8323
        $exerciseId = (int) $exerciseId;
8324
8325
        $result = $this->read($exerciseId);
8326
8327
        if (empty($result)) {
8328
            api_not_allowed(true);
8329
        }
8330
8331
        $statusToFilter = empty($sessionId) ? STUDENT : 0;
8332
8333
        $studentList = CourseManager::get_user_list_from_course_code(
8334
            $courseInfo['code'],
8335
            $sessionId,
8336
            null,
8337
            null,
8338
            $statusToFilter
8339
        );
8340
8341
        if (empty($studentList)) {
8342
            Display::addFlash(Display::return_message(get_lang('No users in course')));
8343
            header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
8344
            exit;
8345
        }
8346
8347
        $tblStats = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8348
8349
        $studentIdList = [];
8350
        if (!empty($studentList)) {
8351
            $studentIdList = array_column($studentList, 'user_id');
8352
        }
8353
8354
        if (false == $this->exercise_was_added_in_lp) {
8355
            $sql = "SELECT * FROM $tblStats
8356
                        WHERE
8357
                            exe_exo_id = $exerciseId AND
8358
                            orig_lp_id = 0 AND
8359
                            orig_lp_item_id = 0 AND
8360
                            status <> 'incomplete' AND
8361
                            session_id = $sessionId AND
8362
                            c_id = $courseId
8363
                        ";
8364
        } else {
8365
            $lpId = null;
8366
            if (!empty($this->lpList)) {
8367
                // Taking only the first LP
8368
                $lpId = $this->getLpBySession($sessionId);
8369
                $lpId = $lpId['lp_id'];
8370
            }
8371
8372
            $sql = "SELECT *
8373
                        FROM $tblStats
8374
                        WHERE
8375
                            exe_exo_id = $exerciseId AND
8376
                            orig_lp_id = $lpId AND
8377
                            status <> 'incomplete' AND
8378
                            session_id = $sessionId AND
8379
                            c_id = $courseId ";
8380
        }
8381
8382
        $sql .= ' ORDER BY exe_id DESC';
8383
8384
        $studentCount = 0;
8385
        $sum = 0;
8386
        $bestResult = 0;
8387
        $sumResult = 0;
8388
        $result = Database::query($sql);
8389
        while ($data = Database::fetch_array($result, 'ASSOC')) {
8390
            // Only take into account users in the current student list.
8391
            if (!empty($studentIdList)) {
8392
                if (!in_array($data['exe_user_id'], $studentIdList)) {
8393
                    continue;
8394
                }
8395
            }
8396
8397
            if (!isset($students[$data['exe_user_id']])) {
8398
                if (0 != $data['exe_weighting']) {
8399
                    $students[$data['exe_user_id']] = $data['exe_result'];
8400
                    if ($data['exe_result'] > $bestResult) {
8401
                        $bestResult = $data['exe_result'];
8402
                    }
8403
                    $sumResult += $data['exe_result'];
8404
                }
8405
            }
8406
        }
8407
8408
        $count = count($studentList);
8409
        $average = $sumResult / $count;
8410
        $em = Database::getManager();
8411
8412
        $links = AbstractLink::getGradebookLinksFromItem(
8413
            $this->getId(),
8414
            LINK_EXERCISE,
8415
            $courseInfo['code'],
8416
            $sessionId
8417
        );
8418
8419
        if (empty($links)) {
8420
            $links = AbstractLink::getGradebookLinksFromItem(
8421
                $this->iId,
8422
                LINK_EXERCISE,
8423
                $courseInfo['code'],
8424
                $sessionId
8425
            );
8426
        }
8427
8428
        if (!empty($links)) {
8429
            $repo = $em->getRepository('ChamiloCoreBundle:GradebookLink');
8430
8431
            foreach ($links as $link) {
8432
                $linkId = $link['id'];
8433
                /** @var GradebookLink $exerciseLink */
8434
                $exerciseLink = $repo->find($linkId);
8435
                if ($exerciseLink) {
8436
                    $exerciseLink
8437
                        ->setUserScoreList($students)
8438
                        ->setBestScore($bestResult)
8439
                        ->setAverageScore($average)
8440
                        ->setScoreWeight($this->get_max_score());
8441
                    $em->persist($exerciseLink);
8442
                    $em->flush();
8443
                }
8444
            }
8445
        }
8446
    }
8447
8448
    /**
8449
     * Return an HTML table of exercises for on-screen printing, including
8450
     * action icons. If no exercise is present and the user can edit the
8451
     * course, show a "create test" button.
8452
     *
8453
     * @param int    $categoryId
8454
     * @param string $keyword
8455
     *
8456
     * @return string
8457
     */
8458
    public static function exerciseGridResource($categoryId, $keyword)
8459
    {
8460
        $courseId = api_get_course_int_id();
8461
        $sessionId = api_get_session_id();
8462
8463
        $course = api_get_course_entity($courseId);
8464
        $session = api_get_session_entity($sessionId);
8465
8466
        $repo = Container::getExerciseRepository();
8467
8468
        // 2. Get query builder from repo.
8469
        $qb = $repo->getResourcesByCourse($course, $session);
8470
8471
        if (!empty($categoryId)) {
8472
            $qb->andWhere($qb->expr()->eq('resource.exerciseCategory', $categoryId));
8473
        } else {
8474
            $qb->andWhere($qb->expr()->isNull('resource.exerciseCategory'));
8475
        }
8476
8477
        /*$editAccess = Container::getAuthorizationChecker()->isGranted(ResourceNodeVoter::ROLE_CURRENT_COURSE_TEACHER);
8478
        return Container::$container->get('twig')->render(
8479
            '@ChamiloCore/Resource/grid.html.twig',
8480
            ['grid' => $grid]
8481
        );*/
8482
8483
        $allowDelete = Exercise::allowAction('delete');
8484
        $allowClean = Exercise::allowAction('clean_results');
8485
8486
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8487
        $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
8488
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8489
8490
        $categoryId = (int) $categoryId;
8491
        $keyword = Database::escape_string($keyword);
8492
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
8493
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
8494
8495
        $autoLaunchAvailable = false;
8496
        if (api_get_course_setting('enable_exercise_auto_launch') == 1 &&
8497
            api_get_configuration_value('allow_exercise_auto_launch')
8498
        ) {
8499
            $autoLaunchAvailable = true;
8500
        }
8501
8502
        $is_allowedToEdit = api_is_allowed_to_edit(null, true);
8503
        $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : api_get_course_info();
8504
        $courseId = $courseInfo['real_id'];
8505
        $tableRows = [];
8506
        $origin = api_get_origin();
8507
        $userId = api_get_user_id();
8508
        $charset = 'utf-8';
8509
        $token = Security::get_token();
8510
        $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh($userId, $courseInfo);
8511
        $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
8512
8513
        // Condition for the session
8514
        $condition_session = api_get_session_condition($sessionId, true, true, 'e.session_id');
8515
        $content = '';
8516
        $column = 0;
8517
        if ($is_allowedToEdit) {
8518
            $column = 1;
8519
        }
8520
8521
        $table = new SortableTableFromArrayConfig(
8522
            [],
8523
            $column,
8524
            self::PAGINATION_ITEMS_PER_PAGE,
8525
            'exercises_cat_'.$categoryId
8526
        );
8527
8528
        $limit = $table->per_page;
8529
        $page = $table->page_nr;
8530
        $from = $limit * ($page - 1);
8531
8532
        if (!empty($keyword)) {
8533
            $qb->andWhere($qb->expr()->eq('resource.title', $keyword));
8534
        }
8535
8536
        $qb->setFirstResult($from);
8537
        $qb->setMaxResults($limit);
8538
8539
        // Only for administrators
8540
        if ($is_allowedToEdit) {
8541
            $qb->andWhere($qb->expr()->neq('resource.active', -1));
8542
        } else {
8543
            $qb->andWhere($qb->expr()->eq('resource.active', 1));
8544
        }
8545
        //var_dump($qb->getQuery()->getSQL());
8546
        $exerciseList = $qb->getQuery()->getResult();
8547
        $total = $qb->select('count(resource.iid)')->setMaxResults(1)->getQuery()->getScalarResult();
8548
8549
        $webPath = api_get_path(WEB_CODE_PATH);
8550
8551
        if (!empty($exerciseList)) {
8552
            $visibilitySetting = api_get_configuration_value('show_hidden_exercise_added_to_lp');
8553
            //avoid sending empty parameters
8554
            $mylpid = empty($learnpath_id) ? '' : '&learnpath_id='.$learnpath_id;
8555
            $mylpitemid = empty($learnpath_item_id) ? '' : '&learnpath_item_id='.$learnpath_item_id;
8556
            /** @var CQuiz $exerciseEntity */
8557
            foreach ($exerciseList as $exerciseEntity) {
8558
                $currentRow = [];
8559
                $exerciseId = $exerciseEntity->getIid();
8560
                $attempt_text = '';
8561
                $actions = '';
8562
                $exercise = new Exercise($courseId);
8563
                $exercise->read($exerciseId, false);
8564
8565
                if (empty($exercise->iId)) {
8566
                    continue;
8567
                }
8568
8569
                $locked = $exercise->is_gradebook_locked;
8570
                // Validation when belongs to a session
8571
                $session_img = null;
8572
                //$session_img = api_get_session_image($row['session_id'], $userInfo['status']);
8573
8574
                $startTime = $exerciseEntity->getStartTime();
8575
                $endTime = $exerciseEntity->getEndTime();
8576
                $time_limits = false;
8577
                if (!empty($startTime) || !empty($endTime)) {
8578
                    $time_limits = true;
8579
                }
8580
8581
                $is_actived_time = false;
8582
                if ($time_limits) {
8583
                    // check if start time
8584
                    $start_time = false;
8585
                    if (!empty($startTime)) {
8586
                        $start_time = api_strtotime($startTime->format('Y-m-d H:i:s'), 'UTC');
8587
                    }
8588
                    $end_time = false;
8589
                    if (!empty($endTime)) {
8590
                        $end_time = api_strtotime($endTime->format('Y-m-d H:i:s'), 'UTC');
8591
                    }
8592
                    $now = time();
8593
                    //If both "clocks" are enable
8594
                    if ($start_time && $end_time) {
8595
                        if ($now > $start_time && $end_time > $now) {
8596
                            $is_actived_time = true;
8597
                        }
8598
                    } else {
8599
                        //we check the start and end
8600
                        if ($start_time) {
8601
                            if ($now > $start_time) {
8602
                                $is_actived_time = true;
8603
                            }
8604
                        }
8605
                        if ($end_time) {
8606
                            if ($end_time > $now) {
8607
                                $is_actived_time = true;
8608
                            }
8609
                        }
8610
                    }
8611
                }
8612
8613
                // Blocking empty start times see BT#2800
8614
                global $_custom;
8615
                if (isset($_custom['exercises_hidden_when_no_start_date']) &&
8616
                    $_custom['exercises_hidden_when_no_start_date']
8617
                ) {
8618
                    if (empty($startTime)) {
8619
                        $time_limits = true;
8620
                        $is_actived_time = false;
8621
                    }
8622
                }
8623
8624
                $cut_title = $exercise->getCutTitle();
8625
                $alt_title = '';
8626
                if ($cut_title != $exerciseEntity->getTitle()) {
8627
                    $alt_title = ' title = "'.$exercise->getUnformattedTitle().'" ';
8628
                }
8629
8630
                // Teacher only
8631
                if ($is_allowedToEdit) {
8632
                    $lp_blocked = null;
8633
                    if ($exercise->exercise_was_added_in_lp == true) {
8634
                        $lp_blocked = Display::div(
8635
                            get_lang('AddedToLPCannotBeAccessed'),
8636
                            ['class' => 'lp_content_type_label']
8637
                        );
8638
                    }
8639
8640
                    $visibility = $exerciseEntity->isVisible($course, null);
8641
                    // Get visibility in base course
8642
                    /*$visibility = api_get_item_visibility(
8643
                        $courseInfo,
8644
                        TOOL_QUIZ,
8645
                        $exerciseId,
8646
                        0
8647
                    );*/
8648
8649
                    if (!empty($sessionId)) {
8650
                        // If we are in a session, the test is invisible
8651
                        // in the base course, it is included in a LP
8652
                        // *and* the setting to show it is *not*
8653
                        // specifically set to true, then hide it.
8654
                        if (false === $visibility) {
8655
                            if (!$visibilitySetting) {
8656
                                if ($exercise->exercise_was_added_in_lp == true) {
8657
                                    continue;
8658
                                }
8659
                            }
8660
                        }
8661
8662
                        $visibility = $exerciseEntity->isVisible($course, $session);
8663
                    }
8664
8665
                    if ($exerciseEntity->getActive() == 0 || false === $visibility) {
8666
                        $title = Display::tag('font', $cut_title, ['style' => 'color:grey']);
8667
                    } else {
8668
                        $title = $cut_title;
8669
                    }
8670
8671
                    $move = null;
8672
                    $class_tip = '';
8673
                    $url = $move.'<a
8674
                        '.$alt_title.' class="'.$class_tip.'" id="tooltip_'.$exerciseId.'"
8675
                        href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&id='.$exerciseId.'">
8676
                         '.Display::return_icon('quiz.png', $title).'
8677
                         '.$title.' </a>'.PHP_EOL;
8678
8679
                    if (ExerciseLib::isQuizEmbeddable($exerciseEntity)) {
8680
                        $embeddableIcon = Display::return_icon('om_integration.png', get_lang('ThisQuizCanBeEmbeddable'));
8681
                        $url .= Display::div($embeddableIcon, ['class' => 'pull-right']);
8682
                    }
8683
8684
                    $currentRow['title'] = $url.' '.$session_img.$lp_blocked;
8685
8686
                    // Count number exercise - teacher
8687
                    $sql = "SELECT count(*) count FROM $TBL_EXERCISE_QUESTION
8688
                            WHERE c_id = $courseId AND exercice_id = $exerciseId";
8689
                    $sqlresult = Database::query($sql);
8690
                    $rowi = (int) Database::result($sqlresult, 0, 0);
8691
8692
                    if ($sessionId == $exerciseEntity->getSessionId()) {
8693
                        // Questions list
8694
                        $actions = Display::url(
8695
                            Display::return_icon('edit.png', get_lang('Edit'), '', ICON_SIZE_SMALL),
8696
                            'admin.php?'.api_get_cidreq().'&id='.$exerciseId
8697
                        );
8698
8699
                        // Test settings
8700
                        $settings = Display::url(
8701
                            Display::return_icon('settings.png', get_lang('Configure'), '', ICON_SIZE_SMALL),
8702
                            'exercise_admin.php?'.api_get_cidreq().'&id='.$exerciseId
8703
                        );
8704
8705
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
8706
                            $settings = '';
8707
                        }
8708
                        $actions .= $settings;
8709
8710
                        // Exercise results
8711
                        $resultsLink = '<a href="exercise_report.php?'.api_get_cidreq().'&id='.$exerciseId.'">'.
8712
                            Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
8713
8714
                        if ($limitTeacherAccess) {
8715
                            if (api_is_platform_admin()) {
8716
                                $actions .= $resultsLink;
8717
                            }
8718
                        } else {
8719
                            // Exercise results
8720
                            $actions .= $resultsLink;
8721
                        }
8722
8723
                        // Auto launch
8724
                        if ($autoLaunchAvailable) {
8725
                            $autoLaunch = $exercise->getAutoLaunch();
8726
                            if (empty($autoLaunch)) {
8727
                                $actions .= Display::url(
8728
                                    Display::return_icon(
8729
                                        'launch_na.png',
8730
                                        get_lang('Enable'),
8731
                                        '',
8732
                                        ICON_SIZE_SMALL
8733
                                    ),
8734
                                    'exercise.php?'.api_get_cidreq().'&choice=enable_launch&sec_token='.$token.'&id='.$exerciseId
8735
                                );
8736
                            } else {
8737
                                $actions .= Display::url(
8738
                                    Display::return_icon(
8739
                                        'launch.png',
8740
                                        get_lang('Disable'),
8741
                                        '',
8742
                                        ICON_SIZE_SMALL
8743
                                    ),
8744
                                    'exercise.php?'.api_get_cidreq().'&choice=disable_launch&sec_token='.$token.'&id='.$exerciseId
8745
                                );
8746
                            }
8747
                        }
8748
8749
                        // Export
8750
                        $actions .= Display::url(
8751
                            Display::return_icon('cd.png', get_lang('CopyExercise')),
8752
                            '',
8753
                            [
8754
                                'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('AreYouSureToCopy'), ENT_QUOTES, $charset))." ".addslashes($title)."?"."')) return false;",
8755
                                'href' => 'exercise.php?'.api_get_cidreq().'&choice=copy_exercise&sec_token='.$token.'&id='.$exerciseId,
8756
                            ]
8757
                        );
8758
8759
                        // Clean exercise
8760
                        $clean = '';
8761
                        if (true === $allowClean) {
8762
                            if (false == $locked) {
8763
                                $clean = Display::url(
8764
                                    Display::return_icon(
8765
                                        'clean.png',
8766
                                        get_lang('CleanStudentResults'),
8767
                                        '',
8768
                                        ICON_SIZE_SMALL
8769
                                    ),
8770
                                    '',
8771
                                    [
8772
                                        'onclick' => "javascript:if(!confirm('".addslashes(
8773
                                                api_htmlentities(
8774
                                                    get_lang('AreYouSureToDeleteResults'),
8775
                                                    ENT_QUOTES,
8776
                                                    $charset
8777
                                                )
8778
                                            )." ".addslashes($title)."?"."')) return false;",
8779
                                        'href' => 'exercise.php?'.api_get_cidreq(
8780
                                            ).'&choice=clean_results&sec_token='.$token.'&id='.$exerciseId,
8781
                                    ]
8782
                                );
8783
                            } else {
8784
                                $clean = Display::return_icon(
8785
                                    'clean_na.png',
8786
                                    get_lang('ResourceLockedByGradebook'),
8787
                                    '',
8788
                                    ICON_SIZE_SMALL
8789
                                );
8790
                            }
8791
                        }
8792
8793
                        $actions .= $clean;
8794
                        // Visible / invisible
8795
                        // Check if this exercise was added in a LP
8796
                        if ($exercise->exercise_was_added_in_lp == true) {
8797
                            $visibility = Display::return_icon(
8798
                                'invisible.png',
8799
                                get_lang('AddedToLPCannotBeAccessed'),
8800
                                '',
8801
                                ICON_SIZE_SMALL
8802
                            );
8803
                        } else {
8804
                            if ($exerciseEntity->getActive() == 0 || $visibility == 0) {
8805
                                $visibility = Display::url(
8806
                                    Display::return_icon(
8807
                                        'invisible.png',
8808
                                        get_lang('Activate'),
8809
                                        '',
8810
                                        ICON_SIZE_SMALL
8811
                                    ),
8812
                                    'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&id='.$exerciseId
8813
                                );
8814
                            } else {
8815
                                // else if not active
8816
                                $visibility = Display::url(
8817
                                    Display::return_icon(
8818
                                        'visible.png',
8819
                                        get_lang('Deactivate'),
8820
                                        '',
8821
                                        ICON_SIZE_SMALL
8822
                                    ),
8823
                                    'exercise.php?'.api_get_cidreq().'&choice=disable&sec_token='.$token.'&id='.$exerciseId
8824
                                );
8825
                            }
8826
                        }
8827
8828
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
8829
                            $visibility = '';
8830
                        }
8831
8832
                        $actions .= $visibility;
8833
8834
                        // Export qti ...
8835
                        $export = Display::url(
8836
                            Display::return_icon(
8837
                                'export_qti2.png',
8838
                                'IMS/QTI',
8839
                                '',
8840
                                ICON_SIZE_SMALL
8841
                            ),
8842
                            'exercise.php?action=exportqti2&id='.$exerciseId.'&'.api_get_cidreq()
8843
                        );
8844
8845
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
8846
                            $export = '';
8847
                        }
8848
8849
                        $actions .= $export;
8850
                    } else {
8851
                        // not session
8852
                        $actions = Display::return_icon(
8853
                            'edit_na.png',
8854
                            get_lang('ExerciseEditionNotAvailableInSession')
8855
                        );
8856
8857
                        // Check if this exercise was added in a LP
8858
                        if ($exercise->exercise_was_added_in_lp == true) {
8859
                            $visibility = Display::return_icon(
8860
                                'invisible.png',
8861
                                get_lang('AddedToLPCannotBeAccessed'),
8862
                                '',
8863
                                ICON_SIZE_SMALL
8864
                            );
8865
                        } else {
8866
                            if ($exerciseEntity->getActive() == 0 || $visibility == 0) {
8867
                                $visibility = Display::url(
8868
                                    Display::return_icon(
8869
                                        'invisible.png',
8870
                                        get_lang('Activate'),
8871
                                        '',
8872
                                        ICON_SIZE_SMALL
8873
                                    ),
8874
                                    'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&id='.$exerciseId
8875
                                );
8876
                            } else {
8877
                                // else if not active
8878
                                $visibility = Display::url(
8879
                                    Display::return_icon(
8880
                                        'visible.png',
8881
                                        get_lang('Deactivate'),
8882
                                        '',
8883
                                        ICON_SIZE_SMALL
8884
                                    ),
8885
                                    'exercise.php?'.api_get_cidreq().'&choice=disable&sec_token='.$token.'&id='.$exerciseId
8886
                                );
8887
                            }
8888
                        }
8889
8890
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
8891
                            $visibility = '';
8892
                        }
8893
8894
                        $actions .= $visibility;
8895
                        $actions .= '<a href="exercise_report.php?'.api_get_cidreq().'&id='.$exerciseId.'">'.
8896
                            Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
8897
                        $actions .= Display::url(
8898
                            Display::return_icon('cd.gif', get_lang('CopyExercise')),
8899
                            '',
8900
                            [
8901
                                'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('AreYouSureToCopy'), ENT_QUOTES, $charset))." ".addslashes($title)."?"."')) return false;",
8902
                                'href' => 'exercise.php?'.api_get_cidreq().'&choice=copy_exercise&sec_token='.$token.'&id='.$exerciseId,
8903
                            ]
8904
                        );
8905
                    }
8906
8907
                    // Delete
8908
                    $delete = '';
8909
                    if ($sessionId == $exerciseEntity->getSessionId()) {
8910
                        if ($locked == false) {
8911
                            $delete = Display::url(
8912
                                Display::return_icon(
8913
                                    'delete.png',
8914
                                    get_lang('Delete'),
8915
                                    '',
8916
                                    ICON_SIZE_SMALL
8917
                                ),
8918
                                '',
8919
                                [
8920
                                    'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('AreYouSureToDeleteJS'), ENT_QUOTES, $charset))." ".addslashes($exercise->getUnformattedTitle())."?"."')) return false;",
8921
                                    'href' => 'exercise.php?'.api_get_cidreq().'&choice=delete&sec_token='.$token.'&id='.$exerciseId,
8922
                                ]
8923
                            );
8924
                        } else {
8925
                            $delete = Display::return_icon(
8926
                                'delete_na.png',
8927
                                get_lang('ResourceLockedByGradebook'),
8928
                                '',
8929
                                ICON_SIZE_SMALL
8930
                            );
8931
                        }
8932
                    }
8933
8934
                    if ($limitTeacherAccess && !api_is_platform_admin()) {
8935
                        $delete = '';
8936
                    }
8937
8938
                    $actions .= $delete;
8939
8940
                    // Number of questions
8941
                    $random_label = null;
8942
                    $random = $exerciseEntity->getRandom();
8943
                    if ($random > 0 || $random == -1) {
8944
                        // if random == -1 means use random questions with all questions
8945
                        $random_number_of_question = $random;
8946
                        if ($random_number_of_question == -1) {
8947
                            $random_number_of_question = $rowi;
8948
                        }
8949
                        if ($exerciseEntity->getRandomByCategory() > 0) {
8950
                            $nbQuestionsTotal = TestCategory::getNumberOfQuestionRandomByCategory(
8951
                                $exerciseId,
8952
                                $random_number_of_question
8953
                            );
8954
                            $number_of_questions = $nbQuestionsTotal.' ';
8955
                            $number_of_questions .= ($nbQuestionsTotal > 1) ? get_lang('QuestionsLowerCase') : get_lang('QuestionLowerCase');
8956
                            $number_of_questions .= ' - ';
8957
                            $number_of_questions .= min(
8958
                                    TestCategory::getNumberMaxQuestionByCat($exerciseId), $random_number_of_question
8959
                                ).' '.get_lang('QuestionByCategory');
8960
                        } else {
8961
                            $random_label = ' ('.get_lang('Random').') ';
8962
                            $number_of_questions = $random_number_of_question.' '.$random_label.' / '.$rowi;
8963
                            // Bug if we set a random value bigger than the real number of questions
8964
                            if ($random_number_of_question > $rowi) {
8965
                                $number_of_questions = $rowi.' '.$random_label;
8966
                            }
8967
                        }
8968
                    } else {
8969
                        $number_of_questions = $rowi;
8970
                    }
8971
8972
                    $currentRow['count_questions'] = $number_of_questions;
8973
                } else {
8974
                    // Student only.
8975
                    $visibility = $exerciseEntity->isVisible($course, null);
8976
8977
                    if (false === $visibility) {
8978
                        continue;
8979
                    }
8980
8981
                    $url = '<a '.$alt_title.'  href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&id='.$exerciseId.'">'.
8982
                        $cut_title.'</a>';
8983
8984
                    // Link of the exercise.
8985
                    $currentRow['title'] = $url.' '.$session_img;
8986
                    // This query might be improved later on by ordering by the new "tms" field rather than by exe_id
8987
                    // Don't remove this marker: note-query-exe-results
8988
                    $sql = "SELECT * FROM $TBL_TRACK_EXERCISES
8989
                            WHERE
8990
                                exe_exo_id = ".$exerciseId." AND
8991
                                exe_user_id = $userId AND
8992
                                c_id = ".api_get_course_int_id()." AND
8993
                                status <> 'incomplete' AND
8994
                                orig_lp_id = 0 AND
8995
                                orig_lp_item_id = 0 AND
8996
                                session_id =  '".api_get_session_id()."'
8997
                            ORDER BY exe_id DESC";
8998
8999
                    $qryres = Database::query($sql);
9000
                    $num = Database :: num_rows($qryres);
9001
9002
                    // Hide the results.
9003
                    $my_result_disabled = $exerciseEntity->getResultsDisabled();
9004
                    $attempt_text = '-';
9005
                    // Time limits are on
9006
                    if ($time_limits) {
9007
                        // Exam is ready to be taken
9008
                        if ($is_actived_time) {
9009
                            // Show results
9010
                            if (
9011
                            in_array(
9012
                                $my_result_disabled,
9013
                                [
9014
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9015
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9016
                                    RESULT_DISABLE_SHOW_SCORE_ONLY,
9017
                                    RESULT_DISABLE_RANKING,
9018
                                ]
9019
                            )
9020
                            ) {
9021
                                // More than one attempt
9022
                                if ($num > 0) {
9023
                                    $row_track = Database :: fetch_array($qryres);
9024
                                    $attempt_text = get_lang('Latest attempt').' : ';
9025
                                    $attempt_text .= ExerciseLib::show_score(
9026
                                        $row_track['exe_result'],
9027
                                        $row_track['exe_weighting']
9028
                                    );
9029
                                } else {
9030
                                    //No attempts
9031
                                    $attempt_text = get_lang('Not attempted');
9032
                                }
9033
                            } else {
9034
                                $attempt_text = '-';
9035
                            }
9036
                        } else {
9037
                            // Quiz not ready due to time limits
9038
                            //@todo use the is_visible function
9039
                            if (!empty($startTime) && !empty($endTime)) {
9040
                                $today = time();
9041
                                if ($today < $start_time) {
9042
                                    $attempt_text = sprintf(
9043
                                        get_lang('ExerciseWillBeActivatedFromXToY'),
9044
                                        api_convert_and_format_date($start_time),
9045
                                        api_convert_and_format_date($end_time)
9046
                                    );
9047
                                } else {
9048
                                    if ($today > $end_time) {
9049
                                        $attempt_text = sprintf(
9050
                                            get_lang('ExerciseWasActivatedFromXToY'),
9051
                                            api_convert_and_format_date($start_time),
9052
                                            api_convert_and_format_date($end_time)
9053
                                        );
9054
                                    }
9055
                                }
9056
                            } else {
9057
                                if (!empty($startTime)) {
9058
                                    $attempt_text = sprintf(
9059
                                        get_lang('ExerciseAvailableFromX'),
9060
                                        api_convert_and_format_date($start_time)
9061
                                    );
9062
                                }
9063
                                if (!empty($endTime)) {
9064
                                    $attempt_text = sprintf(
9065
                                        get_lang('ExerciseAvailableUntilX'),
9066
                                        api_convert_and_format_date($end_time)
9067
                                    );
9068
                                }
9069
                            }
9070
                        }
9071
                    } else {
9072
                        // Normal behaviour.
9073
                        // Show results.
9074
                        if (
9075
                        in_array(
9076
                            $my_result_disabled,
9077
                            [
9078
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9079
                                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9080
                                RESULT_DISABLE_SHOW_SCORE_ONLY,
9081
                                RESULT_DISABLE_RANKING,
9082
                            ]
9083
                        )
9084
                        ) {
9085
                            if ($num > 0) {
9086
                                $row_track = Database :: fetch_array($qryres);
9087
                                $attempt_text = get_lang('LatestAttempt').' : ';
9088
                                $attempt_text .= ExerciseLib::show_score(
9089
                                    $row_track['exe_result'],
9090
                                    $row_track['exe_weighting']
9091
                                );
9092
                            } else {
9093
                                $attempt_text = get_lang('Not attempted');
9094
                            }
9095
                        }
9096
                    }
9097
                }
9098
9099
                $currentRow['attempt'] = $attempt_text;
9100
9101
                if ($is_allowedToEdit) {
9102
                    $additionalActions = ExerciseLib::getAdditionalTeacherActions($exerciseId);
9103
9104
                    if (!empty($additionalActions)) {
9105
                        $actions .= $additionalActions.PHP_EOL;
9106
                    }
9107
9108
                    $currentRow = [
9109
                        $exerciseId,
9110
                        $currentRow['title'],
9111
                        $currentRow['count_questions'],
9112
                        $actions,
9113
                    ];
9114
                } else {
9115
                    $currentRow = [
9116
                        $currentRow['title'],
9117
                        $currentRow['attempt'],
9118
                    ];
9119
9120
                    if ($isDrhOfCourse) {
9121
                        $currentRow[] = '<a href="exercise_report.php?'.api_get_cidreq().'&id='.$exerciseId.'">'.
9122
                            Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
9123
                    }
9124
                }
9125
                $tableRows[] = $currentRow;
9126
            }
9127
        }
9128
9129
        if (empty($tableRows) && empty($categoryId)) {
9130
            if ($is_allowedToEdit && $origin !== 'learnpath') {
9131
                $content .= '<div id="no-data-view">';
9132
                $content .= '<h3>'.get_lang('Quiz').'</h3>';
9133
                $content .= Display::return_icon('quiz.png', '', [], 64);
9134
                $content .= '<div class="controls">';
9135
                $content .= Display::url(
9136
                    '<em class="fa fa-plus"></em> '.get_lang('NewEx'),
9137
                    'exercise_admin.php?'.api_get_cidreq(),
9138
                    ['class' => 'btn btn-primary']
9139
                );
9140
                $content .= '</div>';
9141
                $content .= '</div>';
9142
            }
9143
        } else {
9144
            if (empty($tableRows)) {
9145
                return '';
9146
            }
9147
            $table->setTableData($tableRows);
9148
            $table->setTotalNumberOfItems($total);
9149
            $table->set_additional_parameters([
9150
                'cid' => api_get_course_int_id(),
9151
                'sid' => api_get_session_id(),
9152
                'category_id' => $categoryId,
9153
            ]);
9154
9155
            if ($is_allowedToEdit) {
9156
                $formActions = [];
9157
                $formActions['visible'] = get_lang('Activate');
9158
                $formActions['invisible'] = get_lang('Deactivate');
9159
                $formActions['delete'] = get_lang('Delete');
9160
                $table->set_form_actions($formActions);
9161
            }
9162
9163
            $i = 0;
9164
            if ($is_allowedToEdit) {
9165
                $table->set_header($i++, '', false, 'width="18px"');
9166
            }
9167
            $table->set_header($i++, get_lang('Test name'), false);
9168
9169
            if ($is_allowedToEdit) {
9170
                $table->set_header($i++, get_lang('Questions'), false);
9171
                $table->set_header($i++, get_lang('Actions'), false);
9172
            } else {
9173
                $table->set_header($i++, get_lang('Status'), false);
9174
                if ($isDrhOfCourse) {
9175
                    $table->set_header($i++, get_lang('Actions'), false);
9176
                }
9177
            }
9178
9179
            $content .= $table->return_table();
9180
        }
9181
9182
        return $content;
9183
    }
9184
9185
    /**
9186
     * @return int value in minutes
9187
     */
9188
    public function getResultAccess()
9189
    {
9190
        $extraFieldValue = new ExtraFieldValue('exercise');
9191
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9192
            $this->iId,
9193
            'results_available_for_x_minutes'
9194
        );
9195
9196
        if (!empty($value) && isset($value['value'])) {
9197
            return (int) $value['value'];
9198
        }
9199
9200
        return 0;
9201
    }
9202
9203
    /**
9204
     * @param array $exerciseResultInfo
9205
     *
9206
     * @return bool
9207
     */
9208
    public function getResultAccessTimeDiff($exerciseResultInfo)
9209
    {
9210
        $value = $this->getResultAccess();
9211
        if (!empty($value)) {
9212
            $endDate = new DateTime($exerciseResultInfo['exe_date'], new DateTimeZone('UTC'));
9213
            $endDate->add(new DateInterval('PT'.$value.'M'));
9214
            $now = time();
9215
            if ($endDate->getTimestamp() > $now) {
9216
                return (int) $endDate->getTimestamp() - $now;
9217
            }
9218
        }
9219
9220
        return 0;
9221
    }
9222
9223
    /**
9224
     * @param array $exerciseResultInfo
9225
     *
9226
     * @return bool
9227
     */
9228
    public function hasResultsAccess($exerciseResultInfo)
9229
    {
9230
        $diff = $this->getResultAccessTimeDiff($exerciseResultInfo);
9231
        if (0 === $diff) {
9232
            return false;
9233
        }
9234
9235
        return true;
9236
    }
9237
9238
    /**
9239
     * @return int
9240
     */
9241
    public function getResultsAccess()
9242
    {
9243
        $extraFieldValue = new ExtraFieldValue('exercise');
9244
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
9245
            $this->iId,
9246
            'results_available_for_x_minutes'
9247
        );
9248
        if (!empty($value)) {
9249
            return (int) $value;
9250
        }
9251
9252
        return 0;
9253
    }
9254
9255
    /**
9256
     * @param int   $questionId
9257
     * @param bool  $show_results
9258
     * @param array $question_result
9259
     */
9260
    public function getDelineationResult(Question $objQuestionTmp, $questionId, $show_results, $question_result)
9261
    {
9262
        $id = (int) $objQuestionTmp->id;
9263
        $questionId = (int) $questionId;
9264
9265
        $final_overlap = $question_result['extra']['final_overlap'];
9266
        $final_missing = $question_result['extra']['final_missing'];
9267
        $final_excess = $question_result['extra']['final_excess'];
9268
9269
        $overlap_color = $question_result['extra']['overlap_color'];
9270
        $missing_color = $question_result['extra']['missing_color'];
9271
        $excess_color = $question_result['extra']['excess_color'];
9272
9273
        $threadhold1 = $question_result['extra']['threadhold1'];
9274
        $threadhold2 = $question_result['extra']['threadhold2'];
9275
        $threadhold3 = $question_result['extra']['threadhold3'];
9276
9277
        if ($show_results) {
9278
            if ($overlap_color) {
9279
                $overlap_color = 'green';
9280
            } else {
9281
                $overlap_color = 'red';
9282
            }
9283
9284
            if ($missing_color) {
9285
                $missing_color = 'green';
9286
            } else {
9287
                $missing_color = 'red';
9288
            }
9289
            if ($excess_color) {
9290
                $excess_color = 'green';
9291
            } else {
9292
                $excess_color = 'red';
9293
            }
9294
9295
            if (!is_numeric($final_overlap)) {
9296
                $final_overlap = 0;
9297
            }
9298
9299
            if (!is_numeric($final_missing)) {
9300
                $final_missing = 0;
9301
            }
9302
            if (!is_numeric($final_excess)) {
9303
                $final_excess = 0;
9304
            }
9305
9306
            if ($final_excess > 100) {
9307
                $final_excess = 100;
9308
            }
9309
9310
            $table_resume = '
9311
                    <table class="data_table">
9312
                        <tr class="row_odd" >
9313
                            <td>&nbsp;</td>
9314
                            <td><b>'.get_lang('Requirements').'</b></td>
9315
                            <td><b>'.get_lang('Your answer').'</b></td>
9316
                        </tr>
9317
                        <tr class="row_even">
9318
                            <td><b>'.get_lang('Overlapping areaping area').'</b></td>
9319
                            <td>'.get_lang('Minimumimum').' '.$threadhold1.'</td>
9320
                            <td>
9321
                                <div style="color:'.$overlap_color.'">
9322
                                    '.$final_overlap < 0 ? 0 : (int) $final_overlap.'
9323
                                </div>
9324
                            </td>
9325
                        </tr>
9326
                        <tr>
9327
                            <td><b>'.get_lang('Excessive areaive area').'</b></td>
9328
                            <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold2.'</td>
9329
                            <td>
9330
                                <div style="color:'.$excess_color.'">
9331
                                    '.$final_excess < 0 ? 0 : (int) $final_excess.'
9332
                                </div>
9333
                            </td>
9334
                        </tr>
9335
                        <tr class="row_even">
9336
                            <td><b>'.get_lang('Missing area area').'</b></td>
9337
                            <td>'.get_lang('max. 20 characters, e.g. <i>INNOV21</i>').' '.$threadhold3.'</td>
9338
                            <td>
9339
                                <div style="color:'.$missing_color.'">
9340
                                    '.$final_missing < 0 ? 0 : (int) $final_missing.'
9341
                                </div>
9342
                            </td>
9343
                        </tr>
9344
                    </table>
9345
                ';
9346
9347
            $answerType = $objQuestionTmp->selectType();
9348
            if (HOT_SPOT_DELINEATION != $answerType) {
9349
                $item_list = explode('@@', $destination);
9350
                $try = $item_list[0];
9351
                $lp = $item_list[1];
9352
                $destinationid = $item_list[2];
9353
                $url = $item_list[3];
9354
                $table_resume = '';
9355
            } else {
9356
                if (0 == $next) {
9357
                    $try = $try_hotspot;
9358
                    $lp = $lp_hotspot;
9359
                    $destinationid = $select_question_hotspot;
9360
                    $url = $url_hotspot;
9361
                } else {
9362
                    //show if no error
9363
                    $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
9364
                    $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
9365
                }
9366
            }
9367
9368
            echo '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>';
9369
            if (HOT_SPOT_DELINEATION == $answerType) {
9370
                if ($organs_at_risk_hit > 0) {
9371
                    $message = '<br />'.get_lang('Your result is :').' <b>'.$result_comment.'</b><br />';
9372
                    $message .= '<p style="color:#DC0A0A;"><b>'.get_lang('One (or more) area at risk has been hit').'</b></p>';
9373
                } else {
9374
                    $message = '<p>'.get_lang('Your delineation :').'</p>';
9375
                    $message .= $table_resume;
9376
                    $message .= '<br />'.get_lang('Your result is :').' <b>'.$result_comment.'</b><br />';
9377
                }
9378
                $message .= '<p>'.$comment.'</p>';
9379
                echo $message;
9380
            } else {
9381
                echo '<p>'.$comment.'</p>';
9382
            }
9383
9384
            // Showing the score
9385
            /*$queryfree = "SELECT marks FROM $TBL_TRACK_ATTEMPT
9386
                          WHERE exe_id = $id AND question_id =  $questionId";
9387
            $resfree = Database::query($queryfree);
9388
            $questionScore = Database::result($resfree, 0, 'marks');
9389
            $totalScore += $questionScore;*/
9390
            $relPath = api_get_path(REL_PATH);
9391
            echo '</table></td></tr>';
9392
            echo "
9393
                        <tr>
9394
                            <td colspan=\"2\">
9395
                                <div id=\"hotspot-solution\"></div>
9396
                                <script>
9397
                                    $(function() {
9398
                                        new HotspotQuestion({
9399
                                            questionId: $questionId,
9400
                                            exerciseId: {$this->getId()},
9401
                                            exeId: $id,
9402
                                            selector: '#hotspot-solution',
9403
                                            for: 'solution',
9404
                                            relPath: '$relPath'
9405
                                        });
9406
                                    });
9407
                                </script>
9408
                            </td>
9409
                        </tr>
9410
                    </table>
9411
                ";
9412
        }
9413
    }
9414
9415
    /**
9416
     * Clean exercise session variables.
9417
     */
9418
    public static function cleanSessionVariables()
9419
    {
9420
        Session::erase('objExercise');
9421
        Session::erase('exe_id');
9422
        Session::erase('calculatedAnswerId');
9423
        Session::erase('duration_time_previous');
9424
        Session::erase('duration_time');
9425
        Session::erase('objQuestion');
9426
        Session::erase('objAnswer');
9427
        Session::erase('questionList');
9428
        Session::erase('exerciseResult');
9429
        Session::erase('firstTime');
9430
9431
        Session::erase('exerciseResultCoordinates');
9432
        Session::erase('hotspot_coord');
9433
        Session::erase('hotspot_dest');
9434
        Session::erase('hotspot_delineation_result');
9435
    }
9436
9437
    /**
9438
     * Get the first LP found matching the session ID.
9439
     *
9440
     * @param int $sessionId
9441
     *
9442
     * @return array
9443
     */
9444
    public function getLpBySession($sessionId)
9445
    {
9446
        if (!empty($this->lpList)) {
9447
            $sessionId = (int) $sessionId;
9448
9449
            foreach ($this->lpList as $lp) {
9450
                if ((int) $lp['session_id'] == $sessionId) {
9451
                    return $lp;
9452
                }
9453
            }
9454
9455
            return current($this->lpList);
9456
        }
9457
9458
        return [
9459
            'lp_id' => 0,
9460
            'max_score' => 0,
9461
            'session_id' => 0,
9462
        ];
9463
    }
9464
9465
    public static function saveExerciseInLp($safe_item_id, $safe_exe_id)
9466
    {
9467
        $lp = Session::read('oLP');
9468
9469
        $safe_exe_id = (int) $safe_exe_id;
9470
        $safe_item_id = (int) $safe_item_id;
9471
9472
        if (empty($lp) || empty($safe_exe_id) || empty($safe_item_id)) {
9473
            return false;
9474
        }
9475
9476
        $viewId = $lp->get_view_id();
9477
        $course_id = api_get_course_int_id();
9478
        $userId = (int) api_get_user_id();
9479
        $viewId = (int) $viewId;
9480
9481
        $TBL_TRACK_EXERCICES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
9482
        $TBL_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
9483
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
9484
9485
        $sql = "SELECT start_date, exe_date, exe_result, exe_weighting, exe_exo_id, exe_duration
9486
                FROM $TBL_TRACK_EXERCICES
9487
                WHERE exe_id = $safe_exe_id AND exe_user_id = $userId";
9488
        $res = Database::query($sql);
9489
        $row_dates = Database::fetch_array($res);
9490
9491
        if (empty($row_dates)) {
9492
            return false;
9493
        }
9494
9495
        $duration = (int) $row_dates['exe_duration'];
9496
        $score = (float) $row_dates['exe_result'];
9497
        $max_score = (float) $row_dates['exe_weighting'];
9498
9499
        $sql = "UPDATE $TBL_LP_ITEM SET
9500
                    max_score = '$max_score'
9501
                WHERE iid = $safe_item_id";
9502
        Database::query($sql);
9503
9504
        $sql = "SELECT id FROM $TBL_LP_ITEM_VIEW
9505
                WHERE
9506
                    c_id = $course_id AND
9507
                    lp_item_id = $safe_item_id AND
9508
                    lp_view_id = $viewId
9509
                ORDER BY id DESC
9510
                LIMIT 1";
9511
        $res_last_attempt = Database::query($sql);
9512
9513
        if (Database::num_rows($res_last_attempt) && !api_is_invitee()) {
9514
            $row_last_attempt = Database::fetch_row($res_last_attempt);
9515
            $lp_item_view_id = $row_last_attempt[0];
9516
9517
            $exercise = new Exercise($course_id);
9518
            $exercise->read($row_dates['exe_exo_id']);
9519
            $status = 'completed';
9520
9521
            if (!empty($exercise->pass_percentage)) {
9522
                $status = 'failed';
9523
                $success = ExerciseLib::isSuccessExerciseResult(
9524
                    $score,
9525
                    $max_score,
9526
                    $exercise->pass_percentage
9527
                );
9528
                if ($success) {
9529
                    $status = 'passed';
9530
                }
9531
            }
9532
9533
            $sql = "UPDATE $TBL_LP_ITEM_VIEW SET
9534
                        status = '$status',
9535
                        score = $score,
9536
                        total_time = $duration
9537
                    WHERE iid = $lp_item_view_id";
9538
            Database::query($sql);
9539
9540
            $sql = "UPDATE $TBL_TRACK_EXERCICES SET
9541
                        orig_lp_item_view_id = $lp_item_view_id
9542
                    WHERE exe_id = ".$safe_exe_id;
9543
            Database::query($sql);
9544
        }
9545
    }
9546
9547
    /**
9548
     * Get the user answers saved in exercise.
9549
     *
9550
     * @param int $attemptId
9551
     *
9552
     * @return array
9553
     */
9554
    public function getUserAnswersSavedInExercise($attemptId)
9555
    {
9556
        $exerciseResult = [];
9557
9558
        $attemptList = Event::getAllExerciseEventByExeId($attemptId);
9559
9560
        foreach ($attemptList as $questionId => $options) {
9561
            foreach ($options as $option) {
9562
                $question = Question::read($option['question_id']);
9563
9564
                switch ($question->type) {
9565
                    case FILL_IN_BLANKS:
9566
                        $option['answer'] = $this->fill_in_blank_answer_to_string($option['answer']);
9567
                        break;
9568
                }
9569
9570
                if (!empty($option['answer'])) {
9571
                    $exerciseResult[] = $questionId;
9572
9573
                    break;
9574
                }
9575
            }
9576
        }
9577
9578
        return $exerciseResult;
9579
    }
9580
9581
    /**
9582
     * Get the number of user answers saved in exercise.
9583
     *
9584
     * @param int $attemptId
9585
     *
9586
     * @return int
9587
     */
9588
    public function countUserAnswersSavedInExercise($attemptId)
9589
    {
9590
        $answers = $this->getUserAnswersSavedInExercise($attemptId);
9591
9592
        return count($answers);
9593
    }
9594
9595
    public static function allowAction($action)
9596
    {
9597
        if (api_is_platform_admin()) {
9598
            return true;
9599
        }
9600
9601
        $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
9602
        $disableClean = api_get_configuration_value('disable_clean_exercise_results_for_teachers');
9603
9604
        switch ($action) {
9605
            case 'delete':
9606
                if (api_is_allowed_to_edit(null, true)) {
9607
                    if ($limitTeacherAccess) {
9608
                        return false;
9609
                    }
9610
9611
                    return true;
9612
                }
9613
                break;
9614
            case 'clean_results':
9615
                if (api_is_allowed_to_edit(null, true)) {
9616
                    if ($limitTeacherAccess) {
9617
                        return false;
9618
                    }
9619
9620
                    if ($disableClean) {
9621
                        return false;
9622
                    }
9623
9624
                    return true;
9625
                }
9626
9627
                break;
9628
        }
9629
9630
        return false;
9631
    }
9632
9633
    public static function getLpListFromExercise($exerciseId, $courseId)
9634
    {
9635
        $tableLpItem = Database::get_course_table(TABLE_LP_ITEM);
9636
        $tblLp = Database::get_course_table(TABLE_LP_MAIN);
9637
9638
        $exerciseId = (int) $exerciseId;
9639
        $courseId = (int) $courseId;
9640
9641
        $sql = "SELECT
9642
                    lp.name,
9643
                    lpi.lp_id,
9644
                    lpi.max_score,
9645
                    lp.session_id
9646
                FROM $tableLpItem lpi
9647
                INNER JOIN $tblLp lp
9648
                ON (lpi.lp_id = lp.iid AND lpi.c_id = lp.c_id)
9649
                WHERE
9650
                    lpi.c_id = $courseId AND
9651
                    lpi.item_type = '".TOOL_QUIZ."' AND
9652
                    lpi.path = '$exerciseId'";
9653
        $result = Database::query($sql);
9654
        $lpList = [];
9655
        if (Database::num_rows($result) > 0) {
9656
            $lpList = Database::store_result($result, 'ASSOC');
9657
        }
9658
9659
        return $lpList;
9660
    }
9661
9662
    /**
9663
     * Get number of questions in exercise by user attempt.
9664
     *
9665
     * @return int
9666
     */
9667
    private function countQuestionsInExercise()
9668
    {
9669
        $lpId = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
9670
        $lpItemId = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
9671
        $lpItemViewId = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
9672
9673
        $trackInfo = $this->get_stat_track_exercise_info($lpId, $lpItemId, $lpItemViewId);
9674
9675
        if (!empty($trackInfo)) {
9676
            $questionIds = explode(',', $trackInfo['data_tracking']);
9677
9678
            return count($questionIds);
9679
        }
9680
9681
        return $this->getQuestionCount();
9682
    }
9683
9684
    /**
9685
     * Gets the question list ordered by the question_order setting (drag and drop).
9686
     *
9687
     * @param bool $adminView Optional.
9688
     *
9689
     * @return array
9690
     */
9691
    private function getQuestionOrderedList($adminView = false)
9692
    {
9693
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
9694
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
9695
9696
        // Getting question_order to verify that the question
9697
        // list is correct and all question_order's were set
9698
        $sql = "SELECT DISTINCT count(e.question_order) as count
9699
                FROM $TBL_EXERCICE_QUESTION e
9700
                INNER JOIN $TBL_QUESTIONS q
9701
                ON (e.question_id = q.iid AND e.c_id = q.c_id)
9702
                WHERE
9703
                  e.c_id = {$this->course_id} AND
9704
                  e.exercice_id	= ".$this->getId();
9705
9706
        $result = Database::query($sql);
9707
        $row = Database::fetch_array($result);
9708
        $count_question_orders = $row['count'];
9709
9710
        // Getting question list from the order (question list drag n drop interface).
9711
        $sql = "SELECT DISTINCT e.question_id, e.question_order
9712
                FROM $TBL_EXERCICE_QUESTION e
9713
                INNER JOIN $TBL_QUESTIONS q
9714
                ON (e.question_id = q.iid AND e.c_id = q.c_id)
9715
                WHERE
9716
                    e.c_id = {$this->course_id} AND
9717
                    e.exercice_id = '".$this->getId()."'
9718
                ORDER BY question_order";
9719
        $result = Database::query($sql);
9720
9721
        // Fills the array with the question ID for this exercise
9722
        // the key of the array is the question position
9723
        $temp_question_list = [];
9724
        $counter = 1;
9725
        $questionList = [];
9726
        while ($new_object = Database::fetch_object($result)) {
9727
            if (!$adminView) {
9728
                // Correct order.
9729
                $questionList[$new_object->question_order] = $new_object->question_id;
9730
            } else {
9731
                $questionList[$counter] = $new_object->question_id;
9732
            }
9733
9734
            // Just in case we save the order in other array
9735
            $temp_question_list[$counter] = $new_object->question_id;
9736
            $counter++;
9737
        }
9738
9739
        if (!empty($temp_question_list)) {
9740
            /* If both array don't match it means that question_order was not correctly set
9741
               for all questions using the default mysql order */
9742
            if (count($temp_question_list) != $count_question_orders) {
9743
                $questionList = $temp_question_list;
9744
            }
9745
        }
9746
9747
        return $questionList;
9748
    }
9749
9750
    /**
9751
     * Select N values from the questions per category array.
9752
     *
9753
     * @param array $categoriesAddedInExercise
9754
     * @param array $question_list
9755
     * @param array $questions_by_category     per category
9756
     * @param bool  $flatResult
9757
     * @param bool  $randomizeQuestions
9758
     *
9759
     * @return array
9760
     */
9761
    private function pickQuestionsPerCategory(
9762
        $categoriesAddedInExercise,
9763
        $question_list,
9764
        &$questions_by_category,
9765
        $flatResult = true,
9766
        $randomizeQuestions = false
9767
    ) {
9768
        $addAll = true;
9769
        $categoryCountArray = [];
9770
9771
        // Getting how many questions will be selected per category.
9772
        if (!empty($categoriesAddedInExercise)) {
9773
            $addAll = false;
9774
            // Parsing question according the category rel exercise settings
9775
            foreach ($categoriesAddedInExercise as $category_info) {
9776
                $category_id = $category_info['category_id'];
9777
                if (isset($questions_by_category[$category_id])) {
9778
                    // How many question will be picked from this category.
9779
                    $count = $category_info['count_questions'];
9780
                    // -1 means all questions
9781
                    $categoryCountArray[$category_id] = $count;
9782
                    if (-1 == $count) {
9783
                        $categoryCountArray[$category_id] = 999;
9784
                    }
9785
                }
9786
            }
9787
        }
9788
9789
        if (!empty($questions_by_category)) {
9790
            $temp_question_list = [];
9791
            foreach ($questions_by_category as $category_id => &$categoryQuestionList) {
9792
                if (isset($categoryCountArray) && !empty($categoryCountArray)) {
9793
                    $numberOfQuestions = 0;
9794
                    if (isset($categoryCountArray[$category_id])) {
9795
                        $numberOfQuestions = $categoryCountArray[$category_id];
9796
                    }
9797
                }
9798
9799
                if ($addAll) {
9800
                    $numberOfQuestions = 999;
9801
                }
9802
9803
                if (!empty($numberOfQuestions)) {
9804
                    $elements = TestCategory::getNElementsFromArray(
9805
                        $categoryQuestionList,
9806
                        $numberOfQuestions,
9807
                        $randomizeQuestions
9808
                    );
9809
9810
                    if (!empty($elements)) {
9811
                        $temp_question_list[$category_id] = $elements;
9812
                        $categoryQuestionList = $elements;
9813
                    }
9814
                }
9815
            }
9816
9817
            if (!empty($temp_question_list)) {
9818
                if ($flatResult) {
9819
                    $temp_question_list = array_flatten($temp_question_list);
9820
                }
9821
                $question_list = $temp_question_list;
9822
            }
9823
        }
9824
9825
        return $question_list;
9826
    }
9827
9828
    /**
9829
     * Changes the exercise id.
9830
     *
9831
     * @param int $id - exercise id
9832
     */
9833
    private function updateId($id)
9834
    {
9835
        $this->iId = $id;
9836
    }
9837
9838
    /**
9839
     * Sends a notification when a user ends an examn.
9840
     *
9841
     * @param array  $question_list_answers
9842
     * @param string $origin
9843
     * @param array  $user_info
9844
     * @param string $url_email
9845
     * @param array  $teachers
9846
     */
9847
    private function sendNotificationForOpenQuestions(
9848
        $question_list_answers,
9849
        $origin,
9850
        $user_info,
9851
        $url_email,
9852
        $teachers
9853
    ) {
9854
        // Email configuration settings
9855
        $courseCode = api_get_course_id();
9856
        $courseInfo = api_get_course_info($courseCode);
9857
        $sessionId = api_get_session_id();
9858
        $sessionData = '';
9859
        if (!empty($sessionId)) {
9860
            $sessionInfo = api_get_session_info($sessionId);
9861
            if (!empty($sessionInfo)) {
9862
                $sessionData = '<tr>'
9863
                    .'<td><em>'.get_lang('Session name').'</em></td>'
9864
                    .'<td>&nbsp;<b>'.$sessionInfo['name'].'</b></td>'
9865
                    .'</tr>';
9866
            }
9867
        }
9868
9869
        $msg = get_lang('A learner has answered an open question').'<br /><br />'
9870
            .get_lang('Attempt details').' : <br /><br />'
9871
            .'<table>'
9872
            .'<tr>'
9873
            .'<td><em>'.get_lang('Course name').'</em></td>'
9874
            .'<td>&nbsp;<b>#course#</b></td>'
9875
            .'</tr>'
9876
            .$sessionData
9877
            .'<tr>'
9878
            .'<td>'.get_lang('Test attempted').'</td>'
9879
            .'<td>&nbsp;#exercise#</td>'
9880
            .'</tr>'
9881
            .'<tr>'
9882
            .'<td>'.get_lang('Learner name').'</td>'
9883
            .'<td>&nbsp;#firstName# #lastName#</td>'
9884
            .'</tr>'
9885
            .'<tr>'
9886
            .'<td>'.get_lang('Learner e-mail').'</td>'
9887
            .'<td>&nbsp;#mail#</td>'
9888
            .'</tr>'
9889
            .'</table>';
9890
9891
        $open_question_list = null;
9892
        foreach ($question_list_answers as $item) {
9893
            $question = $item['question'];
9894
            $answer = $item['answer'];
9895
            $answer_type = $item['answer_type'];
9896
9897
            if (!empty($question) && !empty($answer) && FREE_ANSWER == $answer_type) {
9898
                $open_question_list .=
9899
                    '<tr>'
9900
                    .'<td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>'
9901
                    .'<td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>'
9902
                    .'</tr>'
9903
                    .'<tr>'
9904
                    .'<td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>'
9905
                    .'<td valign="top" bgcolor="#F3F3F3">'.$answer.'</td>'
9906
                    .'</tr>';
9907
            }
9908
        }
9909
9910
        if (!empty($open_question_list)) {
9911
            $msg .= '<p><br />'.get_lang('A learner has answered an open questionAre').' :</p>'.
9912
                '<table width="730" height="136" border="0" cellpadding="3" cellspacing="3">';
9913
            $msg .= $open_question_list;
9914
            $msg .= '</table><br />';
9915
9916
            $msg = str_replace('#exercise#', $this->exercise, $msg);
9917
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg);
9918
            $msg = str_replace('#lastName#', $user_info['lastname'], $msg);
9919
            $msg = str_replace('#mail#', $user_info['email'], $msg);
9920
            $msg = str_replace(
9921
                '#course#',
9922
                Display::url($courseInfo['title'], $courseInfo['course_public_url'].'?sid='.$sessionId),
9923
                $msg
9924
            );
9925
9926
            if ('learnpath' != $origin) {
9927
                $msg .= '<br /><a href="#url#">'.get_lang('Click this link to check the answer and/or give feedback').'</a>';
9928
            }
9929
            $msg = str_replace('#url#', $url_email, $msg);
9930
            $subject = get_lang('A learner has answered an open question');
9931
9932
            if (!empty($teachers)) {
9933
                foreach ($teachers as $user_id => $teacher_data) {
9934
                    MessageManager::send_message_simple(
9935
                        $user_id,
9936
                        $subject,
9937
                        $msg
9938
                    );
9939
                }
9940
            }
9941
        }
9942
    }
9943
9944
    /**
9945
     * Send notification for oral questions.
9946
     *
9947
     * @param array  $question_list_answers
9948
     * @param string $origin
9949
     * @param int    $exe_id
9950
     * @param array  $user_info
9951
     * @param string $url_email
9952
     * @param array  $teachers
9953
     */
9954
    private function sendNotificationForOralQuestions(
9955
        $question_list_answers,
9956
        $origin,
9957
        $exe_id,
9958
        $user_info,
9959
        $url_email,
9960
        $teachers
9961
    ) {
9962
        // Email configuration settings
9963
        $courseCode = api_get_course_id();
9964
        $courseInfo = api_get_course_info($courseCode);
9965
        $oral_question_list = null;
9966
        foreach ($question_list_answers as $item) {
9967
            $question = $item['question'];
9968
            $file = $item['generated_oral_file'];
9969
            $answer = $item['answer'];
9970
            if (0 == $answer) {
9971
                $answer = '';
9972
            }
9973
            $answer_type = $item['answer_type'];
9974
            if (!empty($question) && (!empty($answer) || !empty($file)) && ORAL_EXPRESSION == $answer_type) {
9975
                if (!empty($file)) {
9976
                    $file = Display::url($file, $file);
9977
                }
9978
                $oral_question_list .= '<br />
9979
                    <table width="730" height="136" border="0" cellpadding="3" cellspacing="3">
9980
                    <tr>
9981
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
9982
                        <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
9983
                    </tr>
9984
                    <tr>
9985
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
9986
                        <td valign="top" bgcolor="#F3F3F3">'.$answer.$file.'</td>
9987
                    </tr></table>';
9988
            }
9989
        }
9990
9991
        if (!empty($oral_question_list)) {
9992
            $msg = get_lang('A learner has attempted one or more oral question').'<br /><br />
9993
                    '.get_lang('Attempt details').' : <br /><br />
9994
                    <table>
9995
                        <tr>
9996
                            <td><em>'.get_lang('Course name').'</em></td>
9997
                            <td>&nbsp;<b>#course#</b></td>
9998
                        </tr>
9999
                        <tr>
10000
                            <td>'.get_lang('Test attempted').'</td>
10001
                            <td>&nbsp;#exercise#</td>
10002
                        </tr>
10003
                        <tr>
10004
                            <td>'.get_lang('Learner name').'</td>
10005
                            <td>&nbsp;#firstName# #lastName#</td>
10006
                        </tr>
10007
                        <tr>
10008
                            <td>'.get_lang('Learner e-mail').'</td>
10009
                            <td>&nbsp;#mail#</td>
10010
                        </tr>
10011
                    </table>';
10012
            $msg .= '<br />'.sprintf(get_lang('A learner has attempted one or more oral questionAreX'), $oral_question_list).'<br />';
10013
            $msg1 = str_replace('#exercise#', $this->exercise, $msg);
10014
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg1);
10015
            $msg1 = str_replace('#lastName#', $user_info['lastname'], $msg);
10016
            $msg = str_replace('#mail#', $user_info['email'], $msg1);
10017
            $msg = str_replace('#course#', $courseInfo['name'], $msg1);
10018
10019
            if (!in_array($origin, ['learnpath', 'embeddable'])) {
10020
                $msg .= '<br /><a href="#url#">'.get_lang('Click this link to check the answer and/or give feedback').'</a>';
10021
            }
10022
            $msg1 = str_replace('#url#', $url_email, $msg);
10023
            $mail_content = $msg1;
10024
            $subject = get_lang('A learner has attempted one or more oral question');
10025
10026
            if (!empty($teachers)) {
10027
                foreach ($teachers as $user_id => $teacher_data) {
10028
                    MessageManager::send_message_simple(
10029
                        $user_id,
10030
                        $subject,
10031
                        $mail_content
10032
                    );
10033
                }
10034
            }
10035
        }
10036
    }
10037
10038
    /**
10039
     * Returns an array with the media list.
10040
     *
10041
     * @param array $questionList question list
10042
     *
10043
     * @example there's 1 question with iid 5 that belongs to the media question with iid = 100
10044
     * <code>
10045
     * array (size=2)
10046
     *  999 =>
10047
     *    array (size=3)
10048
     *      0 => int 7
10049
     *      1 => int 6
10050
     *      2 => int 3254
10051
     *  100 =>
10052
     *   array (size=1)
10053
     *      0 => int 5
10054
     *  </code>
10055
     */
10056
    private function setMediaList($questionList)
10057
    {
10058
        $mediaList = [];
10059
        /*
10060
         * Media feature is not activated in 1.11.x
10061
        if (!empty($questionList)) {
10062
            foreach ($questionList as $questionId) {
10063
                $objQuestionTmp = Question::read($questionId, $this->course_id);
10064
                // If a media question exists
10065
                if (isset($objQuestionTmp->parent_id) && $objQuestionTmp->parent_id != 0) {
10066
                    $mediaList[$objQuestionTmp->parent_id][] = $objQuestionTmp->id;
10067
                } else {
10068
                    // Always the last item
10069
                    $mediaList[999][] = $objQuestionTmp->id;
10070
                }
10071
            }
10072
        }*/
10073
10074
        $this->mediaList = $mediaList;
10075
    }
10076
10077
    /**
10078
     * @return HTML_QuickForm_group
10079
     */
10080
    private function setResultDisabledGroup(FormValidator $form)
10081
    {
10082
        $resultDisabledGroup = [];
10083
10084
        $resultDisabledGroup[] = $form->createElement(
10085
            'radio',
10086
            'results_disabled',
10087
            null,
10088
            get_lang('Auto-evaluation mode: show score and expected answers'),
10089
            '0',
10090
            ['id' => 'result_disabled_0']
10091
        );
10092
10093
        $resultDisabledGroup[] = $form->createElement(
10094
            'radio',
10095
            'results_disabled',
10096
            null,
10097
            get_lang('Exam mode: Do not show score nor answers'),
10098
            '1',
10099
            ['id' => 'result_disabled_1', 'onclick' => 'check_results_disabled()']
10100
        );
10101
10102
        $resultDisabledGroup[] = $form->createElement(
10103
            'radio',
10104
            'results_disabled',
10105
            null,
10106
            get_lang('Practice mode: Show score only, by category if at least one is used'),
10107
            '2',
10108
            ['id' => 'result_disabled_2', 'onclick' => 'check_results_disabled()']
10109
        );
10110
10111
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
10112
            return $form->addGroup(
10113
                $resultDisabledGroup,
10114
                null,
10115
                get_lang('ShowResults and feedback and feedback and feedback and feedback and feedback and feedbackToStudents')
10116
            );
10117
        }
10118
10119
        $resultDisabledGroup[] = $form->createElement(
10120
            'radio',
10121
            'results_disabled',
10122
            null,
10123
            get_lang('Show score on every attempt, show correct answers only on last attempt (only works with an attempts limit)'),
10124
            '4',
10125
            ['id' => 'result_disabled_4']
10126
        );
10127
10128
        $resultDisabledGroup[] = $form->createElement(
10129
            'radio',
10130
            'results_disabled',
10131
            null,
10132
            get_lang('Do not show the score (only when user finishes all attempts) but show feedback for each attempt.'),
10133
            '5',
10134
            ['id' => 'result_disabled_5', 'onclick' => 'check_results_disabled()']
10135
        );
10136
10137
        $resultDisabledGroup[] = $form->createElement(
10138
            'radio',
10139
            'results_disabled',
10140
            null,
10141
            get_lang('TestRankingMode'),
10142
            RESULT_DISABLE_RANKING,
10143
            ['id' => 'result_disabled_6']
10144
        );
10145
10146
        $resultDisabledGroup[] = $form->createElement(
10147
            'radio',
10148
            'results_disabled',
10149
            null,
10150
            get_lang('TestShowOnlyGlobalScoreAndCorrect answers'),
10151
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
10152
            ['id' => 'result_disabled_7']
10153
        );
10154
10155
        $resultDisabledGroup[] = $form->createElement(
10156
            'radio',
10157
            'results_disabled',
10158
            null,
10159
            get_lang('TestAutoEvaluationAndRankingMode'),
10160
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
10161
            ['id' => 'result_disabled_8']
10162
        );
10163
10164
        return $form->addGroup(
10165
            $resultDisabledGroup,
10166
            null,
10167
            get_lang('ShowResults and feedback and feedback and feedback and feedback and feedback and feedbackToStudents')
10168
        );
10169
    }
10170
}
10171