Passed
Push — 1.11.x ( 841eae...9486ba )
by Julito
14:07
created

Exercise::save()   F

Complexity

Conditions 32
Paths > 20000

Size

Total Lines 235
Code Lines 160

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 32
eloc 160
nop 1
dl 0
loc 235
rs 0
c 0
b 0
f 0
nc 80384

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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