Passed
Pull Request — 1.11.x (#3886)
by Angel Fernando Quiroz
13:31
created

Exercise::getUsersInExercise()   D

Complexity

Conditions 18

Size

Total Lines 105
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 18
eloc 58
nop 6
dl 0
loc 105
rs 4.8666
c 3
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
6
use Chamilo\CoreBundle\Entity\GradebookLink;
7
use Chamilo\CoreBundle\Entity\TrackEExerciseConfirmation;
8
use Chamilo\CoreBundle\Entity\TrackEHotspot;
9
use Chamilo\CourseBundle\Entity\CExerciseCategory;
10
use ChamiloSession as Session;
11
use Doctrine\DBAL\Types\Type;
12
13
/**
14
 * Class Exercise.
15
 *
16
 * Allows to instantiate an object of type Exercise
17
 *
18
 * @todo use getters and setters correctly
19
 *
20
 * @author Olivier Brouckaert
21
 * @author Julio Montoya Cleaning exercises
22
 * Modified by Hubert Borderiou #294
23
 */
24
class Exercise
25
{
26
    public const PAGINATION_ITEMS_PER_PAGE = 20;
27
    public $iId;
28
    public $id;
29
    public $name;
30
    public $title;
31
    public $exercise;
32
    public $description;
33
    public $sound;
34
    public $type; //ALL_ON_ONE_PAGE or ONE_PER_PAGE
35
    public $random;
36
    public $random_answers;
37
    public $active;
38
    public $timeLimit;
39
    public $attempts;
40
    public $feedback_type;
41
    public $end_time;
42
    public $start_time;
43
    public $questionList; // array with the list of this exercise's questions
44
    /* including question list of the media */
45
    public $questionListUncompressed;
46
    public $results_disabled;
47
    public $expired_time;
48
    public $course;
49
    public $course_id;
50
    public $propagate_neg;
51
    public $saveCorrectAnswers;
52
    public $review_answers;
53
    public $randomByCat;
54
    public $text_when_finished;
55
    public $display_category_name;
56
    public $pass_percentage;
57
    public $edit_exercise_in_lp = false;
58
    public $is_gradebook_locked = false;
59
    public $exercise_was_added_in_lp = false;
60
    public $lpList = [];
61
    public $force_edit_exercise_in_lp = false;
62
    public $categories;
63
    public $categories_grouping = true;
64
    public $endButton = 0;
65
    public $categoryWithQuestionList;
66
    public $mediaList;
67
    public $loadQuestionAJAX = false;
68
    // Notification send to the teacher.
69
    public $emailNotificationTemplate = null;
70
    // Notification send to the student.
71
    public $emailNotificationTemplateToUser = null;
72
    public $countQuestions = 0;
73
    public $fastEdition = false;
74
    public $modelType = 1;
75
    public $questionSelectionType = EX_Q_SELECTION_ORDERED;
76
    public $hideQuestionTitle = 0;
77
    public $scoreTypeModel = 0;
78
    public $categoryMinusOne = true; // Shows the category -1: See BT#6540
79
    public $globalCategoryId = null;
80
    public $onSuccessMessage = null;
81
    public $onFailedMessage = null;
82
    public $emailAlert;
83
    public $notifyUserByEmail = '';
84
    public $sessionId = 0;
85
    public $questionFeedbackEnabled = false;
86
    public $questionTypeWithFeedback;
87
    public $showPreviousButton;
88
    public $notifications;
89
    public $export = false;
90
    public $autolaunch;
91
    public $exerciseCategoryId;
92
    public $pageResultConfiguration;
93
    public $hideQuestionNumber;
94
    public $preventBackwards;
95
    public $currentQuestion;
96
    public $hideComment;
97
    public $hideNoAnswer;
98
    public $hideExpectedAnswer;
99
    public $forceShowExpectedChoiceColumn;
100
    public $disableHideCorrectAnsweredQuestions;
101
102
    /**
103
     * Constructor of the class.
104
     *
105
     * @param int $courseId
106
     *
107
     * @author Olivier Brouckaert
108
     */
109
    public function __construct($courseId = 0)
110
    {
111
        $this->iId = 0;
112
        $this->id = 0;
113
        $this->exercise = '';
114
        $this->description = '';
115
        $this->sound = '';
116
        $this->type = ALL_ON_ONE_PAGE;
117
        $this->random = 0;
118
        $this->random_answers = 0;
119
        $this->active = 1;
120
        $this->questionList = [];
121
        $this->timeLimit = 0;
122
        $this->end_time = '';
123
        $this->start_time = '';
124
        $this->results_disabled = 1;
125
        $this->expired_time = 0;
126
        $this->propagate_neg = 0;
127
        $this->saveCorrectAnswers = 0;
128
        $this->review_answers = false;
129
        $this->randomByCat = 0;
130
        $this->text_when_finished = '';
131
        $this->display_category_name = 0;
132
        $this->pass_percentage = 0;
133
        $this->modelType = 1;
134
        $this->questionSelectionType = EX_Q_SELECTION_ORDERED;
135
        $this->endButton = 0;
136
        $this->scoreTypeModel = 0;
137
        $this->globalCategoryId = null;
138
        $this->notifications = [];
139
        $this->exerciseCategoryId = null;
140
        $this->pageResultConfiguration;
141
        $this->hideQuestionNumber = 0;
142
        $this->preventBackwards = 0;
143
        $this->hideComment = false;
144
        $this->hideNoAnswer = false;
145
        $this->hideExpectedAnswer = false;
146
        $this->disableHideCorrectAnsweredQuestions = false;
147
148
        if (!empty($courseId)) {
149
            $courseInfo = api_get_course_info_by_id($courseId);
150
        } else {
151
            $courseInfo = api_get_course_info();
152
        }
153
        $this->course_id = $courseInfo['real_id'];
154
        $this->course = $courseInfo;
155
        $this->sessionId = api_get_session_id();
156
157
        // ALTER TABLE c_quiz_question ADD COLUMN feedback text;
158
        $this->questionFeedbackEnabled = api_get_configuration_value('allow_quiz_question_feedback');
159
        $this->showPreviousButton = true;
160
    }
161
162
    /**
163
     * Reads exercise information from the data base.
164
     *
165
     * @author Olivier Brouckaert
166
     *
167
     * @param int  $id                - exercise Id
168
     * @param bool $parseQuestionList
169
     *
170
     * @return bool - true if exercise exists, otherwise false
171
     */
172
    public function read($id, $parseQuestionList = true)
173
    {
174
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
175
176
        $id = (int) $id;
177
        if (empty($this->course_id)) {
178
            return false;
179
        }
180
181
        $sql = "SELECT * FROM $table
182
                WHERE c_id = ".$this->course_id." AND id = ".$id;
183
        $result = Database::query($sql);
184
185
        // if the exercise has been found
186
        if ($object = Database::fetch_object($result)) {
187
            $this->iId = $object->iid;
188
            $this->id = $id;
189
            $this->exercise = $object->title;
190
            $this->name = $object->title;
191
            $this->title = $object->title;
192
            $this->description = $object->description;
193
            $this->sound = $object->sound;
194
            $this->type = $object->type;
195
            if (empty($this->type)) {
196
                $this->type = ONE_PER_PAGE;
197
            }
198
            $this->random = $object->random;
199
            $this->random_answers = $object->random_answers;
200
            $this->active = $object->active;
201
            $this->results_disabled = $object->results_disabled;
202
            $this->attempts = $object->max_attempt;
203
            $this->feedback_type = $object->feedback_type;
204
            $this->sessionId = $object->session_id;
205
            $this->propagate_neg = $object->propagate_neg;
206
            $this->saveCorrectAnswers = $object->save_correct_answers;
207
            $this->randomByCat = $object->random_by_category;
208
            $this->text_when_finished = $object->text_when_finished;
209
            $this->display_category_name = $object->display_category_name;
210
            $this->pass_percentage = $object->pass_percentage;
211
            $this->is_gradebook_locked = api_resource_is_locked_by_gradebook($id, LINK_EXERCISE);
212
            $this->review_answers = (isset($object->review_answers) && $object->review_answers == 1) ? true : false;
213
            $this->globalCategoryId = isset($object->global_category_id) ? $object->global_category_id : null;
214
            $this->questionSelectionType = isset($object->question_selection_type) ? (int) $object->question_selection_type : null;
215
            $this->hideQuestionTitle = isset($object->hide_question_title) ? (int) $object->hide_question_title : 0;
216
            $this->autolaunch = isset($object->autolaunch) ? (int) $object->autolaunch : 0;
217
            $this->exerciseCategoryId = isset($object->exercise_category_id) ? (int) $object->exercise_category_id : null;
218
            $this->preventBackwards = isset($object->prevent_backwards) ? (int) $object->prevent_backwards : 0;
219
            $this->exercise_was_added_in_lp = false;
220
            $this->lpList = [];
221
            $this->notifications = [];
222
            if (!empty($object->notifications)) {
223
                $this->notifications = explode(',', $object->notifications);
224
            }
225
226
            if (!empty($object->page_result_configuration)) {
227
                $this->pageResultConfiguration = $object->page_result_configuration;
228
            }
229
230
            if (isset($object->hide_question_number)) {
231
                $this->hideQuestionNumber = $object->hide_question_number == 1;
232
            }
233
234
            if (isset($object->show_previous_button)) {
235
                $this->showPreviousButton = $object->show_previous_button == 1;
236
            }
237
238
            $list = self::getLpListFromExercise($id, $this->course_id);
239
            if (!empty($list)) {
240
                $this->exercise_was_added_in_lp = true;
241
                $this->lpList = $list;
242
            }
243
244
            $this->force_edit_exercise_in_lp = api_get_configuration_value('force_edit_exercise_in_lp');
245
            $this->edit_exercise_in_lp = true;
246
            if ($this->exercise_was_added_in_lp) {
247
                $this->edit_exercise_in_lp = $this->force_edit_exercise_in_lp == true;
248
            }
249
250
            if (!empty($object->end_time)) {
251
                $this->end_time = $object->end_time;
252
            }
253
            if (!empty($object->start_time)) {
254
                $this->start_time = $object->start_time;
255
            }
256
257
            // Control time
258
            $this->expired_time = $object->expired_time;
259
260
            // Checking if question_order is correctly set
261
            if ($parseQuestionList) {
262
                $this->setQuestionList(true);
263
            }
264
265
            //overload questions list with recorded questions list
266
            //load questions only for exercises of type 'one question per page'
267
            //this is needed only is there is no questions
268
269
            // @todo not sure were in the code this is used somebody mess with the exercise tool
270
            // @todo don't know who add that config and why $_configuration['live_exercise_tracking']
271
            /*global $_configuration, $questionList;
272
            if ($this->type == ONE_PER_PAGE && $_SERVER['REQUEST_METHOD'] != 'POST'
273
                && defined('QUESTION_LIST_ALREADY_LOGGED') &&
274
                isset($_configuration['live_exercise_tracking']) && $_configuration['live_exercise_tracking']
275
            ) {
276
                $this->questionList = $questionList;
277
            }*/
278
            return true;
279
        }
280
281
        return false;
282
    }
283
284
    /**
285
     * @return string
286
     */
287
    public function getCutTitle()
288
    {
289
        $title = $this->getUnformattedTitle();
290
291
        return cut($title, EXERCISE_MAX_NAME_SIZE);
292
    }
293
294
    /**
295
     * returns the exercise ID.
296
     *
297
     * @author Olivier Brouckaert
298
     *
299
     * @return int - exercise ID
300
     */
301
    public function selectId()
302
    {
303
        return $this->id;
304
    }
305
306
    /**
307
     * returns the exercise title.
308
     *
309
     * @author Olivier Brouckaert
310
     *
311
     * @param bool $unformattedText Optional. Get the title without HTML tags
312
     *
313
     * @return string - exercise title
314
     */
315
    public function selectTitle($unformattedText = false)
316
    {
317
        if ($unformattedText) {
318
            return $this->getUnformattedTitle();
319
        }
320
321
        return $this->exercise;
322
    }
323
324
    /**
325
     * returns the number of attempts setted.
326
     *
327
     * @return int - exercise attempts
328
     */
329
    public function selectAttempts()
330
    {
331
        return $this->attempts;
332
    }
333
334
    /**
335
     * Returns the number of FeedbackType
336
     *  0: Feedback , 1: DirectFeedback, 2: NoFeedback.
337
     *
338
     * @return int - exercise attempts
339
     */
340
    public function getFeedbackType()
341
    {
342
        return (int) $this->feedback_type;
343
    }
344
345
    /**
346
     * returns the time limit.
347
     *
348
     * @return int
349
     */
350
    public function selectTimeLimit()
351
    {
352
        return $this->timeLimit;
353
    }
354
355
    /**
356
     * returns the exercise description.
357
     *
358
     * @author Olivier Brouckaert
359
     *
360
     * @return string - exercise description
361
     */
362
    public function selectDescription()
363
    {
364
        return $this->description;
365
    }
366
367
    /**
368
     * returns the exercise sound file.
369
     *
370
     * @author Olivier Brouckaert
371
     *
372
     * @return string - exercise description
373
     */
374
    public function selectSound()
375
    {
376
        return $this->sound;
377
    }
378
379
    /**
380
     * returns the exercise type.
381
     *
382
     * @author Olivier Brouckaert
383
     *
384
     * @return int - exercise type
385
     */
386
    public function selectType()
387
    {
388
        return $this->type;
389
    }
390
391
    /**
392
     * @return int
393
     */
394
    public function getModelType()
395
    {
396
        return $this->modelType;
397
    }
398
399
    /**
400
     * @return int
401
     */
402
    public function selectEndButton()
403
    {
404
        return $this->endButton;
405
    }
406
407
    /**
408
     * @author hubert borderiou 30-11-11
409
     *
410
     * @return int : do we display the question category name for students
411
     */
412
    public function selectDisplayCategoryName()
413
    {
414
        return $this->display_category_name;
415
    }
416
417
    /**
418
     * @return int
419
     */
420
    public function selectPassPercentage()
421
    {
422
        return $this->pass_percentage;
423
    }
424
425
    /**
426
     * Modify object to update the switch display_category_name.
427
     *
428
     * @author hubert borderiou 30-11-11
429
     *
430
     * @param int $value is an integer 0 or 1
431
     */
432
    public function updateDisplayCategoryName($value)
433
    {
434
        $this->display_category_name = $value;
435
    }
436
437
    /**
438
     * @author hubert borderiou 28-11-11
439
     *
440
     * @return string html text : the text to display ay the end of the test
441
     */
442
    public function getTextWhenFinished()
443
    {
444
        return $this->text_when_finished;
445
    }
446
447
    /**
448
     * @param string $text
449
     *
450
     * @author hubert borderiou 28-11-11
451
     */
452
    public function updateTextWhenFinished($text)
453
    {
454
        $this->text_when_finished = $text;
455
    }
456
457
    /**
458
     * return 1 or 2 if randomByCat.
459
     *
460
     * @author hubert borderiou
461
     *
462
     * @return int - quiz random by category
463
     */
464
    public function getRandomByCategory()
465
    {
466
        return $this->randomByCat;
467
    }
468
469
    /**
470
     * return 0 if no random by cat
471
     * return 1 if random by cat, categories shuffled
472
     * return 2 if random by cat, categories sorted by alphabetic order.
473
     *
474
     * @author hubert borderiou
475
     *
476
     * @return int - quiz random by category
477
     */
478
    public function isRandomByCat()
479
    {
480
        $res = EXERCISE_CATEGORY_RANDOM_DISABLED;
481
        if ($this->randomByCat == EXERCISE_CATEGORY_RANDOM_SHUFFLED) {
482
            $res = EXERCISE_CATEGORY_RANDOM_SHUFFLED;
483
        } elseif ($this->randomByCat == EXERCISE_CATEGORY_RANDOM_ORDERED) {
484
            $res = EXERCISE_CATEGORY_RANDOM_ORDERED;
485
        }
486
487
        return $res;
488
    }
489
490
    /**
491
     * return nothing
492
     * update randomByCat value for object.
493
     *
494
     * @param int $random
495
     *
496
     * @author hubert borderiou
497
     */
498
    public function updateRandomByCat($random)
499
    {
500
        $this->randomByCat = EXERCISE_CATEGORY_RANDOM_DISABLED;
501
        if (in_array(
502
            $random,
503
            [
504
                EXERCISE_CATEGORY_RANDOM_SHUFFLED,
505
                EXERCISE_CATEGORY_RANDOM_ORDERED,
506
                EXERCISE_CATEGORY_RANDOM_DISABLED,
507
            ]
508
        )) {
509
            $this->randomByCat = $random;
510
        }
511
    }
512
513
    /**
514
     * Tells if questions are selected randomly, and if so returns the draws.
515
     *
516
     * @author Carlos Vargas
517
     *
518
     * @return int - results disabled exercise
519
     */
520
    public function selectResultsDisabled()
521
    {
522
        return $this->results_disabled;
523
    }
524
525
    /**
526
     * tells if questions are selected randomly, and if so returns the draws.
527
     *
528
     * @author Olivier Brouckaert
529
     *
530
     * @return bool
531
     */
532
    public function isRandom()
533
    {
534
        $isRandom = false;
535
        // "-1" means all questions will be random
536
        if ($this->random > 0 || $this->random == -1) {
537
            $isRandom = true;
538
        }
539
540
        return $isRandom;
541
    }
542
543
    /**
544
     * returns random answers status.
545
     *
546
     * @author Juan Carlos Rana
547
     */
548
    public function getRandomAnswers()
549
    {
550
        return $this->random_answers;
551
    }
552
553
    /**
554
     * Same as isRandom() but has a name applied to values different than 0 or 1.
555
     *
556
     * @return int
557
     */
558
    public function getShuffle()
559
    {
560
        return $this->random;
561
    }
562
563
    /**
564
     * returns the exercise status (1 = enabled ; 0 = disabled).
565
     *
566
     * @author Olivier Brouckaert
567
     *
568
     * @return int - 1 if enabled, otherwise 0
569
     */
570
    public function selectStatus()
571
    {
572
        return $this->active;
573
    }
574
575
    /**
576
     * If false the question list will be managed as always if true
577
     * the question will be filtered
578
     * depending of the exercise settings (table c_quiz_rel_category).
579
     *
580
     * @param bool $status active or inactive grouping
581
     */
582
    public function setCategoriesGrouping($status)
583
    {
584
        $this->categories_grouping = (bool) $status;
585
    }
586
587
    /**
588
     * @return int
589
     */
590
    public function getHideQuestionTitle()
591
    {
592
        return $this->hideQuestionTitle;
593
    }
594
595
    /**
596
     * @param $value
597
     */
598
    public function setHideQuestionTitle($value)
599
    {
600
        $this->hideQuestionTitle = (int) $value;
601
    }
602
603
    /**
604
     * @return int
605
     */
606
    public function getScoreTypeModel()
607
    {
608
        return $this->scoreTypeModel;
609
    }
610
611
    /**
612
     * @param int $value
613
     */
614
    public function setScoreTypeModel($value)
615
    {
616
        $this->scoreTypeModel = (int) $value;
617
    }
618
619
    /**
620
     * @return int
621
     */
622
    public function getGlobalCategoryId()
623
    {
624
        return $this->globalCategoryId;
625
    }
626
627
    /**
628
     * @param int $value
629
     */
630
    public function setGlobalCategoryId($value)
631
    {
632
        if (is_array($value) && isset($value[0])) {
633
            $value = $value[0];
634
        }
635
        $this->globalCategoryId = (int) $value;
636
    }
637
638
    /**
639
     * @param int    $start
640
     * @param int    $limit
641
     * @param int    $sidx
642
     * @param string $sord
643
     * @param array  $whereCondition
644
     * @param array  $extraFields
645
     *
646
     * @return array
647
     */
648
    public function getQuestionListPagination(
649
        $start,
650
        $limit,
651
        $sidx,
652
        $sord,
653
        $whereCondition = [],
654
        $extraFields = []
655
    ) {
656
        if (!empty($this->id)) {
657
            $category_list = TestCategory::getListOfCategoriesNameForTest(
658
                $this->id,
659
                false
660
            );
661
            $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
662
            $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
663
664
            $sql = "SELECT q.iid
665
                    FROM $TBL_EXERCICE_QUESTION e
666
                    INNER JOIN $TBL_QUESTIONS  q
667
                    ON (e.question_id = q.id AND e.c_id = ".$this->course_id." )
668
					WHERE e.exercice_id	= '".$this->id."' ";
669
670
            $orderCondition = ' ORDER BY question_order ';
671
672
            if (!empty($sidx) && !empty($sord)) {
673
                if ('question' === $sidx) {
674
                    if (in_array(strtolower($sord), ['desc', 'asc'])) {
675
                        $orderCondition = " ORDER BY `q.$sidx` $sord";
676
                    }
677
                }
678
            }
679
680
            $sql .= $orderCondition;
681
            $limitCondition = null;
682
            if (isset($start) && isset($limit)) {
683
                $start = (int) $start;
684
                $limit = (int) $limit;
685
                $limitCondition = " LIMIT $start, $limit";
686
            }
687
            $sql .= $limitCondition;
688
            $result = Database::query($sql);
689
            $questions = [];
690
            if (Database::num_rows($result)) {
691
                if (!empty($extraFields)) {
692
                    $extraFieldValue = new ExtraFieldValue('question');
693
                }
694
                while ($question = Database::fetch_array($result, 'ASSOC')) {
695
                    /** @var Question $objQuestionTmp */
696
                    $objQuestionTmp = Question::read($question['iid']);
697
                    $category_labels = TestCategory::return_category_labels(
698
                        $objQuestionTmp->category_list,
699
                        $category_list
700
                    );
701
702
                    if (empty($category_labels)) {
703
                        $category_labels = '-';
704
                    }
705
706
                    // Question type
707
                    $typeImg = $objQuestionTmp->getTypePicture();
708
                    $typeExpl = $objQuestionTmp->getExplanation();
709
710
                    $question_media = null;
711
                    if (!empty($objQuestionTmp->parent_id)) {
712
                        $objQuestionMedia = Question::read($objQuestionTmp->parent_id);
713
                        $question_media = Question::getMediaLabel($objQuestionMedia->question);
714
                    }
715
716
                    $questionType = Display::tag(
717
                        'div',
718
                        Display::return_icon($typeImg, $typeExpl, [], ICON_SIZE_MEDIUM).$question_media
719
                    );
720
721
                    $question = [
722
                        'id' => $question['iid'],
723
                        'question' => $objQuestionTmp->selectTitle(),
724
                        'type' => $questionType,
725
                        'category' => Display::tag(
726
                            'div',
727
                            '<a href="#" style="padding:0px; margin:0px;">'.$category_labels.'</a>'
728
                        ),
729
                        'score' => $objQuestionTmp->selectWeighting(),
730
                        'level' => $objQuestionTmp->level,
731
                    ];
732
733
                    if (!empty($extraFields)) {
734
                        foreach ($extraFields as $extraField) {
735
                            $value = $extraFieldValue->get_values_by_handler_and_field_id(
736
                                $question['id'],
737
                                $extraField['id']
738
                            );
739
                            $stringValue = null;
740
                            if ($value) {
741
                                $stringValue = $value['field_value'];
742
                            }
743
                            $question[$extraField['field_variable']] = $stringValue;
744
                        }
745
                    }
746
                    $questions[] = $question;
747
                }
748
            }
749
750
            return $questions;
751
        }
752
    }
753
754
    /**
755
     * Get question count per exercise from DB (any special treatment).
756
     *
757
     * @return int
758
     */
759
    public function getQuestionCount()
760
    {
761
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
762
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
763
        $sql = "SELECT count(q.id) as count
764
                FROM $TBL_EXERCICE_QUESTION e
765
                INNER JOIN $TBL_QUESTIONS q
766
                ON (e.question_id = q.id AND e.c_id = q.c_id)
767
                WHERE
768
                    e.c_id = {$this->course_id} AND
769
                    e.exercice_id = ".$this->id;
770
        $result = Database::query($sql);
771
772
        $count = 0;
773
        if (Database::num_rows($result)) {
774
            $row = Database::fetch_array($result);
775
            $count = (int) $row['count'];
776
        }
777
778
        return $count;
779
    }
780
781
    /**
782
     * @return array
783
     */
784
    public function getQuestionOrderedListByName()
785
    {
786
        if (empty($this->course_id) || empty($this->id)) {
787
            return [];
788
        }
789
790
        $exerciseQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
791
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
792
793
        // Getting question list from the order (question list drag n drop interface ).
794
        $sql = "SELECT e.question_id
795
                FROM $exerciseQuestionTable e
796
                INNER JOIN $questionTable q
797
                ON (e.question_id= q.id AND e.c_id = q.c_id)
798
                WHERE
799
                    e.c_id = {$this->course_id} AND
800
                    e.exercice_id = '".$this->id."'
801
                ORDER BY q.question";
802
        $result = Database::query($sql);
803
        $list = [];
804
        if (Database::num_rows($result)) {
805
            $list = Database::store_result($result, 'ASSOC');
806
        }
807
808
        return $list;
809
    }
810
811
    /**
812
     * Selecting question list depending in the exercise-category
813
     * relationship (category table in exercise settings).
814
     *
815
     * @param array $question_list
816
     * @param int   $questionSelectionType
817
     *
818
     * @return array
819
     */
820
    public function getQuestionListWithCategoryListFilteredByCategorySettings(
821
        $question_list,
822
        $questionSelectionType
823
    ) {
824
        $result = [
825
            'question_list' => [],
826
            'category_with_questions_list' => [],
827
        ];
828
829
        // Order/random categories
830
        $cat = new TestCategory();
831
        // Setting category order.
832
        switch ($questionSelectionType) {
833
            case EX_Q_SELECTION_ORDERED: // 1
834
            case EX_Q_SELECTION_RANDOM:  // 2
835
                // This options are not allowed here.
836
                break;
837
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED: // 3
838
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
839
                    $this,
840
                    $this->course['real_id'],
841
                    'title ASC',
842
                    false,
843
                    true
844
                );
845
846
                $questions_by_category = TestCategory::getQuestionsByCat(
847
                    $this->id,
848
                    $question_list,
849
                    $categoriesAddedInExercise
850
                );
851
852
                $question_list = $this->pickQuestionsPerCategory(
853
                    $categoriesAddedInExercise,
854
                    $question_list,
855
                    $questions_by_category,
856
                    true,
857
                    false
858
                );
859
                break;
860
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED: // 4
861
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7
862
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
863
                    $this,
864
                    $this->course['real_id'],
865
                    null,
866
                    true,
867
                    true
868
                );
869
                $questions_by_category = TestCategory::getQuestionsByCat(
870
                    $this->id,
871
                    $question_list,
872
                    $categoriesAddedInExercise
873
                );
874
                $question_list = $this->pickQuestionsPerCategory(
875
                    $categoriesAddedInExercise,
876
                    $question_list,
877
                    $questions_by_category,
878
                    true,
879
                    false
880
                );
881
                break;
882
            case EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM: // 5
883
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
884
                    $this,
885
                    $this->course['real_id'],
886
                    'title ASC',
887
                    false,
888
                    true
889
                );
890
                $questions_by_category = TestCategory::getQuestionsByCat(
891
                    $this->id,
892
                    $question_list,
893
                    $categoriesAddedInExercise
894
                );
895
896
                $questionsByCategoryMandatory = [];
897
                if (EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM == $this->getQuestionSelectionType() &&
898
                    api_get_configuration_value('allow_mandatory_question_in_category')
899
                ) {
900
                    $questionsByCategoryMandatory = TestCategory::getQuestionsByCat(
901
                        $this->id,
902
                        $question_list,
903
                        $categoriesAddedInExercise,
904
                        true
905
                    );
906
                }
907
908
                $question_list = $this->pickQuestionsPerCategory(
909
                    $categoriesAddedInExercise,
910
                    $question_list,
911
                    $questions_by_category,
912
                    true,
913
                    true,
914
                    $questionsByCategoryMandatory
915
                );
916
                break;
917
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM: // 6
918
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED:
919
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
920
                    $this,
921
                    $this->course['real_id'],
922
                    null,
923
                    true,
924
                    true
925
                );
926
927
                $questions_by_category = TestCategory::getQuestionsByCat(
928
                    $this->id,
929
                    $question_list,
930
                    $categoriesAddedInExercise
931
                );
932
933
                $question_list = $this->pickQuestionsPerCategory(
934
                    $categoriesAddedInExercise,
935
                    $question_list,
936
                    $questions_by_category,
937
                    true,
938
                    true
939
                );
940
                break;
941
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED: // 7
942
                break;
943
            case EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED: // 8
944
                break;
945
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED: // 9
946
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
947
                    $this,
948
                    $this->course['real_id'],
949
                    'root ASC, lft ASC',
950
                    false,
951
                    true
952
                );
953
                $questions_by_category = TestCategory::getQuestionsByCat(
954
                    $this->id,
955
                    $question_list,
956
                    $categoriesAddedInExercise
957
                );
958
                $question_list = $this->pickQuestionsPerCategory(
959
                    $categoriesAddedInExercise,
960
                    $question_list,
961
                    $questions_by_category,
962
                    true,
963
                    false
964
                );
965
                break;
966
            case EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM: // 10
967
                $categoriesAddedInExercise = $cat->getCategoryExerciseTree(
968
                    $this,
969
                    $this->course['real_id'],
970
                    'root, lft ASC',
971
                    false,
972
                    true
973
                );
974
                $questions_by_category = TestCategory::getQuestionsByCat(
975
                    $this->id,
976
                    $question_list,
977
                    $categoriesAddedInExercise
978
                );
979
                $question_list = $this->pickQuestionsPerCategory(
980
                    $categoriesAddedInExercise,
981
                    $question_list,
982
                    $questions_by_category,
983
                    true,
984
                    true
985
                );
986
                break;
987
        }
988
989
        $result['question_list'] = isset($question_list) ? $question_list : [];
990
        $result['category_with_questions_list'] = isset($questions_by_category) ? $questions_by_category : [];
991
        $parentsLoaded = [];
992
        // Adding category info in the category list with question list:
993
        if (!empty($questions_by_category)) {
994
            $newCategoryList = [];
995
            $em = Database::getManager();
996
            $repo = $em->getRepository('ChamiloCourseBundle:CQuizCategory');
997
998
            foreach ($questions_by_category as $categoryId => $questionList) {
999
                $cat = new TestCategory();
1000
                $cat = $cat->getCategory($categoryId);
1001
                if ($cat) {
1002
                    $cat = (array) $cat;
1003
                    $cat['iid'] = $cat['id'];
1004
                }
1005
1006
                $categoryParentInfo = null;
1007
                // Parent is not set no loop here
1008
                if (isset($cat['parent_id']) && !empty($cat['parent_id'])) {
1009
                    /** @var \Chamilo\CourseBundle\Entity\CQuizCategory $categoryEntity */
1010
                    if (!isset($parentsLoaded[$cat['parent_id']])) {
1011
                        $categoryEntity = $em->find('ChamiloCourseBundle:CQuizCategory', $cat['parent_id']);
1012
                        $parentsLoaded[$cat['parent_id']] = $categoryEntity;
1013
                    } else {
1014
                        $categoryEntity = $parentsLoaded[$cat['parent_id']];
1015
                    }
1016
                    $path = $repo->getPath($categoryEntity);
1017
1018
                    $index = 0;
1019
                    if ($this->categoryMinusOne) {
1020
                        //$index = 1;
1021
                    }
1022
1023
                    /** @var \Chamilo\CourseBundle\Entity\CQuizCategory $categoryParent */
1024
                    foreach ($path as $categoryParent) {
1025
                        $visibility = $categoryParent->getVisibility();
1026
                        if (0 == $visibility) {
1027
                            $categoryParentId = $categoryId;
1028
                            $categoryTitle = $cat['title'];
1029
                            if (count($path) > 1) {
1030
                                continue;
1031
                            }
1032
                        } else {
1033
                            $categoryParentId = $categoryParent->getIid();
1034
                            $categoryTitle = $categoryParent->getTitle();
1035
                        }
1036
1037
                        $categoryParentInfo['id'] = $categoryParentId;
1038
                        $categoryParentInfo['iid'] = $categoryParentId;
1039
                        $categoryParentInfo['parent_path'] = null;
1040
                        $categoryParentInfo['title'] = $categoryTitle;
1041
                        $categoryParentInfo['name'] = $categoryTitle;
1042
                        $categoryParentInfo['parent_id'] = null;
1043
                        break;
1044
                    }
1045
                }
1046
                $cat['parent_info'] = $categoryParentInfo;
1047
                $newCategoryList[$categoryId] = [
1048
                    'category' => $cat,
1049
                    'question_list' => $questionList,
1050
                ];
1051
            }
1052
1053
            $result['category_with_questions_list'] = $newCategoryList;
1054
        }
1055
1056
        return $result;
1057
    }
1058
1059
    /**
1060
     * returns the array with the question ID list.
1061
     *
1062
     * @param bool $fromDatabase Whether the results should be fetched in the database or just from memory
1063
     * @param bool $adminView    Whether we should return all questions (admin view) or
1064
     *                           just a list limited by the max number of random questions
1065
     *
1066
     * @author Olivier Brouckaert
1067
     *
1068
     * @return array - question ID list
1069
     */
1070
    public function selectQuestionList($fromDatabase = false, $adminView = false)
1071
    {
1072
        if ($fromDatabase && !empty($this->id)) {
1073
            $nbQuestions = $this->getQuestionCount();
1074
            $questionSelectionType = $this->getQuestionSelectionType();
1075
1076
            switch ($questionSelectionType) {
1077
                case EX_Q_SELECTION_ORDERED:
1078
                    $questionList = $this->getQuestionOrderedList($adminView);
1079
                    break;
1080
                case EX_Q_SELECTION_RANDOM:
1081
                    // Not a random exercise, or if there are not at least 2 questions
1082
                    if ($this->random == 0 || $nbQuestions < 2) {
1083
                        $questionList = $this->getQuestionOrderedList($adminView);
1084
                    } else {
1085
                        $questionList = $this->getRandomList($adminView);
1086
                    }
1087
                    break;
1088
                default:
1089
                    $questionList = $this->getQuestionOrderedList($adminView);
1090
                    $result = $this->getQuestionListWithCategoryListFilteredByCategorySettings(
1091
                        $questionList,
1092
                        $questionSelectionType
1093
                    );
1094
                    $this->categoryWithQuestionList = $result['category_with_questions_list'];
1095
                    $questionList = $result['question_list'];
1096
                    break;
1097
            }
1098
1099
            return $questionList;
1100
        }
1101
1102
        return $this->questionList;
1103
    }
1104
1105
    /**
1106
     * returns the number of questions in this exercise.
1107
     *
1108
     * @author Olivier Brouckaert
1109
     *
1110
     * @return int - number of questions
1111
     */
1112
    public function selectNbrQuestions()
1113
    {
1114
        return count($this->questionList);
1115
    }
1116
1117
    /**
1118
     * @return int
1119
     */
1120
    public function selectPropagateNeg()
1121
    {
1122
        return $this->propagate_neg;
1123
    }
1124
1125
    /**
1126
     * @return int
1127
     */
1128
    public function getSaveCorrectAnswers()
1129
    {
1130
        return $this->saveCorrectAnswers;
1131
    }
1132
1133
    /**
1134
     * Selects questions randomly in the question list.
1135
     *
1136
     * @author Olivier Brouckaert
1137
     * @author Hubert Borderiou 15 nov 2011
1138
     *
1139
     * @param bool $adminView Whether we should return all
1140
     *                        questions (admin view) or just a list limited by the max number of random questions
1141
     *
1142
     * @return array - if the exercise is not set to take questions randomly, returns the question list
1143
     *               without randomizing, otherwise, returns the list with questions selected randomly
1144
     */
1145
    public function getRandomList($adminView = false)
1146
    {
1147
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1148
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
1149
        $random = isset($this->random) && !empty($this->random) ? $this->random : 0;
1150
1151
        // Random with limit
1152
        $randomLimit = " ORDER BY RAND() LIMIT $random";
1153
1154
        // Random with no limit
1155
        if (-1 == $random) {
1156
            $randomLimit = ' ORDER BY RAND() ';
1157
        }
1158
1159
        // Admin see the list in default order
1160
        if (true === $adminView) {
1161
            // If viewing it as admin for edition, don't show it randomly, use title + id
1162
            $randomLimit = 'ORDER BY e.question_order';
1163
        }
1164
1165
        $sql = "SELECT e.question_id
1166
                FROM $quizRelQuestion e
1167
                INNER JOIN $question q
1168
                ON (e.question_id= q.id AND e.c_id = q.c_id)
1169
                WHERE
1170
                    e.c_id = {$this->course_id} AND
1171
                    e.exercice_id = '".Database::escape_string($this->id)."'
1172
                    $randomLimit ";
1173
        $result = Database::query($sql);
1174
        $questionList = [];
1175
        while ($row = Database::fetch_object($result)) {
1176
            $questionList[] = $row->question_id;
1177
        }
1178
1179
        return $questionList;
1180
    }
1181
1182
    /**
1183
     * returns 'true' if the question ID is in the question list.
1184
     *
1185
     * @author Olivier Brouckaert
1186
     *
1187
     * @param int $questionId - question ID
1188
     *
1189
     * @return bool - true if in the list, otherwise false
1190
     */
1191
    public function isInList($questionId)
1192
    {
1193
        $inList = false;
1194
        if (is_array($this->questionList)) {
1195
            $inList = in_array($questionId, $this->questionList);
1196
        }
1197
1198
        return $inList;
1199
    }
1200
1201
    /**
1202
     * If current exercise has a question.
1203
     *
1204
     * @param int $questionId
1205
     *
1206
     * @return int
1207
     */
1208
    public function hasQuestion($questionId)
1209
    {
1210
        $questionId = (int) $questionId;
1211
1212
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1213
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1214
        $sql = "SELECT q.id
1215
                FROM $TBL_EXERCICE_QUESTION e
1216
                INNER JOIN $TBL_QUESTIONS q
1217
                ON (e.question_id = q.id AND e.c_id = q.c_id)
1218
                WHERE
1219
                    q.id = $questionId AND
1220
                    e.c_id = {$this->course_id} AND
1221
                    e.exercice_id = ".$this->id;
1222
1223
        $result = Database::query($sql);
1224
1225
        return Database::num_rows($result) > 0;
1226
    }
1227
1228
    public function hasQuestionWithType($type)
1229
    {
1230
        $type = (int) $type;
1231
1232
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1233
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
1234
        $sql = "SELECT q.id
1235
                FROM $table e
1236
                INNER JOIN $tableQuestion q
1237
                ON (e.question_id = q.id AND e.c_id = q.c_id)
1238
                WHERE
1239
                    q.type = $type AND
1240
                    e.c_id = {$this->course_id} AND
1241
                    e.exercice_id = ".$this->id;
1242
1243
        $result = Database::query($sql);
1244
1245
        return Database::num_rows($result) > 0;
1246
    }
1247
1248
    public function hasQuestionWithTypeNotInList(array $questionTypeList)
1249
    {
1250
        if (empty($questionTypeList)) {
1251
            return false;
1252
        }
1253
1254
        $questionTypeToString = implode("','", array_map('intval', $questionTypeList));
1255
1256
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1257
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
1258
        $sql = "SELECT q.id
1259
                FROM $table e
1260
                INNER JOIN $tableQuestion q
1261
                ON (e.question_id = q.id AND e.c_id = q.c_id)
1262
                WHERE
1263
                    q.type NOT IN ('$questionTypeToString')  AND
1264
                    e.c_id = {$this->course_id} AND
1265
                    e.exercice_id = ".$this->id;
1266
1267
        $result = Database::query($sql);
1268
1269
        return Database::num_rows($result) > 0;
1270
    }
1271
1272
    /**
1273
     * changes the exercise title.
1274
     *
1275
     * @author Olivier Brouckaert
1276
     *
1277
     * @param string $title - exercise title
1278
     */
1279
    public function updateTitle($title)
1280
    {
1281
        $this->title = $this->exercise = $title;
1282
    }
1283
1284
    /**
1285
     * changes the exercise max attempts.
1286
     *
1287
     * @param int $attempts - exercise max attempts
1288
     */
1289
    public function updateAttempts($attempts)
1290
    {
1291
        $this->attempts = $attempts;
1292
    }
1293
1294
    /**
1295
     * changes the exercise feedback type.
1296
     *
1297
     * @param int $feedback_type
1298
     */
1299
    public function updateFeedbackType($feedback_type)
1300
    {
1301
        $this->feedback_type = $feedback_type;
1302
    }
1303
1304
    /**
1305
     * changes the exercise description.
1306
     *
1307
     * @author Olivier Brouckaert
1308
     *
1309
     * @param string $description - exercise description
1310
     */
1311
    public function updateDescription($description)
1312
    {
1313
        $this->description = $description;
1314
    }
1315
1316
    /**
1317
     * changes the exercise expired_time.
1318
     *
1319
     * @author Isaac flores
1320
     *
1321
     * @param int $expired_time The expired time of the quiz
1322
     */
1323
    public function updateExpiredTime($expired_time)
1324
    {
1325
        $this->expired_time = $expired_time;
1326
    }
1327
1328
    /**
1329
     * @param $value
1330
     */
1331
    public function updatePropagateNegative($value)
1332
    {
1333
        $this->propagate_neg = $value;
1334
    }
1335
1336
    /**
1337
     * @param int $value
1338
     */
1339
    public function updateSaveCorrectAnswers($value)
1340
    {
1341
        $this->saveCorrectAnswers = (int) $value;
1342
    }
1343
1344
    /**
1345
     * @param $value
1346
     */
1347
    public function updateReviewAnswers($value)
1348
    {
1349
        $this->review_answers = isset($value) && $value ? true : false;
1350
    }
1351
1352
    /**
1353
     * @param $value
1354
     */
1355
    public function updatePassPercentage($value)
1356
    {
1357
        $this->pass_percentage = $value;
1358
    }
1359
1360
    /**
1361
     * @param string $text
1362
     */
1363
    public function updateEmailNotificationTemplate($text)
1364
    {
1365
        $this->emailNotificationTemplate = $text;
1366
    }
1367
1368
    /**
1369
     * @param string $text
1370
     */
1371
    public function setEmailNotificationTemplateToUser($text)
1372
    {
1373
        $this->emailNotificationTemplateToUser = $text;
1374
    }
1375
1376
    /**
1377
     * @param string $value
1378
     */
1379
    public function setNotifyUserByEmail($value)
1380
    {
1381
        $this->notifyUserByEmail = $value;
1382
    }
1383
1384
    /**
1385
     * @param int $value
1386
     */
1387
    public function updateEndButton($value)
1388
    {
1389
        $this->endButton = (int) $value;
1390
    }
1391
1392
    /**
1393
     * @param string $value
1394
     */
1395
    public function setOnSuccessMessage($value)
1396
    {
1397
        $this->onSuccessMessage = $value;
1398
    }
1399
1400
    /**
1401
     * @param string $value
1402
     */
1403
    public function setOnFailedMessage($value)
1404
    {
1405
        $this->onFailedMessage = $value;
1406
    }
1407
1408
    /**
1409
     * @param $value
1410
     */
1411
    public function setModelType($value)
1412
    {
1413
        $this->modelType = (int) $value;
1414
    }
1415
1416
    /**
1417
     * @param int $value
1418
     */
1419
    public function setQuestionSelectionType($value)
1420
    {
1421
        $this->questionSelectionType = (int) $value;
1422
    }
1423
1424
    /**
1425
     * @return int
1426
     */
1427
    public function getQuestionSelectionType()
1428
    {
1429
        return (int) $this->questionSelectionType;
1430
    }
1431
1432
    /**
1433
     * @param array $categories
1434
     */
1435
    public function updateCategories($categories)
1436
    {
1437
        if (!empty($categories)) {
1438
            $categories = array_map('intval', $categories);
1439
            $this->categories = $categories;
1440
        }
1441
    }
1442
1443
    /**
1444
     * changes the exercise sound file.
1445
     *
1446
     * @author Olivier Brouckaert
1447
     *
1448
     * @param string $sound  - exercise sound file
1449
     * @param string $delete - ask to delete the file
1450
     */
1451
    public function updateSound($sound, $delete)
1452
    {
1453
        global $audioPath, $documentPath;
1454
        $TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
1455
1456
        if ($sound['size'] && (strstr($sound['type'], 'audio') || strstr($sound['type'], 'video'))) {
1457
            $this->sound = $sound['name'];
1458
1459
            if (@move_uploaded_file($sound['tmp_name'], $audioPath.'/'.$this->sound)) {
1460
                $sql = "SELECT 1 FROM $TBL_DOCUMENT
1461
                        WHERE
1462
                            c_id = ".$this->course_id." AND
1463
                            path = '".str_replace($documentPath, '', $audioPath).'/'.$this->sound."'";
1464
                $result = Database::query($sql);
1465
1466
                if (!Database::num_rows($result)) {
1467
                    $id = add_document(
1468
                        $this->course,
1469
                        str_replace($documentPath, '', $audioPath).'/'.$this->sound,
1470
                        'file',
1471
                        $sound['size'],
1472
                        $sound['name']
1473
                    );
1474
                    api_item_property_update(
1475
                        $this->course,
1476
                        TOOL_DOCUMENT,
1477
                        $id,
1478
                        'DocumentAdded',
1479
                        api_get_user_id()
1480
                    );
1481
                    item_property_update_on_folder(
1482
                        $this->course,
1483
                        str_replace($documentPath, '', $audioPath),
1484
                        api_get_user_id()
1485
                    );
1486
                }
1487
            }
1488
        } elseif ($delete && is_file($audioPath.'/'.$this->sound)) {
1489
            $this->sound = '';
1490
        }
1491
    }
1492
1493
    /**
1494
     * changes the exercise type.
1495
     *
1496
     * @author Olivier Brouckaert
1497
     *
1498
     * @param int $type - exercise type
1499
     */
1500
    public function updateType($type)
1501
    {
1502
        $this->type = $type;
1503
    }
1504
1505
    /**
1506
     * sets to 0 if questions are not selected randomly
1507
     * if questions are selected randomly, sets the draws.
1508
     *
1509
     * @author Olivier Brouckaert
1510
     *
1511
     * @param int $random - 0 if not random, otherwise the draws
1512
     */
1513
    public function setRandom($random)
1514
    {
1515
        $this->random = $random;
1516
    }
1517
1518
    /**
1519
     * sets to 0 if answers are not selected randomly
1520
     * if answers are selected randomly.
1521
     *
1522
     * @author Juan Carlos Rana
1523
     *
1524
     * @param int $random_answers - random answers
1525
     */
1526
    public function updateRandomAnswers($random_answers)
1527
    {
1528
        $this->random_answers = $random_answers;
1529
    }
1530
1531
    /**
1532
     * enables the exercise.
1533
     *
1534
     * @author Olivier Brouckaert
1535
     */
1536
    public function enable()
1537
    {
1538
        $this->active = 1;
1539
    }
1540
1541
    /**
1542
     * disables the exercise.
1543
     *
1544
     * @author Olivier Brouckaert
1545
     */
1546
    public function disable()
1547
    {
1548
        $this->active = 0;
1549
    }
1550
1551
    /**
1552
     * Set disable results.
1553
     */
1554
    public function disable_results()
1555
    {
1556
        $this->results_disabled = true;
1557
    }
1558
1559
    /**
1560
     * Enable results.
1561
     */
1562
    public function enable_results()
1563
    {
1564
        $this->results_disabled = false;
1565
    }
1566
1567
    /**
1568
     * @param int $results_disabled
1569
     */
1570
    public function updateResultsDisabled($results_disabled)
1571
    {
1572
        $this->results_disabled = (int) $results_disabled;
1573
    }
1574
1575
    /**
1576
     * updates the exercise in the data base.
1577
     *
1578
     * @param string $type_e
1579
     *
1580
     * @author Olivier Brouckaert
1581
     */
1582
    public function save($type_e = '')
1583
    {
1584
        $_course = $this->course;
1585
        $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
1586
1587
        $id = $this->id;
1588
        $exercise = $this->exercise;
1589
        $description = $this->description;
1590
        $sound = $this->sound;
1591
        $type = $this->type;
1592
        $attempts = isset($this->attempts) ? $this->attempts : 0;
1593
        $feedback_type = isset($this->feedback_type) ? $this->feedback_type : 0;
1594
        $random = $this->random;
1595
        $random_answers = $this->random_answers;
1596
        $active = $this->active;
1597
        $propagate_neg = (int) $this->propagate_neg;
1598
        $saveCorrectAnswers = isset($this->saveCorrectAnswers) ? (int) $this->saveCorrectAnswers : 0;
1599
        $review_answers = isset($this->review_answers) && $this->review_answers ? 1 : 0;
1600
        $randomByCat = (int) $this->randomByCat;
1601
        $text_when_finished = $this->text_when_finished;
1602
        $display_category_name = (int) $this->display_category_name;
1603
        $pass_percentage = (int) $this->pass_percentage;
1604
        $session_id = $this->sessionId;
1605
1606
        // If direct we do not show results
1607
        $results_disabled = (int) $this->results_disabled;
1608
        if (in_array($feedback_type, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1609
            $results_disabled = 0;
1610
        }
1611
        $expired_time = (int) $this->expired_time;
1612
        $showHideConfiguration = api_get_configuration_value('quiz_hide_question_number');
1613
1614
        // Exercise already exists
1615
        if ($id) {
1616
            // we prepare date in the database using the api_get_utc_datetime() function
1617
            $start_time = null;
1618
            if (!empty($this->start_time)) {
1619
                $start_time = $this->start_time;
1620
            }
1621
1622
            $end_time = null;
1623
            if (!empty($this->end_time)) {
1624
                $end_time = $this->end_time;
1625
            }
1626
1627
            $params = [
1628
                'title' => $exercise,
1629
                'description' => $description,
1630
            ];
1631
1632
            $paramsExtra = [];
1633
            if ($type_e != 'simple') {
1634
                $paramsExtra = [
1635
                    'sound' => $sound,
1636
                    'type' => $type,
1637
                    'random' => $random,
1638
                    'random_answers' => $random_answers,
1639
                    'active' => $active,
1640
                    'feedback_type' => $feedback_type,
1641
                    'start_time' => $start_time,
1642
                    'end_time' => $end_time,
1643
                    'max_attempt' => $attempts,
1644
                    'expired_time' => $expired_time,
1645
                    'propagate_neg' => $propagate_neg,
1646
                    'save_correct_answers' => $saveCorrectAnswers,
1647
                    'review_answers' => $review_answers,
1648
                    'random_by_category' => $randomByCat,
1649
                    'text_when_finished' => $text_when_finished,
1650
                    'display_category_name' => $display_category_name,
1651
                    'pass_percentage' => $pass_percentage,
1652
                    'results_disabled' => $results_disabled,
1653
                    'question_selection_type' => $this->getQuestionSelectionType(),
1654
                    'hide_question_title' => $this->getHideQuestionTitle(),
1655
                ];
1656
1657
                $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
1658
                if ($allow === true) {
1659
                    $paramsExtra['show_previous_button'] = $this->showPreviousButton();
1660
                }
1661
1662
                if (api_get_configuration_value('quiz_prevent_backwards_move')) {
1663
                    $paramsExtra['prevent_backwards'] = $this->getPreventBackwards();
1664
                }
1665
1666
                $allow = api_get_configuration_value('allow_exercise_categories');
1667
                if ($allow === true) {
1668
                    if (!empty($this->getExerciseCategoryId())) {
1669
                        $paramsExtra['exercise_category_id'] = $this->getExerciseCategoryId();
1670
                    }
1671
                }
1672
1673
                $allow = api_get_configuration_value('allow_notification_setting_per_exercise');
1674
                if ($allow === true) {
1675
                    $notifications = $this->getNotifications();
1676
                    $notifications = implode(',', $notifications);
1677
                    $paramsExtra['notifications'] = $notifications;
1678
                }
1679
1680
                $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
1681
                if ($pageConfig && !empty($this->pageResultConfiguration)) {
1682
                    $paramsExtra['page_result_configuration'] = $this->pageResultConfiguration;
1683
                }
1684
            }
1685
1686
            if ($showHideConfiguration) {
1687
                $paramsExtra['hide_question_number'] = $this->hideQuestionNumber;
1688
            }
1689
1690
            $params = array_merge($params, $paramsExtra);
1691
1692
            Database::update(
1693
                $TBL_EXERCISES,
1694
                $params,
1695
                ['c_id = ? AND id = ?' => [$this->course_id, $id]]
1696
            );
1697
1698
            // update into the item_property table
1699
            api_item_property_update(
1700
                $_course,
1701
                TOOL_QUIZ,
1702
                $id,
1703
                'QuizUpdated',
1704
                api_get_user_id()
1705
            );
1706
1707
            if (api_get_setting('search_enabled') === 'true') {
1708
                $this->search_engine_edit();
1709
            }
1710
        } else {
1711
            // Creates a new exercise
1712
            // In this case of new exercise, we don't do the api_get_utc_datetime()
1713
            // for date because, bellow, we call function api_set_default_visibility()
1714
            // In this function, api_set_default_visibility,
1715
            // the Quiz is saved too, with an $id and api_get_utc_datetime() is done.
1716
            // If we do it now, it will be done twice (cf. https://support.chamilo.org/issues/6586)
1717
            $start_time = null;
1718
            if (!empty($this->start_time)) {
1719
                $start_time = $this->start_time;
1720
            }
1721
1722
            $end_time = null;
1723
            if (!empty($this->end_time)) {
1724
                $end_time = $this->end_time;
1725
            }
1726
1727
            $params = [
1728
                'c_id' => $this->course_id,
1729
                'start_time' => $start_time,
1730
                'end_time' => $end_time,
1731
                'title' => $exercise,
1732
                'description' => $description,
1733
                'sound' => $sound,
1734
                'type' => $type,
1735
                'random' => $random,
1736
                'random_answers' => $random_answers,
1737
                'active' => $active,
1738
                'results_disabled' => $results_disabled,
1739
                'max_attempt' => $attempts,
1740
                'feedback_type' => $feedback_type,
1741
                'expired_time' => $expired_time,
1742
                'session_id' => $session_id,
1743
                'review_answers' => $review_answers,
1744
                'random_by_category' => $randomByCat,
1745
                'text_when_finished' => $text_when_finished,
1746
                'display_category_name' => $display_category_name,
1747
                'pass_percentage' => $pass_percentage,
1748
                'save_correct_answers' => $saveCorrectAnswers,
1749
                'propagate_neg' => $propagate_neg,
1750
                'hide_question_title' => $this->getHideQuestionTitle(),
1751
            ];
1752
1753
            $allow = api_get_configuration_value('allow_exercise_categories');
1754
            if (true === $allow) {
1755
                if (!empty($this->getExerciseCategoryId())) {
1756
                    $params['exercise_category_id'] = $this->getExerciseCategoryId();
1757
                }
1758
            }
1759
1760
            if (api_get_configuration_value('quiz_prevent_backwards_move')) {
1761
                $params['prevent_backwards'] = $this->getPreventBackwards();
1762
            }
1763
1764
            $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
1765
            if (true === $allow) {
1766
                $params['show_previous_button'] = $this->showPreviousButton();
1767
            }
1768
1769
            $allow = api_get_configuration_value('allow_notification_setting_per_exercise');
1770
            if (true === $allow) {
1771
                $notifications = $this->getNotifications();
1772
                $params['notifications'] = '';
1773
                if (!empty($notifications)) {
1774
                    $notifications = implode(',', $notifications);
1775
                    $params['notifications'] = $notifications;
1776
                }
1777
            }
1778
1779
            $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
1780
            if ($pageConfig && !empty($this->pageResultConfiguration)) {
1781
                $params['page_result_configuration'] = $this->pageResultConfiguration;
1782
            }
1783
            if ($showHideConfiguration) {
1784
                $params['hide_question_number'] = $this->hideQuestionNumber;
1785
            }
1786
1787
            $this->id = $this->iId = Database::insert($TBL_EXERCISES, $params);
1788
1789
            if ($this->id) {
1790
                $sql = "UPDATE $TBL_EXERCISES SET id = iid WHERE iid = {$this->id} ";
1791
                Database::query($sql);
1792
1793
                $sql = "UPDATE $TBL_EXERCISES
1794
                        SET question_selection_type= ".$this->getQuestionSelectionType()."
1795
                        WHERE id = ".$this->id." AND c_id = ".$this->course_id;
1796
                Database::query($sql);
1797
1798
                // insert into the item_property table
1799
                api_item_property_update(
1800
                    $this->course,
1801
                    TOOL_QUIZ,
1802
                    $this->id,
1803
                    'QuizAdded',
1804
                    api_get_user_id()
1805
                );
1806
1807
                // This function save the quiz again, carefull about start_time
1808
                // and end_time if you remove this line (see above)
1809
                api_set_default_visibility(
1810
                    $this->id,
1811
                    TOOL_QUIZ,
1812
                    null,
1813
                    $this->course
1814
                );
1815
1816
                if (api_get_setting('search_enabled') === 'true' && extension_loaded('xapian')) {
1817
                    $this->search_engine_save();
1818
                }
1819
            }
1820
        }
1821
1822
        $this->save_categories_in_exercise($this->categories);
1823
1824
        return $this->iId;
1825
    }
1826
1827
    /**
1828
     * Updates question position.
1829
     *
1830
     * @return bool
1831
     */
1832
    public function update_question_positions()
1833
    {
1834
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1835
        // Fixes #3483 when updating order
1836
        $questionList = $this->selectQuestionList(true);
1837
1838
        $this->id = (int) $this->id;
1839
1840
        if (empty($this->id)) {
1841
            return false;
1842
        }
1843
1844
        if (!empty($questionList)) {
1845
            foreach ($questionList as $position => $questionId) {
1846
                $position = (int) $position;
1847
                $questionId = (int) $questionId;
1848
                $sql = "UPDATE $table SET
1849
                            question_order ='".$position."'
1850
                        WHERE
1851
                            c_id = ".$this->course_id." AND
1852
                            question_id = ".$questionId." AND
1853
                            exercice_id=".$this->id;
1854
                Database::query($sql);
1855
            }
1856
        }
1857
1858
        return true;
1859
    }
1860
1861
    /**
1862
     * Adds a question into the question list.
1863
     *
1864
     * @author Olivier Brouckaert
1865
     *
1866
     * @param int $questionId - question ID
1867
     *
1868
     * @return bool - true if the question has been added, otherwise false
1869
     */
1870
    public function addToList($questionId)
1871
    {
1872
        // checks if the question ID is not in the list
1873
        if (!$this->isInList($questionId)) {
1874
            // selects the max position
1875
            if (!$this->selectNbrQuestions()) {
1876
                $pos = 1;
1877
            } else {
1878
                if (is_array($this->questionList)) {
1879
                    $pos = max(array_keys($this->questionList)) + 1;
1880
                }
1881
            }
1882
            $this->questionList[$pos] = $questionId;
1883
1884
            return true;
1885
        }
1886
1887
        return false;
1888
    }
1889
1890
    /**
1891
     * removes a question from the question list.
1892
     *
1893
     * @author Olivier Brouckaert
1894
     *
1895
     * @param int $questionId - question ID
1896
     *
1897
     * @return bool - true if the question has been removed, otherwise false
1898
     */
1899
    public function removeFromList($questionId)
1900
    {
1901
        // searches the position of the question ID in the list
1902
        $pos = array_search($questionId, $this->questionList);
1903
        // question not found
1904
        if (false === $pos) {
1905
            return false;
1906
        } else {
1907
            // dont reduce the number of random question if we use random by category option, or if
1908
            // random all questions
1909
            if ($this->isRandom() && 0 == $this->isRandomByCat()) {
1910
                if (count($this->questionList) >= $this->random && $this->random > 0) {
1911
                    $this->random--;
1912
                    $this->save();
1913
                }
1914
            }
1915
            // deletes the position from the array containing the wanted question ID
1916
            unset($this->questionList[$pos]);
1917
1918
            return true;
1919
        }
1920
    }
1921
1922
    /**
1923
     * deletes the exercise from the database
1924
     * Notice : leaves the question in the data base.
1925
     *
1926
     * @author Olivier Brouckaert
1927
     */
1928
    public function delete()
1929
    {
1930
        $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
1931
1932
        if ($limitTeacherAccess && !api_is_platform_admin()) {
1933
            return false;
1934
        }
1935
1936
        $locked = api_resource_is_locked_by_gradebook(
1937
            $this->id,
1938
            LINK_EXERCISE
1939
        );
1940
1941
        if ($locked) {
1942
            return false;
1943
        }
1944
1945
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
1946
        $sql = "UPDATE $table SET active='-1'
1947
                WHERE c_id = ".$this->course_id." AND id = ".intval($this->id);
1948
        Database::query($sql);
1949
1950
        api_item_property_update(
1951
            $this->course,
1952
            TOOL_QUIZ,
1953
            $this->id,
1954
            'QuizDeleted',
1955
            api_get_user_id()
1956
        );
1957
        api_item_property_update(
1958
            $this->course,
1959
            TOOL_QUIZ,
1960
            $this->id,
1961
            'delete',
1962
            api_get_user_id()
1963
        );
1964
1965
        Skill::deleteSkillsFromItem($this->iId, ITEM_TYPE_EXERCISE);
1966
1967
        if (api_get_setting('search_enabled') === 'true' &&
1968
            extension_loaded('xapian')
1969
        ) {
1970
            $this->search_engine_delete();
1971
        }
1972
1973
        $linkInfo = GradebookUtils::isResourceInCourseGradebook(
1974
            $this->course['code'],
1975
            LINK_EXERCISE,
1976
            $this->id,
1977
            $this->sessionId
1978
        );
1979
1980
        if ($linkInfo !== false) {
1981
            GradebookUtils::remove_resource_from_course_gradebook($linkInfo['id']);
1982
        }
1983
1984
        return true;
1985
    }
1986
1987
    /**
1988
     * Creates the form to create / edit an exercise.
1989
     *
1990
     * @param FormValidator $form
1991
     * @param string        $type
1992
     */
1993
    public function createForm($form, $type = 'full')
1994
    {
1995
        if (empty($type)) {
1996
            $type = 'full';
1997
        }
1998
1999
        // Form title
2000
        $form_title = get_lang('NewEx');
2001
        if (!empty($_GET['exerciseId'])) {
2002
            $form_title = get_lang('ModifyExercise');
2003
        }
2004
2005
        $form->addHeader($form_title);
2006
2007
        // Title.
2008
        if (api_get_configuration_value('save_titles_as_html')) {
2009
            $form->addHtmlEditor(
2010
                'exerciseTitle',
2011
                get_lang('ExerciseName'),
2012
                false,
2013
                false,
2014
                ['ToolbarSet' => 'TitleAsHtml']
2015
            );
2016
        } else {
2017
            $form->addElement(
2018
                'text',
2019
                'exerciseTitle',
2020
                get_lang('ExerciseName'),
2021
                ['id' => 'exercise_title']
2022
            );
2023
        }
2024
2025
        $form->addElement('advanced_settings', 'advanced_params', get_lang('AdvancedParameters'));
2026
        $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
2027
2028
        if (api_get_configuration_value('allow_exercise_categories')) {
2029
            $categoryManager = new ExerciseCategoryManager();
2030
            $categories = $categoryManager->getCategories(api_get_course_int_id());
2031
            $options = [];
2032
            if (!empty($categories)) {
2033
                /** @var CExerciseCategory $category */
2034
                foreach ($categories as $category) {
2035
                    $options[$category->getId()] = $category->getName();
2036
                }
2037
            }
2038
2039
            $form->addSelect(
2040
                'exercise_category_id',
2041
                get_lang('Category'),
2042
                $options,
2043
                ['placeholder' => get_lang('SelectAnOption')]
2044
            );
2045
        }
2046
2047
        $editor_config = [
2048
            'ToolbarSet' => 'TestQuestionDescription',
2049
            'Width' => '100%',
2050
            'Height' => '150',
2051
        ];
2052
2053
        if (is_array($type)) {
2054
            $editor_config = array_merge($editor_config, $type);
2055
        }
2056
2057
        $form->addHtmlEditor(
2058
            'exerciseDescription',
2059
            get_lang('ExerciseDescription'),
2060
            false,
2061
            false,
2062
            $editor_config
2063
        );
2064
2065
        $skillList = [];
2066
        if ('full' === $type) {
2067
            // Can't modify a DirectFeedback question.
2068
            if (!in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
2069
                $this->setResultFeedbackGroup($form);
2070
2071
                // Type of results display on the final page
2072
                $this->setResultDisabledGroup($form);
2073
2074
                // Type of questions disposition on page
2075
                $radios = [];
2076
                $radios[] = $form->createElement(
2077
                    'radio',
2078
                    'exerciseType',
2079
                    null,
2080
                    get_lang('SimpleExercise'),
2081
                    '1',
2082
                    [
2083
                        'onclick' => 'check_per_page_all()',
2084
                        'id' => 'option_page_all',
2085
                    ]
2086
                );
2087
                $radios[] = $form->createElement(
2088
                    'radio',
2089
                    'exerciseType',
2090
                    null,
2091
                    get_lang('SequentialExercise'),
2092
                    '2',
2093
                    [
2094
                        'onclick' => 'check_per_page_one()',
2095
                        'id' => 'option_page_one',
2096
                    ]
2097
                );
2098
2099
                $form->addGroup($radios, null, get_lang('QuestionsPerPage'));
2100
            } else {
2101
                // if is Direct feedback but has not questions we can allow to modify the question type
2102
                if (empty($this->iId) || 0 === $this->getQuestionCount()) {
2103
                    $this->setResultFeedbackGroup($form);
2104
                    $this->setResultDisabledGroup($form);
2105
2106
                    // Type of questions disposition on page
2107
                    $radios = [];
2108
                    $radios[] = $form->createElement('radio', 'exerciseType', null, get_lang('SimpleExercise'), '1');
2109
                    $radios[] = $form->createElement(
2110
                        'radio',
2111
                        'exerciseType',
2112
                        null,
2113
                        get_lang('SequentialExercise'),
2114
                        '2'
2115
                    );
2116
                    $form->addGroup($radios, null, get_lang('ExerciseType'));
2117
                } else {
2118
                    $this->setResultFeedbackGroup($form, true);
2119
                    $group = $this->setResultDisabledGroup($form);
2120
                    $group->freeze();
2121
2122
                    // we force the options to the DirectFeedback exercisetype
2123
                    //$form->addElement('hidden', 'exerciseFeedbackType', $this->getFeedbackType());
2124
                    //$form->addElement('hidden', 'exerciseType', ONE_PER_PAGE);
2125
2126
                    // Type of questions disposition on page
2127
                    $radios[] = $form->createElement(
2128
                        'radio',
2129
                        'exerciseType',
2130
                        null,
2131
                        get_lang('SimpleExercise'),
2132
                        '1',
2133
                        [
2134
                            'onclick' => 'check_per_page_all()',
2135
                            'id' => 'option_page_all',
2136
                        ]
2137
                    );
2138
                    $radios[] = $form->createElement(
2139
                        'radio',
2140
                        'exerciseType',
2141
                        null,
2142
                        get_lang('SequentialExercise'),
2143
                        '2',
2144
                        [
2145
                            'onclick' => 'check_per_page_one()',
2146
                            'id' => 'option_page_one',
2147
                        ]
2148
                    );
2149
2150
                    $type_group = $form->addGroup($radios, null, get_lang('QuestionsPerPage'));
2151
                    $type_group->freeze();
2152
                }
2153
            }
2154
2155
            $option = [
2156
                EX_Q_SELECTION_ORDERED => get_lang('OrderedByUser'),
2157
                //  Defined by user
2158
                EX_Q_SELECTION_RANDOM => get_lang('Random'),
2159
                // 1-10, All
2160
                'per_categories' => '--------'.get_lang('UsingCategories').'----------',
2161
                // Base (A 123 {3} B 456 {3} C 789{2} D 0{0}) --> Matrix {3, 3, 2, 0}
2162
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED => get_lang('OrderedCategoriesAlphabeticallyWithQuestionsOrdered'),
2163
                // A 123 B 456 C 78 (0, 1, all)
2164
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED => get_lang('RandomCategoriesWithQuestionsOrdered'),
2165
                // C 78 B 456 A 123
2166
                EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM => get_lang('OrderedCategoriesAlphabeticallyWithRandomQuestions'),
2167
                // A 321 B 654 C 87
2168
                EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM => get_lang('RandomCategoriesWithRandomQuestions'),
2169
                // C 87 B 654 A 321
2170
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_ORDERED_NO_GROUPED => get_lang('RandomCategoriesWithQuestionsOrderedNoQuestionGrouped'),
2171
                /*    B 456 C 78 A 123
2172
                        456 78 123
2173
                        123 456 78
2174
                */
2175
                //EX_Q_SELECTION_CATEGORIES_RANDOM_QUESTIONS_RANDOM_NO_GROUPED => get_lang('RandomCategoriesWithRandomQuestionsNoQuestionGrouped'),
2176
                /*
2177
                    A 123 B 456 C 78
2178
                    B 456 C 78 A 123
2179
                    B 654 C 87 A 321
2180
                    654 87 321
2181
                    165 842 73
2182
                */
2183
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED => get_lang('OrderedCategoriesByParentWithQuestionsOrdered'),
2184
                //EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM => get_lang('OrderedCategoriesByParentWithQuestionsRandom'),
2185
            ];
2186
2187
            $form->addElement(
2188
                'select',
2189
                'question_selection_type',
2190
                [get_lang('QuestionSelection')],
2191
                $option,
2192
                [
2193
                    'id' => 'questionSelection',
2194
                    'onchange' => 'checkQuestionSelection()',
2195
                ]
2196
            );
2197
2198
            $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
2199
            if ($pageConfig) {
2200
                $group = [
2201
                    $form->createElement(
2202
                        'checkbox',
2203
                        'hide_expected_answer',
2204
                        null,
2205
                        get_lang('HideExpectedAnswer')
2206
                    ),
2207
                    $form->createElement(
2208
                        'checkbox',
2209
                        'hide_total_score',
2210
                        null,
2211
                        get_lang('HideTotalScore')
2212
                    ),
2213
                    $form->createElement(
2214
                        'checkbox',
2215
                        'hide_question_score',
2216
                        null,
2217
                        get_lang('HideQuestionScore')
2218
                    ),
2219
                    $form->createElement(
2220
                        'checkbox',
2221
                        'hide_category_table',
2222
                        null,
2223
                        get_lang('HideCategoryTable')
2224
                    ),
2225
                    $form->createElement(
2226
                        'checkbox',
2227
                        'hide_correct_answered_questions',
2228
                        null,
2229
                        get_lang('HideCorrectAnsweredQuestions')
2230
                    ),
2231
                ];
2232
                $form->addGroup($group, null, get_lang('ResultsConfigurationPage'));
2233
            }
2234
            $showHideConfiguration = api_get_configuration_value('quiz_hide_question_number');
2235
            if ($showHideConfiguration) {
2236
                $group = [
2237
                    $form->createElement('radio', 'hide_question_number', null, get_lang('Yes'), '1'),
2238
                    $form->createElement('radio', 'hide_question_number', null, get_lang('No'), '0'),
2239
                ];
2240
                $form->addGroup($group, null, get_lang('HideQuestionNumber'));
2241
            }
2242
2243
            $displayMatrix = 'none';
2244
            $displayRandom = 'none';
2245
            $selectionType = $this->getQuestionSelectionType();
2246
            switch ($selectionType) {
2247
                case EX_Q_SELECTION_RANDOM:
2248
                    $displayRandom = 'block';
2249
                    break;
2250
                case $selectionType >= EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED:
2251
                    $displayMatrix = 'block';
2252
                    break;
2253
            }
2254
2255
            $form->addHtml('<div id="hidden_random" style="display:'.$displayRandom.'">');
2256
            // Number of random question.
2257
            $max = ($this->id > 0) ? $this->getQuestionCount() : 10;
2258
            $option = range(0, $max);
2259
            $option[0] = get_lang('No');
2260
            $option[-1] = get_lang('AllQuestionsShort');
2261
            $form->addElement(
2262
                'select',
2263
                'randomQuestions',
2264
                [
2265
                    get_lang('RandomQuestions'),
2266
                    get_lang('RandomQuestionsHelp'),
2267
                ],
2268
                $option,
2269
                ['id' => 'randomQuestions']
2270
            );
2271
            $form->addHtml('</div>');
2272
            $form->addHtml('<div id="hidden_matrix" style="display:'.$displayMatrix.'">');
2273
2274
            // Category selection.
2275
            $cat = new TestCategory();
2276
            $cat_form = $cat->returnCategoryForm($this);
2277
            if (empty($cat_form)) {
2278
                $cat_form = '<span class="label label-warning">'.get_lang('NoCategoriesDefined').'</span>';
2279
            }
2280
            $form->addElement('label', null, $cat_form);
2281
            $form->addHtml('</div>');
2282
2283
            // Random answers.
2284
            $radios_random_answers = [
2285
                $form->createElement('radio', 'randomAnswers', null, get_lang('Yes'), '1'),
2286
                $form->createElement('radio', 'randomAnswers', null, get_lang('No'), '0'),
2287
            ];
2288
            $form->addGroup($radios_random_answers, null, get_lang('RandomAnswers'));
2289
2290
            // Category name.
2291
            $radio_display_cat_name = [
2292
                $form->createElement('radio', 'display_category_name', null, get_lang('Yes'), '1'),
2293
                $form->createElement('radio', 'display_category_name', null, get_lang('No'), '0'),
2294
            ];
2295
            $form->addGroup($radio_display_cat_name, null, get_lang('QuestionDisplayCategoryName'));
2296
2297
            // Hide question title.
2298
            $group = [
2299
                $form->createElement('radio', 'hide_question_title', null, get_lang('Yes'), '1'),
2300
                $form->createElement('radio', 'hide_question_title', null, get_lang('No'), '0'),
2301
            ];
2302
            $form->addGroup($group, null, get_lang('HideQuestionTitle'));
2303
2304
            $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
2305
            if (true === $allow) {
2306
                // Hide question title.
2307
                $group = [
2308
                    $form->createElement(
2309
                        'radio',
2310
                        'show_previous_button',
2311
                        null,
2312
                        get_lang('Yes'),
2313
                        '1'
2314
                    ),
2315
                    $form->createElement(
2316
                        'radio',
2317
                        'show_previous_button',
2318
                        null,
2319
                        get_lang('No'),
2320
                        '0'
2321
                    ),
2322
                ];
2323
                $form->addGroup($group, null, get_lang('ShowPreviousButton'));
2324
            }
2325
2326
            $form->addElement(
2327
                'number',
2328
                'exerciseAttempts',
2329
                get_lang('ExerciseAttempts'),
2330
                null,
2331
                ['id' => 'exerciseAttempts']
2332
            );
2333
2334
            // Exercise time limit
2335
            $form->addElement(
2336
                'checkbox',
2337
                'activate_start_date_check',
2338
                null,
2339
                get_lang('EnableStartTime'),
2340
                ['onclick' => 'activate_start_date()']
2341
            );
2342
2343
            if (!empty($this->start_time)) {
2344
                $form->addElement('html', '<div id="start_date_div" style="display:block;">');
2345
            } else {
2346
                $form->addElement('html', '<div id="start_date_div" style="display:none;">');
2347
            }
2348
2349
            $form->addElement('date_time_picker', 'start_time');
2350
            $form->addElement('html', '</div>');
2351
            $form->addElement(
2352
                'checkbox',
2353
                'activate_end_date_check',
2354
                null,
2355
                get_lang('EnableEndTime'),
2356
                ['onclick' => 'activate_end_date()']
2357
            );
2358
2359
            if (!empty($this->end_time)) {
2360
                $form->addHtml('<div id="end_date_div" style="display:block;">');
2361
            } else {
2362
                $form->addHtml('<div id="end_date_div" style="display:none;">');
2363
            }
2364
2365
            $form->addElement('date_time_picker', 'end_time');
2366
            $form->addElement('html', '</div>');
2367
2368
            $display = 'block';
2369
            $form->addElement(
2370
                'checkbox',
2371
                'propagate_neg',
2372
                null,
2373
                get_lang('PropagateNegativeResults')
2374
            );
2375
2376
            if (api_get_configuration_value('allow_quiz_save_correct_options')) {
2377
                $options = [
2378
                    '' => get_lang('SelectAnOption'),
2379
                    1 => get_lang('SaveTheCorrectAnswersForTheNextAttempt'),
2380
                    2 => get_lang('SaveAllAnswers'),
2381
                ];
2382
                $form->addSelect(
2383
                    'save_correct_answers',
2384
                    get_lang('SaveAnswers'),
2385
                    $options
2386
                );
2387
            } else {
2388
                $form->addCheckBox(
2389
                    'save_correct_answers',
2390
                    null,
2391
                    get_lang('SaveTheCorrectAnswersForTheNextAttempt')
2392
                );
2393
            }
2394
2395
            $form->addElement('html', '<div class="clear">&nbsp;</div>');
2396
            $form->addElement('checkbox', 'review_answers', null, get_lang('ReviewAnswers'));
2397
            $form->addElement('html', '<div id="divtimecontrol"  style="display:'.$display.';">');
2398
2399
            // Timer control
2400
            $form->addElement(
2401
                'checkbox',
2402
                'enabletimercontrol',
2403
                null,
2404
                get_lang('EnableTimerControl'),
2405
                [
2406
                    'onclick' => 'option_time_expired()',
2407
                    'id' => 'enabletimercontrol',
2408
                    'onload' => 'check_load_time()',
2409
                ]
2410
            );
2411
2412
            $expired_date = (int) $this->selectExpiredTime();
2413
2414
            if (($expired_date != '0')) {
2415
                $form->addElement('html', '<div id="timercontrol" style="display:block;">');
2416
            } else {
2417
                $form->addElement('html', '<div id="timercontrol" style="display:none;">');
2418
            }
2419
            $form->addText(
2420
                'enabletimercontroltotalminutes',
2421
                get_lang('ExerciseTotalDurationInMinutes'),
2422
                false,
2423
                [
2424
                    'id' => 'enabletimercontroltotalminutes',
2425
                    'cols-size' => [2, 2, 8],
2426
                ]
2427
            );
2428
            $form->addElement('html', '</div>');
2429
2430
            if (api_get_configuration_value('quiz_prevent_backwards_move')) {
2431
                $form->addCheckBox(
2432
                    'prevent_backwards',
2433
                    null,
2434
                    get_lang('QuizPreventBackwards')
2435
                );
2436
            }
2437
2438
            $form->addElement(
2439
                'text',
2440
                'pass_percentage',
2441
                [get_lang('PassPercentage'), null, '%'],
2442
                ['id' => 'pass_percentage']
2443
            );
2444
2445
            $form->addRule('pass_percentage', get_lang('Numeric'), 'numeric');
2446
            $form->addRule('pass_percentage', get_lang('ValueTooSmall'), 'min_numeric_length', 0);
2447
            $form->addRule('pass_percentage', get_lang('ValueTooBig'), 'max_numeric_length', 100);
2448
2449
            // add the text_when_finished textbox
2450
            $form->addHtmlEditor(
2451
                'text_when_finished',
2452
                get_lang('TextWhenFinished'),
2453
                false,
2454
                false,
2455
                $editor_config
2456
            );
2457
2458
            $allow = api_get_configuration_value('allow_notification_setting_per_exercise');
2459
            if ($allow === true) {
2460
                $settings = ExerciseLib::getNotificationSettings();
2461
                $group = [];
2462
                foreach ($settings as $itemId => $label) {
2463
                    $group[] = $form->createElement(
2464
                        'checkbox',
2465
                        'notifications[]',
2466
                        null,
2467
                        $label,
2468
                        ['value' => $itemId]
2469
                    );
2470
                }
2471
                $form->addGroup($group, '', [get_lang('EmailNotifications')]);
2472
            }
2473
2474
            $form->addCheckBox(
2475
                'update_title_in_lps',
2476
                null,
2477
                get_lang('UpdateTitleInLps')
2478
            );
2479
2480
            $defaults = [];
2481
            if (api_get_setting('search_enabled') === 'true') {
2482
                require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
2483
                $form->addElement('checkbox', 'index_document', '', get_lang('SearchFeatureDoIndexDocument'));
2484
                $form->addSelectLanguage('language', get_lang('SearchFeatureDocumentLanguage'));
2485
                $specific_fields = get_specific_field_list();
2486
2487
                foreach ($specific_fields as $specific_field) {
2488
                    $form->addElement('text', $specific_field['code'], $specific_field['name']);
2489
                    $filter = [
2490
                        'c_id' => api_get_course_int_id(),
2491
                        'field_id' => $specific_field['id'],
2492
                        'ref_id' => $this->id,
2493
                        'tool_id' => "'".TOOL_QUIZ."'",
2494
                    ];
2495
                    $values = get_specific_field_values_list($filter, ['value']);
2496
                    if (!empty($values)) {
2497
                        $arr_str_values = [];
2498
                        foreach ($values as $value) {
2499
                            $arr_str_values[] = $value['value'];
2500
                        }
2501
                        $defaults[$specific_field['code']] = implode(', ', $arr_str_values);
2502
                    }
2503
                }
2504
            }
2505
2506
            $skillList = Skill::addSkillsToForm($form, ITEM_TYPE_EXERCISE, $this->iId);
2507
2508
            $extraField = new ExtraField('exercise');
2509
            $extraField->addElements(
2510
                $form,
2511
                $this->iId,
2512
                [
2513
                    'notifications',
2514
                    'remedialcourselist',
2515
                    'advancedcourselist',
2516
                ], //exclude
2517
                false, // filter
2518
                false, // tag as select
2519
                [], //show only fields
2520
                [], // order fields
2521
                [] // extra data
2522
            );
2523
2524
            // See BT#18165
2525
            $remedialList = [
2526
                'remedialcourselist' => 'RemedialCourses',
2527
                'advancedcourselist' => 'AdvancedCourses',
2528
            ];
2529
            $extraFieldExercice = new ExtraField('exercise');
2530
            $extraFieldExerciceValue = new ExtraFieldValue('exercise');
2531
            $pluginRemedial = api_get_plugin_setting('remedial_course', 'enabled') === 'true';
2532
            if ($pluginRemedial) {
2533
                $sessionId = api_get_session_id();
2534
                $userId = api_get_user_id();
2535
                foreach ($remedialList as $item => $label) {
2536
                    $remedialField = $extraFieldExercice->get_handler_field_info_by_field_variable($item);
2537
                    $optionRemedial = [];
2538
                    $defaults[$item] = [];
2539
                    $remedialExtraValue = $extraFieldExerciceValue->get_values_by_handler_and_field_id($this->iId, $remedialField['id']);
2540
                    $defaults[$item] = isset($remedialExtraValue['value']) ? explode(';', $remedialExtraValue['value']) : [];
2541
                    if ($sessionId != 0) {
2542
                        $courseList = SessionManager::getCoursesInSession($sessionId);
2543
                        foreach ($courseList as $course) {
2544
                            $courseSession = api_get_course_info_by_id($course);
2545
                            if (!empty($courseSession) && isset($courseSession['real_id'])) {
2546
                                $courseId = $courseSession['real_id'];
2547
                                if (api_get_course_int_id() != $courseId) {
2548
                                    $optionRemedial[$courseId] = $courseSession['title'];
2549
                                }
2550
                            }
2551
                        }
2552
                    } else {
2553
                        $courseList = CourseManager::get_course_list();
2554
                        foreach ($courseList as $course) {
2555
                            if (!empty($course) && isset($course['real_id'])) {
2556
                                $courseId = $course['real_id'];
2557
                                if (api_get_course_int_id() != $courseId) {
2558
                                    $optionRemedial[$courseId] = $course['title'];
2559
                                }
2560
                            }
2561
                        }
2562
                    }
2563
                    unset($optionRemedial[0]);
2564
                    $form->addSelect(
2565
                        "extra_".$item,
2566
                        get_plugin_lang($label, RemedialCoursePlugin::class),
2567
                        $optionRemedial,
2568
                        [
2569
                            'placeholder' => get_lang('SelectAnOption'),
2570
                            'multiple' => 'multiple',
2571
                        ]
2572
                    );
2573
                }
2574
            }
2575
2576
            $settings = api_get_configuration_value('exercise_finished_notification_settings');
2577
            if (!empty($settings)) {
2578
                $options = [];
2579
                foreach ($settings as $name => $data) {
2580
                    $options[$name] = $name;
2581
                }
2582
                $form->addSelect(
2583
                    'extra_notifications',
2584
                    get_lang('Notifications'),
2585
                    $options,
2586
                    ['placeholder' => get_lang('SelectAnOption')]
2587
                );
2588
            }
2589
            $form->addElement('html', '</div>'); //End advanced setting
2590
            $form->addElement('html', '</div>');
2591
        }
2592
2593
        // submit
2594
        if (isset($_GET['exerciseId'])) {
2595
            $form->addButtonSave(get_lang('ModifyExercise'), 'submitExercise');
2596
        } else {
2597
            $form->addButtonUpdate(get_lang('ProcedToQuestions'), 'submitExercise');
2598
        }
2599
2600
        $form->addRule('exerciseTitle', get_lang('GiveExerciseName'), 'required');
2601
2602
        // defaults
2603
        if ($type == 'full') {
2604
            // rules
2605
            $form->addRule('exerciseAttempts', get_lang('Numeric'), 'numeric');
2606
            $form->addRule('start_time', get_lang('InvalidDate'), 'datetime');
2607
            $form->addRule('end_time', get_lang('InvalidDate'), 'datetime');
2608
2609
            if ($this->id > 0) {
2610
                $defaults['randomQuestions'] = $this->random;
2611
                $defaults['randomAnswers'] = $this->getRandomAnswers();
2612
                $defaults['exerciseType'] = $this->selectType();
2613
                $defaults['exerciseTitle'] = $this->get_formated_title();
2614
                $defaults['exerciseDescription'] = $this->selectDescription();
2615
                $defaults['exerciseAttempts'] = $this->selectAttempts();
2616
                $defaults['exerciseFeedbackType'] = $this->getFeedbackType();
2617
                $defaults['results_disabled'] = $this->selectResultsDisabled();
2618
                $defaults['propagate_neg'] = $this->selectPropagateNeg();
2619
                $defaults['save_correct_answers'] = $this->getSaveCorrectAnswers();
2620
                $defaults['review_answers'] = $this->review_answers;
2621
                $defaults['randomByCat'] = $this->getRandomByCategory();
2622
                $defaults['text_when_finished'] = $this->getTextWhenFinished();
2623
                $defaults['display_category_name'] = $this->selectDisplayCategoryName();
2624
                $defaults['pass_percentage'] = $this->selectPassPercentage();
2625
                $defaults['question_selection_type'] = $this->getQuestionSelectionType();
2626
                $defaults['hide_question_title'] = $this->getHideQuestionTitle();
2627
                $defaults['show_previous_button'] = $this->showPreviousButton();
2628
                $defaults['exercise_category_id'] = $this->getExerciseCategoryId();
2629
                $defaults['prevent_backwards'] = $this->getPreventBackwards();
2630
2631
                if (!empty($this->start_time)) {
2632
                    $defaults['activate_start_date_check'] = 1;
2633
                }
2634
                if (!empty($this->end_time)) {
2635
                    $defaults['activate_end_date_check'] = 1;
2636
                }
2637
2638
                $defaults['start_time'] = !empty($this->start_time) ? api_get_local_time($this->start_time) : date('Y-m-d 12:00:00');
2639
                $defaults['end_time'] = !empty($this->end_time) ? api_get_local_time($this->end_time) : date('Y-m-d 12:00:00', time() + 84600);
2640
2641
                // Get expired time
2642
                if ($this->expired_time != '0') {
2643
                    $defaults['enabletimercontrol'] = 1;
2644
                    $defaults['enabletimercontroltotalminutes'] = $this->expired_time;
2645
                } else {
2646
                    $defaults['enabletimercontroltotalminutes'] = 0;
2647
                }
2648
                $defaults['skills'] = array_keys($skillList);
2649
                $defaults['notifications'] = $this->getNotifications();
2650
            } else {
2651
                $defaults['exerciseType'] = 2;
2652
                $defaults['exerciseAttempts'] = 0;
2653
                $defaults['randomQuestions'] = 0;
2654
                $defaults['randomAnswers'] = 0;
2655
                $defaults['exerciseDescription'] = '';
2656
                $defaults['exerciseFeedbackType'] = 0;
2657
                $defaults['results_disabled'] = 0;
2658
                $defaults['randomByCat'] = 0;
2659
                $defaults['text_when_finished'] = '';
2660
                $defaults['start_time'] = date('Y-m-d 12:00:00');
2661
                $defaults['display_category_name'] = 1;
2662
                $defaults['end_time'] = date('Y-m-d 12:00:00', time() + 84600);
2663
                $defaults['pass_percentage'] = '';
2664
                $defaults['end_button'] = $this->selectEndButton();
2665
                $defaults['question_selection_type'] = 1;
2666
                $defaults['hide_question_title'] = 0;
2667
                $defaults['show_previous_button'] = 1;
2668
                $defaults['on_success_message'] = null;
2669
                $defaults['on_failed_message'] = null;
2670
            }
2671
        } else {
2672
            $defaults['exerciseTitle'] = $this->selectTitle();
2673
            $defaults['exerciseDescription'] = $this->selectDescription();
2674
        }
2675
2676
        if (api_get_setting('search_enabled') === 'true') {
2677
            $defaults['index_document'] = 'checked="checked"';
2678
        }
2679
2680
        $this->setPageResultConfigurationDefaults($defaults);
2681
        $this->setHideQuestionNumberDefaults($defaults);
2682
        $form->setDefaults($defaults);
2683
2684
        // Freeze some elements.
2685
        if ($this->id != 0 && $this->edit_exercise_in_lp == false) {
2686
            $elementsToFreeze = [
2687
                'randomQuestions',
2688
                //'randomByCat',
2689
                'exerciseAttempts',
2690
                'propagate_neg',
2691
                'enabletimercontrol',
2692
                'review_answers',
2693
            ];
2694
2695
            foreach ($elementsToFreeze as $elementName) {
2696
                /** @var HTML_QuickForm_element $element */
2697
                $element = $form->getElement($elementName);
2698
                $element->freeze();
2699
            }
2700
        }
2701
    }
2702
2703
    public function setResultFeedbackGroup(FormValidator $form, $checkFreeze = true)
2704
    {
2705
        // Feedback type.
2706
        $feedback = [];
2707
        $warning = sprintf(
2708
            get_lang('TheSettingXWillChangeToX'),
2709
            get_lang('ShowResultsToStudents'),
2710
            get_lang('ShowScoreAndRightAnswer')
2711
        );
2712
        $endTest = $form->createElement(
2713
            'radio',
2714
            'exerciseFeedbackType',
2715
            null,
2716
            get_lang('ExerciseAtTheEndOfTheTest'),
2717
            EXERCISE_FEEDBACK_TYPE_END,
2718
            [
2719
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_END,
2720
                //'onclick' => 'if confirm() check_feedback()',
2721
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_feedback(); } else { return false;} ',
2722
            ]
2723
        );
2724
2725
        $noFeedBack = $form->createElement(
2726
            'radio',
2727
            'exerciseFeedbackType',
2728
            null,
2729
            get_lang('NoFeedback'),
2730
            EXERCISE_FEEDBACK_TYPE_EXAM,
2731
            [
2732
                'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_EXAM,
2733
            ]
2734
        );
2735
2736
        $feedback[] = $endTest;
2737
        $feedback[] = $noFeedBack;
2738
2739
        $scenarioEnabled = 'true' === api_get_setting('enable_quiz_scenario');
2740
        $freeze = true;
2741
        if ($scenarioEnabled) {
2742
            if ($this->getQuestionCount() > 0) {
2743
                $hasDifferentQuestion = $this->hasQuestionWithTypeNotInList([UNIQUE_ANSWER, HOT_SPOT_DELINEATION]);
2744
2745
                if (false === $hasDifferentQuestion) {
2746
                    $freeze = false;
2747
                }
2748
            } else {
2749
                $freeze = false;
2750
            }
2751
2752
            $direct = $form->createElement(
2753
                'radio',
2754
                'exerciseFeedbackType',
2755
                null,
2756
                get_lang('DirectFeedback'),
2757
                EXERCISE_FEEDBACK_TYPE_DIRECT,
2758
                [
2759
                    'id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_DIRECT,
2760
                    'onclick' => 'check_direct_feedback()',
2761
                ]
2762
            );
2763
2764
            $directPopUp = $form->createElement(
2765
                'radio',
2766
                'exerciseFeedbackType',
2767
                null,
2768
                get_lang('ExerciseDirectPopUp'),
2769
                EXERCISE_FEEDBACK_TYPE_POPUP,
2770
                ['id' => 'exerciseType_'.EXERCISE_FEEDBACK_TYPE_POPUP, 'onclick' => 'check_direct_feedback()']
2771
            );
2772
2773
            if ($freeze) {
2774
                $direct->freeze();
2775
                $directPopUp->freeze();
2776
            }
2777
2778
            // If has delineation freeze all.
2779
            $hasDelineation = $this->hasQuestionWithType(HOT_SPOT_DELINEATION);
2780
            if ($hasDelineation) {
2781
                $endTest->freeze();
2782
                $noFeedBack->freeze();
2783
                $direct->freeze();
2784
                $directPopUp->freeze();
2785
            }
2786
2787
            $feedback[] = $direct;
2788
            $feedback[] = $directPopUp;
2789
        }
2790
2791
        $form->addGroup(
2792
            $feedback,
2793
            null,
2794
            [
2795
                get_lang('FeedbackType'),
2796
                get_lang('FeedbackDisplayOptions'),
2797
            ]
2798
        );
2799
    }
2800
2801
    /**
2802
     * function which process the creation of exercises.
2803
     *
2804
     * @param FormValidator $form
2805
     * @param string
2806
     *
2807
     * @return int c_quiz.iid
2808
     */
2809
    public function processCreation($form, $type = '')
2810
    {
2811
        $this->updateTitle(self::format_title_variable($form->getSubmitValue('exerciseTitle')));
2812
        $this->updateDescription($form->getSubmitValue('exerciseDescription'));
2813
        $this->updateAttempts($form->getSubmitValue('exerciseAttempts'));
2814
        $this->updateFeedbackType($form->getSubmitValue('exerciseFeedbackType'));
2815
        $this->updateType($form->getSubmitValue('exerciseType'));
2816
2817
        // If direct feedback then force to One per page
2818
        if (EXERCISE_FEEDBACK_TYPE_DIRECT == $form->getSubmitValue('exerciseFeedbackType')) {
2819
            $this->updateType(ONE_PER_PAGE);
2820
        }
2821
2822
        $this->setRandom($form->getSubmitValue('randomQuestions'));
2823
        $this->updateRandomAnswers($form->getSubmitValue('randomAnswers'));
2824
        $this->updateResultsDisabled($form->getSubmitValue('results_disabled'));
2825
        $this->updateExpiredTime($form->getSubmitValue('enabletimercontroltotalminutes'));
2826
        $this->updatePropagateNegative($form->getSubmitValue('propagate_neg'));
2827
        $this->updateSaveCorrectAnswers($form->getSubmitValue('save_correct_answers'));
2828
        $this->updateRandomByCat($form->getSubmitValue('randomByCat'));
2829
        $this->updateTextWhenFinished($form->getSubmitValue('text_when_finished'));
2830
        $this->updateDisplayCategoryName($form->getSubmitValue('display_category_name'));
2831
        $this->updateReviewAnswers($form->getSubmitValue('review_answers'));
2832
        $this->updatePassPercentage($form->getSubmitValue('pass_percentage'));
2833
        $this->updateCategories($form->getSubmitValue('category'));
2834
        $this->updateEndButton($form->getSubmitValue('end_button'));
2835
        $this->setOnSuccessMessage($form->getSubmitValue('on_success_message'));
2836
        $this->setOnFailedMessage($form->getSubmitValue('on_failed_message'));
2837
        $this->updateEmailNotificationTemplate($form->getSubmitValue('email_notification_template'));
2838
        $this->setEmailNotificationTemplateToUser($form->getSubmitValue('email_notification_template_to_user'));
2839
        $this->setNotifyUserByEmail($form->getSubmitValue('notify_user_by_email'));
2840
        $this->setModelType($form->getSubmitValue('model_type'));
2841
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2842
        $this->setHideQuestionTitle($form->getSubmitValue('hide_question_title'));
2843
        $this->sessionId = api_get_session_id();
2844
        $this->setQuestionSelectionType($form->getSubmitValue('question_selection_type'));
2845
        $this->setScoreTypeModel($form->getSubmitValue('score_type_model'));
2846
        $this->setGlobalCategoryId($form->getSubmitValue('global_category_id'));
2847
        $this->setShowPreviousButton($form->getSubmitValue('show_previous_button'));
2848
        $this->setNotifications($form->getSubmitValue('notifications'));
2849
        $this->setExerciseCategoryId($form->getSubmitValue('exercise_category_id'));
2850
        $this->setPageResultConfiguration($form->getSubmitValues());
2851
        $showHideConfiguration = api_get_configuration_value('quiz_hide_question_number');
2852
        if ($showHideConfiguration) {
2853
            $this->setHideQuestionNumber($form->getSubmitValue('hide_question_number'));
2854
        }
2855
        $this->preventBackwards = (int) $form->getSubmitValue('prevent_backwards');
2856
2857
        $this->start_time = null;
2858
        if ($form->getSubmitValue('activate_start_date_check') == 1) {
2859
            $start_time = $form->getSubmitValue('start_time');
2860
            $this->start_time = api_get_utc_datetime($start_time);
2861
        }
2862
2863
        $this->end_time = null;
2864
        if ($form->getSubmitValue('activate_end_date_check') == 1) {
2865
            $end_time = $form->getSubmitValue('end_time');
2866
            $this->end_time = api_get_utc_datetime($end_time);
2867
        }
2868
2869
        $this->expired_time = 0;
2870
        if ($form->getSubmitValue('enabletimercontrol') == 1) {
2871
            $expired_total_time = $form->getSubmitValue('enabletimercontroltotalminutes');
2872
            if ($this->expired_time == 0) {
2873
                $this->expired_time = $expired_total_time;
2874
            }
2875
        }
2876
2877
        $this->random_answers = 0;
2878
        if ($form->getSubmitValue('randomAnswers') == 1) {
2879
            $this->random_answers = 1;
2880
        }
2881
2882
        // Update title in all LPs that have this quiz added
2883
        if ($form->getSubmitValue('update_title_in_lps') == 1) {
2884
            $courseId = api_get_course_int_id();
2885
            $table = Database::get_course_table(TABLE_LP_ITEM);
2886
            $sql = "SELECT * FROM $table
2887
                    WHERE
2888
                        c_id = $courseId AND
2889
                        item_type = 'quiz' AND
2890
                        path = '".$this->id."'
2891
                    ";
2892
            $result = Database::query($sql);
2893
            $items = Database::store_result($result);
2894
            if (!empty($items)) {
2895
                foreach ($items as $item) {
2896
                    $itemId = $item['iid'];
2897
                    $sql = "UPDATE $table SET title = '".$this->title."'
2898
                            WHERE iid = $itemId AND c_id = $courseId ";
2899
                    Database::query($sql);
2900
                }
2901
            }
2902
        }
2903
2904
        $iId = $this->save($type);
2905
        if (!empty($iId)) {
2906
            $values = $form->getSubmitValues();
2907
            $values['item_id'] = $iId;
2908
            $extraFieldValue = new ExtraFieldValue('exercise');
2909
            $extraFieldValue->saveFieldValues($values);
2910
2911
            Skill::saveSkills($form, ITEM_TYPE_EXERCISE, $iId);
2912
        }
2913
    }
2914
2915
    public function search_engine_save()
2916
    {
2917
        if ($_POST['index_document'] != 1) {
2918
            return;
2919
        }
2920
        $course_id = api_get_course_id();
2921
2922
        require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
2923
2924
        $specific_fields = get_specific_field_list();
2925
        $ic_slide = new IndexableChunk();
2926
2927
        $all_specific_terms = '';
2928
        foreach ($specific_fields as $specific_field) {
2929
            if (isset($_REQUEST[$specific_field['code']])) {
2930
                $sterms = trim($_REQUEST[$specific_field['code']]);
2931
                if (!empty($sterms)) {
2932
                    $all_specific_terms .= ' '.$sterms;
2933
                    $sterms = explode(',', $sterms);
2934
                    foreach ($sterms as $sterm) {
2935
                        $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2936
                        add_specific_field_value($specific_field['id'], $course_id, TOOL_QUIZ, $this->id, $sterm);
2937
                    }
2938
                }
2939
            }
2940
        }
2941
2942
        // build the chunk to index
2943
        $ic_slide->addValue("title", $this->exercise);
2944
        $ic_slide->addCourseId($course_id);
2945
        $ic_slide->addToolId(TOOL_QUIZ);
2946
        $xapian_data = [
2947
            SE_COURSE_ID => $course_id,
2948
            SE_TOOL_ID => TOOL_QUIZ,
2949
            SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->id],
2950
            SE_USER => (int) api_get_user_id(),
2951
        ];
2952
        $ic_slide->xapian_data = serialize($xapian_data);
2953
        $exercise_description = $all_specific_terms.' '.$this->description;
2954
        $ic_slide->addValue("content", $exercise_description);
2955
2956
        $di = new ChamiloIndexer();
2957
        isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
2958
        $di->connectDb(null, null, $lang);
2959
        $di->addChunk($ic_slide);
2960
2961
        //index and return search engine document id
2962
        $did = $di->index();
2963
        if ($did) {
2964
            // save it to db
2965
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2966
            $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2967
			    VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2968
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id, $did);
2969
            Database::query($sql);
2970
        }
2971
    }
2972
2973
    public function search_engine_edit()
2974
    {
2975
        // update search enchine and its values table if enabled
2976
        if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
2977
            $course_id = api_get_course_id();
2978
2979
            // actually, it consists on delete terms from db,
2980
            // insert new ones, create a new search engine document, and remove the old one
2981
            // get search_did
2982
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2983
            $sql = 'SELECT * FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s LIMIT 1';
2984
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
2985
            $res = Database::query($sql);
2986
2987
            if (Database::num_rows($res) > 0) {
2988
                require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
2989
2990
                $se_ref = Database::fetch_array($res);
2991
                $specific_fields = get_specific_field_list();
2992
                $ic_slide = new IndexableChunk();
2993
2994
                $all_specific_terms = '';
2995
                foreach ($specific_fields as $specific_field) {
2996
                    delete_all_specific_field_value($course_id, $specific_field['id'], TOOL_QUIZ, $this->id);
2997
                    if (isset($_REQUEST[$specific_field['code']])) {
2998
                        $sterms = trim($_REQUEST[$specific_field['code']]);
2999
                        $all_specific_terms .= ' '.$sterms;
3000
                        $sterms = explode(',', $sterms);
3001
                        foreach ($sterms as $sterm) {
3002
                            $ic_slide->addTerm(trim($sterm), $specific_field['code']);
3003
                            add_specific_field_value($specific_field['id'], $course_id, TOOL_QUIZ, $this->id, $sterm);
3004
                        }
3005
                    }
3006
                }
3007
3008
                // build the chunk to index
3009
                $ic_slide->addValue('title', $this->exercise);
3010
                $ic_slide->addCourseId($course_id);
3011
                $ic_slide->addToolId(TOOL_QUIZ);
3012
                $xapian_data = [
3013
                    SE_COURSE_ID => $course_id,
3014
                    SE_TOOL_ID => TOOL_QUIZ,
3015
                    SE_DATA => ['type' => SE_DOCTYPE_EXERCISE_EXERCISE, 'exercise_id' => (int) $this->id],
3016
                    SE_USER => (int) api_get_user_id(),
3017
                ];
3018
                $ic_slide->xapian_data = serialize($xapian_data);
3019
                $exercise_description = $all_specific_terms.' '.$this->description;
3020
                $ic_slide->addValue('content', $exercise_description);
3021
3022
                $di = new ChamiloIndexer();
3023
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
3024
                $di->connectDb(null, null, $lang);
3025
                $di->remove_document($se_ref['search_did']);
3026
                $di->addChunk($ic_slide);
3027
3028
                //index and return search engine document id
3029
                $did = $di->index();
3030
                if ($did) {
3031
                    // save it to db
3032
                    $sql = 'DELETE FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=\'%s\'';
3033
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
3034
                    Database::query($sql);
3035
                    $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
3036
                        VALUES (NULL , \'%s\', \'%s\', %s, %s)';
3037
                    $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id, $did);
3038
                    Database::query($sql);
3039
                }
3040
            } else {
3041
                $this->search_engine_save();
3042
            }
3043
        }
3044
    }
3045
3046
    public function search_engine_delete()
3047
    {
3048
        // remove from search engine if enabled
3049
        if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
3050
            $course_id = api_get_course_id();
3051
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
3052
            $sql = 'SELECT * FROM %s
3053
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
3054
                    LIMIT 1';
3055
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
3056
            $res = Database::query($sql);
3057
            if (Database::num_rows($res) > 0) {
3058
                $row = Database::fetch_array($res);
3059
                $di = new ChamiloIndexer();
3060
                $di->remove_document($row['search_did']);
3061
                unset($di);
3062
                $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
3063
                foreach ($this->questionList as $question_i) {
3064
                    $sql = 'SELECT type FROM %s WHERE id=%s';
3065
                    $sql = sprintf($sql, $tbl_quiz_question, $question_i);
3066
                    $qres = Database::query($sql);
3067
                    if (Database::num_rows($qres) > 0) {
3068
                        $qrow = Database::fetch_array($qres);
3069
                        $objQuestion = Question::getInstance($qrow['type']);
3070
                        $objQuestion = Question::read((int) $question_i);
3071
                        $objQuestion->search_engine_edit($this->id, false, true);
3072
                        unset($objQuestion);
3073
                    }
3074
                }
3075
            }
3076
            $sql = 'DELETE FROM %s
3077
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level IS NULL
3078
                    LIMIT 1';
3079
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
3080
            Database::query($sql);
3081
3082
            // remove terms from db
3083
            require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
3084
            delete_all_values_for_item($course_id, TOOL_QUIZ, $this->id);
3085
        }
3086
    }
3087
3088
    public function selectExpiredTime()
3089
    {
3090
        return $this->expired_time;
3091
    }
3092
3093
    /**
3094
     * Cleans the student's results only for the Exercise tool (Not from the LP)
3095
     * The LP results are NOT deleted by default, otherwise put $cleanLpTests = true
3096
     * Works with exercises in sessions.
3097
     *
3098
     * @param bool   $cleanLpTests
3099
     * @param string $cleanResultBeforeDate
3100
     *
3101
     * @return int quantity of user's exercises deleted
3102
     */
3103
    public function cleanResults($cleanLpTests = false, $cleanResultBeforeDate = null)
3104
    {
3105
        $sessionId = api_get_session_id();
3106
        $table_track_e_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3107
        $table_track_e_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3108
3109
        $sql_where = '  AND
3110
                        orig_lp_id = 0 AND
3111
                        orig_lp_item_id = 0';
3112
3113
        // if we want to delete results from LP too
3114
        if ($cleanLpTests) {
3115
            $sql_where = '';
3116
        }
3117
3118
        // if we want to delete attempts before date $cleanResultBeforeDate
3119
        // $cleanResultBeforeDate must be a valid UTC-0 date yyyy-mm-dd
3120
3121
        if (!empty($cleanResultBeforeDate)) {
3122
            $cleanResultBeforeDate = Database::escape_string($cleanResultBeforeDate);
3123
            if (api_is_valid_date($cleanResultBeforeDate)) {
3124
                $sql_where .= "  AND exe_date <= '$cleanResultBeforeDate' ";
3125
            } else {
3126
                return 0;
3127
            }
3128
        }
3129
3130
        $sql = "SELECT exe_id
3131
            FROM $table_track_e_exercises
3132
            WHERE
3133
                c_id = ".api_get_course_int_id()." AND
3134
                exe_exo_id = ".$this->id." AND
3135
                session_id = ".$sessionId." ".
3136
                $sql_where;
3137
3138
        $result = Database::query($sql);
3139
        $exe_list = Database::store_result($result);
3140
3141
        // deleting TRACK_E_ATTEMPT table
3142
        // check if exe in learning path or not
3143
        $i = 0;
3144
        if (is_array($exe_list) && count($exe_list) > 0) {
3145
            foreach ($exe_list as $item) {
3146
                $sql = "DELETE FROM $table_track_e_attempt
3147
                        WHERE exe_id = '".$item['exe_id']."'";
3148
                Database::query($sql);
3149
                $i++;
3150
            }
3151
        }
3152
3153
        // delete TRACK_E_EXERCISES table
3154
        $sql = "DELETE FROM $table_track_e_exercises
3155
                WHERE
3156
                  c_id = ".api_get_course_int_id()." AND
3157
                  exe_exo_id = ".$this->id." $sql_where AND
3158
                  session_id = ".$sessionId;
3159
        Database::query($sql);
3160
3161
        $this->generateStats($this->id, api_get_course_info(), $sessionId);
3162
3163
        Event::addEvent(
3164
            LOG_EXERCISE_RESULT_DELETE,
3165
            LOG_EXERCISE_ID,
3166
            $this->id,
3167
            null,
3168
            null,
3169
            api_get_course_int_id(),
3170
            $sessionId
3171
        );
3172
3173
        return $i;
3174
    }
3175
3176
    /**
3177
     * Copies an exercise (duplicate all questions and answers).
3178
     */
3179
    public function copyExercise()
3180
    {
3181
        $exerciseObject = $this;
3182
        $categories = $exerciseObject->getCategoriesInExercise(true);
3183
        // Get all questions no matter the order/category settings
3184
        $questionList = $exerciseObject->getQuestionOrderedList();
3185
        $sourceId = $exerciseObject->iId;
3186
        // Force the creation of a new exercise
3187
        $exerciseObject->updateTitle($exerciseObject->selectTitle().' - '.get_lang('Copy'));
3188
        // Hides the new exercise
3189
        $exerciseObject->updateStatus(false);
3190
        $exerciseObject->updateId(0);
3191
        $exerciseObject->sessionId = api_get_session_id();
3192
        $courseId = api_get_course_int_id();
3193
        $exerciseObject->save();
3194
        $newId = $exerciseObject->selectId();
3195
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
3196
3197
        $count = 1;
3198
        $batchSize = 20;
3199
        $em = Database::getManager();
3200
3201
        if ($newId && !empty($questionList)) {
3202
            $extraField = new ExtraFieldValue('exercise');
3203
            $extraField->copy($sourceId, $newId);
3204
3205
            // Question creation
3206
            foreach ($questionList as $oldQuestionId) {
3207
                $oldQuestionObj = Question::read($oldQuestionId, null, false);
3208
                $newQuestionId = $oldQuestionObj->duplicate();
3209
                if ($newQuestionId) {
3210
                    $newQuestionObj = Question::read($newQuestionId, null, false);
3211
                    if (isset($newQuestionObj) && $newQuestionObj) {
3212
                        $sql = "INSERT INTO $exerciseRelQuestionTable (c_id, question_id, exercice_id, question_order)
3213
                                VALUES ($courseId, ".$newQuestionId.", ".$newId.", '$count')";
3214
                        Database::query($sql);
3215
                        $count++;
3216
3217
                        if (!empty($oldQuestionObj->category)) {
3218
                            $newQuestionObj->saveCategory($oldQuestionObj->category);
3219
                        }
3220
3221
                        // This should be moved to the duplicate function
3222
                        $newAnswerObj = new Answer($oldQuestionId, $courseId, $exerciseObject);
3223
                        $newAnswerObj->read();
3224
                        $newAnswerObj->duplicate($newQuestionObj);
3225
3226
                        if (($count % $batchSize) === 0) {
3227
                            $em->clear(); // Detaches all objects from Doctrine!
3228
                        }
3229
                    }
3230
                }
3231
            }
3232
            if (!empty($categories)) {
3233
                $newCategoryList = [];
3234
                foreach ($categories as $category) {
3235
                    $newCategoryList[$category['category_id']] = $category['count_questions'];
3236
                }
3237
                $exerciseObject->save_categories_in_exercise($newCategoryList);
3238
            }
3239
        }
3240
    }
3241
3242
    /**
3243
     * Changes the exercise status.
3244
     *
3245
     * @param string $status - exercise status
3246
     */
3247
    public function updateStatus($status)
3248
    {
3249
        $this->active = $status;
3250
    }
3251
3252
    /**
3253
     * @param int    $lp_id
3254
     * @param int    $lp_item_id
3255
     * @param int    $lp_item_view_id
3256
     * @param string $status
3257
     *
3258
     * @return array
3259
     */
3260
    public function get_stat_track_exercise_info(
3261
        $lp_id = 0,
3262
        $lp_item_id = 0,
3263
        $lp_item_view_id = 0,
3264
        $status = 'incomplete'
3265
    ) {
3266
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3267
        if (empty($lp_id)) {
3268
            $lp_id = 0;
3269
        }
3270
        if (empty($lp_item_id)) {
3271
            $lp_item_id = 0;
3272
        }
3273
        if (empty($lp_item_view_id)) {
3274
            $lp_item_view_id = 0;
3275
        }
3276
        $condition = ' WHERE exe_exo_id 	= '."'".$this->id."'".' AND
3277
					   exe_user_id 			= '."'".api_get_user_id()."'".' AND
3278
					   c_id                 = '.api_get_course_int_id().' AND
3279
					   status 				= '."'".Database::escape_string($status)."'".' AND
3280
					   orig_lp_id 			= '."'".$lp_id."'".' AND
3281
					   orig_lp_item_id 		= '."'".$lp_item_id."'".' AND
3282
                       orig_lp_item_view_id = '."'".$lp_item_view_id."'".' AND
3283
					   session_id 			= '."'".api_get_session_id()."' LIMIT 1"; //Adding limit 1 just in case
3284
3285
        $sql_track = 'SELECT * FROM '.$track_exercises.$condition;
3286
3287
        $result = Database::query($sql_track);
3288
        $new_array = [];
3289
        if (Database::num_rows($result) > 0) {
3290
            $new_array = Database::fetch_array($result, 'ASSOC');
3291
            $new_array['num_exe'] = Database::num_rows($result);
3292
        }
3293
3294
        return $new_array;
3295
    }
3296
3297
    /**
3298
     * Saves a test attempt.
3299
     *
3300
     * @param int $clock_expired_time clock_expired_time
3301
     * @param int  int lp id
3302
     * @param int  int lp item id
3303
     * @param int  int lp item_view id
3304
     * @param array $questionList
3305
     * @param float $weight
3306
     *
3307
     * @return int
3308
     */
3309
    public function save_stat_track_exercise_info(
3310
        $clock_expired_time = 0,
3311
        $safe_lp_id = 0,
3312
        $safe_lp_item_id = 0,
3313
        $safe_lp_item_view_id = 0,
3314
        $questionList = [],
3315
        $weight = 0
3316
    ) {
3317
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3318
        $safe_lp_id = (int) $safe_lp_id;
3319
        $safe_lp_item_id = (int) $safe_lp_item_id;
3320
        $safe_lp_item_view_id = (int) $safe_lp_item_view_id;
3321
3322
        if (empty($clock_expired_time)) {
3323
            $clock_expired_time = null;
3324
        }
3325
3326
        $questionList = array_map('intval', $questionList);
3327
3328
        $params = [
3329
            'exe_exo_id' => $this->id,
3330
            'exe_user_id' => api_get_user_id(),
3331
            'c_id' => api_get_course_int_id(),
3332
            'status' => 'incomplete',
3333
            'session_id' => api_get_session_id(),
3334
            'data_tracking' => implode(',', $questionList),
3335
            'start_date' => api_get_utc_datetime(),
3336
            'orig_lp_id' => $safe_lp_id,
3337
            'orig_lp_item_id' => $safe_lp_item_id,
3338
            'orig_lp_item_view_id' => $safe_lp_item_view_id,
3339
            'exe_weighting' => $weight,
3340
            'user_ip' => Database::escape_string(api_get_real_ip()),
3341
            'exe_date' => api_get_utc_datetime(),
3342
            'exe_result' => 0,
3343
            'steps_counter' => 0,
3344
            'exe_duration' => 0,
3345
            'expired_time_control' => $clock_expired_time,
3346
            'questions_to_check' => '',
3347
        ];
3348
3349
        return Database::insert($track_exercises, $params);
3350
    }
3351
3352
    /**
3353
     * @param int    $question_id
3354
     * @param int    $questionNum
3355
     * @param array  $questions_in_media
3356
     * @param string $currentAnswer
3357
     * @param array  $myRemindList
3358
     * @param bool   $showPreviousButton
3359
     *
3360
     * @return string
3361
     */
3362
    public function show_button(
3363
        $question_id,
3364
        $questionNum,
3365
        $questions_in_media = [],
3366
        $currentAnswer = '',
3367
        $myRemindList = [],
3368
        $showPreviousButton = true
3369
    ) {
3370
        global $safe_lp_id, $safe_lp_item_id, $safe_lp_item_view_id;
3371
        $nbrQuestions = $this->countQuestionsInExercise();
3372
        $buttonList = [];
3373
        $html = $label = '';
3374
        $hotspotGet = isset($_POST['hotspot']) ? Security::remove_XSS($_POST['hotspot']) : null;
3375
3376
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]) &&
3377
            $this->type == ONE_PER_PAGE
3378
        ) {
3379
            $urlTitle = get_lang('ContinueTest');
3380
            if ($questionNum == count($this->questionList)) {
3381
                $urlTitle = get_lang('EndTest');
3382
            }
3383
3384
            $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit_modal.php?'.api_get_cidreq();
3385
            $url .= '&'.http_build_query([
3386
                'learnpath_id' => $safe_lp_id,
3387
                'learnpath_item_id' => $safe_lp_item_id,
3388
                'learnpath_item_view_id' => $safe_lp_item_view_id,
3389
                'hotspot' => $hotspotGet,
3390
                'nbrQuestions' => $nbrQuestions,
3391
                'num' => $questionNum,
3392
                'exerciseType' => $this->type,
3393
                'exerciseId' => $this->id,
3394
                'reminder' => empty($myRemindList) ? null : 2,
3395
                'tryagain' => isset($_REQUEST['tryagain']) && 1 === (int) $_REQUEST['tryagain'] ? 1 : 0,
3396
            ]);
3397
3398
            $params = [
3399
                'class' => 'ajax btn btn-default no-close-button',
3400
                'data-title' => Security::remove_XSS(get_lang('Comment')),
3401
                'data-size' => 'md',
3402
                'id' => "button_$question_id",
3403
            ];
3404
3405
            if ($this->getFeedbackType() === EXERCISE_FEEDBACK_TYPE_POPUP) {
3406
                $params['data-block-closing'] = 'true';
3407
                $params['class'] .= ' no-header ';
3408
            }
3409
3410
            $html .= Display::url($urlTitle, $url, $params);
3411
            $html .= '<br />';
3412
3413
            return $html;
3414
        }
3415
3416
        if (!api_is_allowed_to_session_edit()) {
3417
            return '';
3418
        }
3419
3420
        $isReviewingAnswers = isset($_REQUEST['reminder']) && 2 === (int) $_REQUEST['reminder'];
3421
        $endReminderValue = false;
3422
        if (!empty($myRemindList) && $isReviewingAnswers) {
3423
            $endValue = end($myRemindList);
3424
            if ($endValue == $question_id) {
3425
                $endReminderValue = true;
3426
            }
3427
        }
3428
        $endTest = false;
3429
        if ($this->type == ALL_ON_ONE_PAGE || $nbrQuestions == $questionNum || $endReminderValue) {
3430
            if ($this->review_answers) {
3431
                $label = get_lang('ReviewQuestions');
3432
                $class = 'btn btn-success';
3433
            } else {
3434
                $endTest = true;
3435
                $label = get_lang('EndTest');
3436
                $class = 'btn btn-warning';
3437
            }
3438
        } else {
3439
            $label = get_lang('NextQuestion');
3440
            $class = 'btn btn-primary';
3441
        }
3442
        // used to select it with jquery
3443
        $class .= ' question-validate-btn';
3444
        if ($this->type == ONE_PER_PAGE) {
3445
            if ($questionNum != 1 && $this->showPreviousButton()) {
3446
                $prev_question = $questionNum - 2;
3447
                $showPreview = true;
3448
                if (!empty($myRemindList) && $isReviewingAnswers) {
3449
                    $beforeId = null;
3450
                    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...
3451
                        if (isset($myRemindList[$i]) && $myRemindList[$i] == $question_id) {
3452
                            $beforeId = isset($myRemindList[$i - 1]) ? $myRemindList[$i - 1] : null;
3453
                            break;
3454
                        }
3455
                    }
3456
3457
                    if (empty($beforeId)) {
3458
                        $showPreview = false;
3459
                    } else {
3460
                        $num = 0;
3461
                        foreach ($this->questionList as $originalQuestionId) {
3462
                            if ($originalQuestionId == $beforeId) {
3463
                                break;
3464
                            }
3465
                            $num++;
3466
                        }
3467
                        $prev_question = $num;
3468
                    }
3469
                }
3470
3471
                if ($showPreviousButton && $showPreview && 0 === $this->getPreventBackwards()) {
3472
                    $buttonList[] = Display::button(
3473
                        'previous_question_and_save',
3474
                        get_lang('PreviousQuestion'),
3475
                        [
3476
                            'type' => 'button',
3477
                            'class' => 'btn btn-default',
3478
                            'data-prev' => $prev_question,
3479
                            'data-question' => $question_id,
3480
                        ]
3481
                    );
3482
                }
3483
            }
3484
3485
            // Next question
3486
            if (!empty($questions_in_media)) {
3487
                $buttonList[] = Display::button(
3488
                    'save_question_list',
3489
                    $label,
3490
                    [
3491
                        'type' => 'button',
3492
                        'class' => $class,
3493
                        'data-list' => implode(",", $questions_in_media),
3494
                    ]
3495
                );
3496
            } else {
3497
                $attributes = ['type' => 'button', 'class' => $class, 'data-question' => $question_id];
3498
                $name = 'save_now';
3499
                if ($endTest && api_get_configuration_value('quiz_check_all_answers_before_end_test')) {
3500
                    $name = 'check_answers';
3501
                }
3502
                $buttonList[] = Display::button(
3503
                    $name,
3504
                    $label,
3505
                    $attributes
3506
                );
3507
            }
3508
            $buttonList[] = '<span id="save_for_now_'.$question_id.'" class="exercise_save_mini_message"></span>';
3509
3510
            $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3511
3512
            return $html;
3513
        }
3514
3515
        if ($this->review_answers) {
3516
            $all_label = get_lang('ReviewQuestions');
3517
            $class = 'btn btn-success';
3518
        } else {
3519
            $all_label = get_lang('EndTest');
3520
            $class = 'btn btn-warning';
3521
        }
3522
        // used to select it with jquery
3523
        $class .= ' question-validate-btn';
3524
        $buttonList[] = Display::button(
3525
            'validate_all',
3526
            $all_label,
3527
            ['type' => 'button', 'class' => $class]
3528
        );
3529
        $buttonList[] = Display::span(null, ['id' => 'save_all_response']);
3530
        $html .= implode(PHP_EOL, $buttonList).PHP_EOL;
3531
3532
        return $html;
3533
    }
3534
3535
    /**
3536
     * @param int    $timeLeft in seconds
3537
     * @param string $url
3538
     *
3539
     * @return string
3540
     */
3541
    public function showSimpleTimeControl($timeLeft, $url = '')
3542
    {
3543
        $timeLeft = (int) $timeLeft;
3544
3545
        return "<script>
3546
            function openClockWarning() {
3547
                $('#clock_warning').dialog({
3548
                    modal:true,
3549
                    height:320,
3550
                    width:550,
3551
                    closeOnEscape: false,
3552
                    resizable: false,
3553
                    buttons: {
3554
                        '".addslashes(get_lang('Close'))."': function() {
3555
                            $('#clock_warning').dialog('close');
3556
                        }
3557
                    },
3558
                    close: function() {
3559
                        window.location.href = '$url';
3560
                    }
3561
                });
3562
                $('#clock_warning').dialog('open');
3563
                $('#counter_to_redirect').epiclock({
3564
                    mode: $.epiclock.modes.countdown,
3565
                    offset: {seconds: 5},
3566
                    format: 's'
3567
                }).bind('timer', function () {
3568
                    window.location.href = '$url';
3569
                });
3570
            }
3571
3572
            function onExpiredTimeExercise() {
3573
                $('#wrapper-clock').hide();
3574
                $('#expired-message-id').show();
3575
                // Fixes bug #5263
3576
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3577
                openClockWarning();
3578
            }
3579
3580
			$(function() {
3581
				// time in seconds when using minutes there are some seconds lost
3582
                var time_left = parseInt(".$timeLeft.");
3583
                $('#exercise_clock_warning').epiclock({
3584
                    mode: $.epiclock.modes.countdown,
3585
                    offset: {seconds: time_left},
3586
                    format: 'x:i:s',
3587
                    renderer: 'minute'
3588
                }).bind('timer', function () {
3589
                    onExpiredTimeExercise();
3590
                });
3591
	       		$('#submit_save').click(function () {});
3592
	        });
3593
	    </script>";
3594
    }
3595
3596
    /**
3597
     * So the time control will work.
3598
     *
3599
     * @param int    $timeLeft
3600
     * @param string $redirectToUrl
3601
     *
3602
     * @return string
3603
     */
3604
    public function showTimeControlJS($timeLeft, $redirectToUrl = '')
3605
    {
3606
        $timeLeft = (int) $timeLeft;
3607
        $script = 'redirectExerciseToResult();';
3608
        if (ALL_ON_ONE_PAGE == $this->type) {
3609
            $script = "save_now_all('validate');";
3610
        } elseif (ONE_PER_PAGE == $this->type) {
3611
            $script = 'window.quizTimeEnding = true;
3612
                $(\'[name="save_now"]\').trigger(\'click\');';
3613
        }
3614
3615
        $exerciseSubmitRedirect = '';
3616
        if (!empty($redirectToUrl)) {
3617
            $exerciseSubmitRedirect = "window.location = '$redirectToUrl'";
3618
        }
3619
3620
        return "<script>
3621
            function openClockWarning() {
3622
                $('#clock_warning').dialog({
3623
                    modal:true,
3624
                    height:320,
3625
                    width:550,
3626
                    closeOnEscape: false,
3627
                    resizable: false,
3628
                    buttons: {
3629
                        '".addslashes(get_lang('EndTest'))."': function() {
3630
                            $('#clock_warning').dialog('close');
3631
                        }
3632
                    },
3633
                    close: function() {
3634
                        send_form();
3635
                    }
3636
                });
3637
3638
                $('#clock_warning').dialog('open');
3639
                $('#counter_to_redirect').epiclock({
3640
                    mode: $.epiclock.modes.countdown,
3641
                    offset: {seconds: 5},
3642
                    format: 's'
3643
                }).bind('timer', function () {
3644
                    send_form();
3645
                });
3646
            }
3647
3648
            function send_form() {
3649
                if ($('#exercise_form').length) {
3650
                    $script
3651
                } else {
3652
                    $exerciseSubmitRedirect
3653
                    // In exercise_reminder.php
3654
                    final_submit();
3655
                }
3656
            }
3657
3658
            function onExpiredTimeExercise() {
3659
                $('#wrapper-clock').hide();
3660
                $('#expired-message-id').show();
3661
                // Fixes bug #5263
3662
                $('#num_current_id').attr('value', '".$this->selectNbrQuestions()."');
3663
                openClockWarning();
3664
            }
3665
3666
			$(function() {
3667
				// time in seconds when using minutes there are some seconds lost
3668
                var time_left = parseInt(".$timeLeft.");
3669
                $('#exercise_clock_warning').epiclock({
3670
                    mode: $.epiclock.modes.countdown,
3671
                    offset: {seconds: time_left},
3672
                    format: 'x:C:s',
3673
                    renderer: 'minute'
3674
                }).bind('timer', function () {
3675
                    onExpiredTimeExercise();
3676
                });
3677
	       		$('#submit_save').click(function () {});
3678
	        });
3679
	    </script>";
3680
    }
3681
3682
    /**
3683
     * This function was originally found in the exercise_show.php.
3684
     *
3685
     * @param int    $exeId
3686
     * @param int    $questionId
3687
     * @param mixed  $choice                                    the user-selected option
3688
     * @param string $from                                      function is called from 'exercise_show' or
3689
     *                                                          'exercise_result'
3690
     * @param array  $exerciseResultCoordinates                 the hotspot coordinates $hotspot[$question_id] =
3691
     *                                                          coordinates
3692
     * @param bool   $save_results                              save results in the DB or just show the response
3693
     * @param bool   $from_database                             gets information from DB or from the current selection
3694
     * @param bool   $show_result                               show results or not
3695
     * @param int    $propagate_neg
3696
     * @param array  $hotspot_delineation_result
3697
     * @param bool   $showTotalScoreAndUserChoicesInLastAttempt
3698
     * @param bool   $updateResults
3699
     * @param bool   $showHotSpotDelineationTable
3700
     * @param int    $questionDuration                          seconds
3701
     *
3702
     * @todo    reduce parameters of this function
3703
     *
3704
     * @return string html code
3705
     */
3706
    public function manage_answer(
3707
        $exeId,
3708
        $questionId,
3709
        $choice,
3710
        $from = 'exercise_show',
3711
        $exerciseResultCoordinates = [],
3712
        $save_results = true,
3713
        $from_database = false,
3714
        $show_result = true,
3715
        $propagate_neg = 0,
3716
        $hotspot_delineation_result = [],
3717
        $showTotalScoreAndUserChoicesInLastAttempt = true,
3718
        $updateResults = false,
3719
        $showHotSpotDelineationTable = false,
3720
        $questionDuration = 0
3721
    ) {
3722
        $debug = false;
3723
        //needed in order to use in the exercise_attempt() for the time
3724
        global $learnpath_id, $learnpath_item_id;
3725
        require_once api_get_path(LIBRARY_PATH).'geometry.lib.php';
3726
        $em = Database::getManager();
3727
        $feedback_type = $this->getFeedbackType();
3728
        $results_disabled = $this->selectResultsDisabled();
3729
        $questionDuration = (int) $questionDuration;
3730
3731
        if ($debug) {
3732
            error_log("<------ manage_answer ------> ");
3733
            error_log('exe_id: '.$exeId);
3734
            error_log('$from:  '.$from);
3735
            error_log('$save_results: '.intval($save_results));
3736
            error_log('$from_database: '.intval($from_database));
3737
            error_log('$show_result: '.intval($show_result));
3738
            error_log('$propagate_neg: '.$propagate_neg);
3739
            error_log('$exerciseResultCoordinates: '.print_r($exerciseResultCoordinates, 1));
3740
            error_log('$hotspot_delineation_result: '.print_r($hotspot_delineation_result, 1));
3741
            error_log('$learnpath_id: '.$learnpath_id);
3742
            error_log('$learnpath_item_id: '.$learnpath_item_id);
3743
            error_log('$choice: '.print_r($choice, 1));
3744
            error_log('-----------------------------');
3745
        }
3746
3747
        $final_overlap = 0;
3748
        $final_missing = 0;
3749
        $final_excess = 0;
3750
        $overlap_color = 0;
3751
        $missing_color = 0;
3752
        $excess_color = 0;
3753
        $threadhold1 = 0;
3754
        $threadhold2 = 0;
3755
        $threadhold3 = 0;
3756
        $arrques = null;
3757
        $arrans = null;
3758
        $studentChoice = null;
3759
        $expectedAnswer = '';
3760
        $calculatedChoice = '';
3761
        $calculatedStatus = '';
3762
        $questionId = (int) $questionId;
3763
        $exeId = (int) $exeId;
3764
        $TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3765
        $table_ans = Database::get_course_table(TABLE_QUIZ_ANSWER);
3766
        $studentChoiceDegree = null;
3767
3768
        // Creates a temporary Question object
3769
        $course_id = $this->course_id;
3770
        $objQuestionTmp = Question::read($questionId, $this->course);
3771
3772
        if (false === $objQuestionTmp) {
3773
            return false;
3774
        }
3775
3776
        $questionName = $objQuestionTmp->selectTitle();
3777
        $questionWeighting = $objQuestionTmp->selectWeighting();
3778
        $answerType = $objQuestionTmp->selectType();
3779
        $quesId = $objQuestionTmp->selectId();
3780
        $extra = $objQuestionTmp->extra;
3781
        $next = 1; //not for now
3782
        $totalWeighting = 0;
3783
        $totalScore = 0;
3784
3785
        // Extra information of the question
3786
        if ((
3787
            $answerType == MULTIPLE_ANSWER_TRUE_FALSE ||
3788
            $answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY
3789
            )
3790
            && !empty($extra)
3791
        ) {
3792
            $extra = explode(':', $extra);
3793
            // Fixes problems with negatives values using intval
3794
            $true_score = (float) trim($extra[0]);
3795
            $false_score = (float) trim($extra[1]);
3796
            $doubt_score = (float) trim($extra[2]);
3797
        }
3798
3799
        // Construction of the Answer object
3800
        $objAnswerTmp = new Answer($questionId, $course_id);
3801
        $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
3802
3803
        if ($debug) {
3804
            error_log('Count of possible answers: '.$nbrAnswers);
3805
            error_log('$answerType: '.$answerType);
3806
        }
3807
3808
        if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
3809
            $choiceTmp = $choice;
3810
            $choice = isset($choiceTmp['choice']) ? $choiceTmp['choice'] : '';
3811
            $choiceDegreeCertainty = isset($choiceTmp['choiceDegreeCertainty']) ? $choiceTmp['choiceDegreeCertainty'] : '';
3812
        }
3813
3814
        if ($answerType == FREE_ANSWER ||
3815
            $answerType == ORAL_EXPRESSION ||
3816
            $answerType == CALCULATED_ANSWER ||
3817
            $answerType == ANNOTATION
3818
        ) {
3819
            $nbrAnswers = 1;
3820
        }
3821
3822
        $generatedFile = '';
3823
        if ($answerType == ORAL_EXPRESSION) {
3824
            $exe_info = Event::get_exercise_results_by_attempt($exeId);
3825
            $exe_info = isset($exe_info[$exeId]) ? $exe_info[$exeId] : null;
3826
            $objQuestionTmp->initFile(
3827
                api_get_session_id(),
3828
                isset($exe_info['exe_user_id']) ? $exe_info['exe_user_id'] : api_get_user_id(),
3829
                isset($exe_info['exe_exo_id']) ? $exe_info['exe_exo_id'] : $this->id,
3830
                isset($exe_info['exe_id']) ? $exe_info['exe_id'] : $exeId
3831
            );
3832
3833
            // Probably this attempt came in an exercise all question by page
3834
            if ($feedback_type == 0) {
3835
                $objQuestionTmp->replaceWithRealExe($exeId);
3836
            }
3837
            $generatedFile = $objQuestionTmp->getFileUrl();
3838
        }
3839
3840
        $user_answer = '';
3841
        // Get answer list for matching.
3842
        $sql = "SELECT id_auto, id, answer
3843
                FROM $table_ans
3844
                WHERE c_id = $course_id AND question_id = $questionId";
3845
        $res_answer = Database::query($sql);
3846
3847
        $answerMatching = [];
3848
        while ($real_answer = Database::fetch_array($res_answer)) {
3849
            $answerMatching[$real_answer['id_auto']] = $real_answer['answer'];
3850
        }
3851
3852
        // Get first answer needed for global question, no matter the answer shuffle option;
3853
        $firstAnswer = [];
3854
        if ($answerType == MULTIPLE_ANSWER_COMBINATION ||
3855
            $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE
3856
        ) {
3857
            $sql = "SELECT *
3858
                    FROM $table_ans
3859
                    WHERE c_id = $course_id AND question_id = $questionId
3860
                    ORDER BY position
3861
                    LIMIT 1";
3862
            $result = Database::query($sql);
3863
            if (Database::num_rows($result)) {
3864
                $firstAnswer = Database::fetch_array($result);
3865
            }
3866
        }
3867
3868
        $real_answers = [];
3869
        $quiz_question_options = Question::readQuestionOption($questionId, $course_id);
3870
        $organs_at_risk_hit = 0;
3871
        $questionScore = 0;
3872
        $orderedHotSpots = [];
3873
        if ($answerType == HOT_SPOT || $answerType == ANNOTATION) {
3874
            $orderedHotSpots = $em->getRepository('ChamiloCoreBundle:TrackEHotspot')->findBy(
3875
                [
3876
                    'hotspotQuestionId' => $questionId,
3877
                    'cId' => $course_id,
3878
                    'hotspotExeId' => $exeId,
3879
                ],
3880
                ['hotspotAnswerId' => 'ASC']
3881
            );
3882
        }
3883
3884
        if ($debug) {
3885
            error_log('-- Start answer loop --');
3886
        }
3887
3888
        $answerDestination = null;
3889
        $userAnsweredQuestion = false;
3890
        $correctAnswerId = [];
3891
        for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
3892
            $answer = $objAnswerTmp->selectAnswer($answerId);
3893
            $answerComment = $objAnswerTmp->selectComment($answerId);
3894
            $answerCorrect = $objAnswerTmp->isCorrect($answerId);
3895
            $answerWeighting = (float) $objAnswerTmp->selectWeighting($answerId);
3896
            $answerAutoId = $objAnswerTmp->selectAutoId($answerId);
3897
            $answerIid = isset($objAnswerTmp->iid[$answerId]) ? (int) $objAnswerTmp->iid[$answerId] : 0;
3898
3899
            if ($debug) {
3900
                error_log("c_quiz_answer.id_auto: $answerAutoId ");
3901
                error_log("Answer marked as correct in db (0/1)?: $answerCorrect ");
3902
                error_log("answerWeighting: $answerWeighting");
3903
            }
3904
3905
            // Delineation.
3906
            $delineation_cord = $objAnswerTmp->selectHotspotCoordinates(1);
3907
            $answer_delineation_destination = $objAnswerTmp->selectDestination(1);
3908
3909
            switch ($answerType) {
3910
                case UNIQUE_ANSWER:
3911
                case UNIQUE_ANSWER_IMAGE:
3912
                case UNIQUE_ANSWER_NO_OPTION:
3913
                case READING_COMPREHENSION:
3914
                    if ($from_database) {
3915
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3916
                                WHERE
3917
                                    exe_id = $exeId AND
3918
                                    question_id = $questionId";
3919
                        $result = Database::query($sql);
3920
                        $choice = Database::result($result, 0, 'answer');
3921
3922
                        if (false === $userAnsweredQuestion) {
3923
                            $userAnsweredQuestion = !empty($choice);
3924
                        }
3925
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3926
                        if ($studentChoice) {
3927
                            $questionScore += $answerWeighting;
3928
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3929
                            $correctAnswerId[] = $answerId;
3930
                        }
3931
                    } else {
3932
                        $studentChoice = $choice == $answerAutoId ? 1 : 0;
3933
                        if ($studentChoice) {
3934
                            $questionScore += $answerWeighting;
3935
                            $answerDestination = $objAnswerTmp->selectDestination($answerId);
3936
                            $correctAnswerId[] = $answerId;
3937
                        }
3938
                    }
3939
                    break;
3940
                case MULTIPLE_ANSWER_TRUE_FALSE:
3941
                    if ($from_database) {
3942
                        $choice = [];
3943
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
3944
                                WHERE
3945
                                    exe_id = $exeId AND
3946
                                    question_id = ".$questionId;
3947
3948
                        $result = Database::query($sql);
3949
                        while ($row = Database::fetch_array($result)) {
3950
                            $values = explode(':', $row['answer']);
3951
                            $my_answer_id = isset($values[0]) ? $values[0] : '';
3952
                            $option = isset($values[1]) ? $values[1] : '';
3953
                            $choice[$my_answer_id] = $option;
3954
                        }
3955
                        $userAnsweredQuestion = !empty($choice);
3956
                    }
3957
3958
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
3959
                    if (!empty($studentChoice)) {
3960
                        $correctAnswerId[] = $answerAutoId;
3961
                        if ($studentChoice == $answerCorrect) {
3962
                            $questionScore += $true_score;
3963
                        } else {
3964
                            if ($quiz_question_options[$studentChoice]['name'] === "Don't know" ||
3965
                                $quiz_question_options[$studentChoice]['name'] === "DoubtScore"
3966
                            ) {
3967
                                $questionScore += $doubt_score;
3968
                            } else {
3969
                                $questionScore += $false_score;
3970
                            }
3971
                        }
3972
                    } else {
3973
                        // If no result then the user just hit don't know
3974
                        $studentChoice = 3;
3975
                        $questionScore += $doubt_score;
3976
                    }
3977
                    $totalScore = $questionScore;
3978
                    break;
3979
                case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
3980
                    if ($from_database) {
3981
                        $choice = [];
3982
                        $choiceDegreeCertainty = [];
3983
                        $sql = "SELECT answer
3984
                            FROM $TBL_TRACK_ATTEMPT
3985
                            WHERE
3986
                            exe_id = $exeId AND question_id = $questionId";
3987
3988
                        $result = Database::query($sql);
3989
                        while ($row = Database::fetch_array($result)) {
3990
                            $ind = $row['answer'];
3991
                            $values = explode(':', $ind);
3992
                            $myAnswerId = $values[0];
3993
                            $option = $values[1];
3994
                            $percent = $values[2];
3995
                            $choice[$myAnswerId] = $option;
3996
                            $choiceDegreeCertainty[$myAnswerId] = $percent;
3997
                        }
3998
                    }
3999
4000
                    $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
4001
                    $studentChoiceDegree = isset($choiceDegreeCertainty[$answerAutoId]) ? $choiceDegreeCertainty[$answerAutoId] : null;
4002
4003
                    // student score update
4004
                    if (!empty($studentChoice)) {
4005
                        if ($studentChoice == $answerCorrect) {
4006
                            // correct answer and student is Unsure or PrettySur
4007
                            if (isset($quiz_question_options[$studentChoiceDegree]) &&
4008
                                $quiz_question_options[$studentChoiceDegree]['position'] >= 3 &&
4009
                                $quiz_question_options[$studentChoiceDegree]['position'] < 9
4010
                            ) {
4011
                                $questionScore += $true_score;
4012
                            } else {
4013
                                // student ignore correct answer
4014
                                $questionScore += $doubt_score;
4015
                            }
4016
                        } else {
4017
                            // false answer and student is Unsure or PrettySur
4018
                            if ($quiz_question_options[$studentChoiceDegree]['position'] >= 3
4019
                                && $quiz_question_options[$studentChoiceDegree]['position'] < 9) {
4020
                                $questionScore += $false_score;
4021
                            } else {
4022
                                // student ignore correct answer
4023
                                $questionScore += $doubt_score;
4024
                            }
4025
                        }
4026
                    }
4027
                    $totalScore = $questionScore;
4028
                    break;
4029
                case MULTIPLE_ANSWER:
4030
                    if ($from_database) {
4031
                        $choice = [];
4032
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4033
                                WHERE exe_id = $exeId AND question_id = $questionId ";
4034
                        $resultans = Database::query($sql);
4035
                        while ($row = Database::fetch_array($resultans)) {
4036
                            $choice[$row['answer']] = 1;
4037
                        }
4038
4039
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
4040
                        $real_answers[$answerId] = (bool) $studentChoice;
4041
4042
                        if ($studentChoice) {
4043
                            $questionScore += $answerWeighting;
4044
                        }
4045
                    } else {
4046
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
4047
                        $real_answers[$answerId] = (bool) $studentChoice;
4048
4049
                        if (isset($studentChoice)) {
4050
                            $correctAnswerId[] = $answerAutoId;
4051
                            $questionScore += $answerWeighting;
4052
                        }
4053
                    }
4054
                    $totalScore += $answerWeighting;
4055
                    break;
4056
                case GLOBAL_MULTIPLE_ANSWER:
4057
                    if ($from_database) {
4058
                        $choice = [];
4059
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4060
                                WHERE exe_id = $exeId AND question_id = $questionId ";
4061
                        $resultans = Database::query($sql);
4062
                        while ($row = Database::fetch_array($resultans)) {
4063
                            $choice[$row['answer']] = 1;
4064
                        }
4065
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
4066
                        $real_answers[$answerId] = (bool) $studentChoice;
4067
                        if ($studentChoice) {
4068
                            $questionScore += $answerWeighting;
4069
                        }
4070
                    } else {
4071
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
4072
                        if (isset($studentChoice)) {
4073
                            $questionScore += $answerWeighting;
4074
                        }
4075
                        $real_answers[$answerId] = (bool) $studentChoice;
4076
                    }
4077
                    $totalScore += $answerWeighting;
4078
                    if ($debug) {
4079
                        error_log("studentChoice: $studentChoice");
4080
                    }
4081
                    break;
4082
                case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
4083
                    if ($from_database) {
4084
                        $choice = [];
4085
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4086
                                WHERE exe_id = $exeId AND question_id = $questionId";
4087
                        $resultans = Database::query($sql);
4088
                        while ($row = Database::fetch_array($resultans)) {
4089
                            $result = explode(':', $row['answer']);
4090
                            if (isset($result[0])) {
4091
                                $my_answer_id = isset($result[0]) ? $result[0] : '';
4092
                                $option = isset($result[1]) ? $result[1] : '';
4093
                                $choice[$my_answer_id] = $option;
4094
                            }
4095
                        }
4096
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
4097
                        $real_answers[$answerId] = false;
4098
                        if ($answerCorrect == $studentChoice) {
4099
                            $real_answers[$answerId] = true;
4100
                        }
4101
                    } else {
4102
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
4103
                        $real_answers[$answerId] = false;
4104
                        if ($answerCorrect == $studentChoice) {
4105
                            $real_answers[$answerId] = true;
4106
                        }
4107
                    }
4108
                    break;
4109
                case MULTIPLE_ANSWER_COMBINATION:
4110
                    if ($from_database) {
4111
                        $choice = [];
4112
                        $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4113
                                WHERE exe_id = $exeId AND question_id = $questionId";
4114
                        $resultans = Database::query($sql);
4115
                        while ($row = Database::fetch_array($resultans)) {
4116
                            $choice[$row['answer']] = 1;
4117
                        }
4118
4119
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
4120
                        if (1 == $answerCorrect) {
4121
                            $real_answers[$answerId] = false;
4122
                            if ($studentChoice) {
4123
                                $real_answers[$answerId] = true;
4124
                            }
4125
                        } else {
4126
                            $real_answers[$answerId] = true;
4127
                            if ($studentChoice) {
4128
                                $real_answers[$answerId] = false;
4129
                            }
4130
                        }
4131
                    } else {
4132
                        $studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
4133
                        if (1 == $answerCorrect) {
4134
                            $real_answers[$answerId] = false;
4135
                            if ($studentChoice) {
4136
                                $real_answers[$answerId] = true;
4137
                            }
4138
                        } else {
4139
                            $real_answers[$answerId] = true;
4140
                            if ($studentChoice) {
4141
                                $real_answers[$answerId] = false;
4142
                            }
4143
                        }
4144
                    }
4145
                    break;
4146
                case FILL_IN_BLANKS:
4147
                    $str = '';
4148
                    $answerFromDatabase = '';
4149
                    if ($from_database) {
4150
                        $sql = "SELECT answer
4151
                                FROM $TBL_TRACK_ATTEMPT
4152
                                WHERE
4153
                                    exe_id = $exeId AND
4154
                                    question_id= $questionId ";
4155
                        $result = Database::query($sql);
4156
                        $str = $answerFromDatabase = Database::result($result, 0, 'answer');
4157
                    }
4158
4159
                    // if ($save_results == false && strpos($answerFromDatabase, 'font color') !== false) {
4160
                    if (false) {
4161
                        // the question is encoded like this
4162
                        // [A] B [C] D [E] F::10,10,10@1
4163
                        // number 1 before the "@" means that is a switchable fill in blank question
4164
                        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4165
                        // means that is a normal fill blank question
4166
                        // first we explode the "::"
4167
                        $pre_array = explode('::', $answer);
4168
4169
                        // is switchable fill blank or not
4170
                        $last = count($pre_array) - 1;
4171
                        $is_set_switchable = explode('@', $pre_array[$last]);
4172
                        $switchable_answer_set = false;
4173
                        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
4174
                            $switchable_answer_set = true;
4175
                        }
4176
                        $answer = '';
4177
                        for ($k = 0; $k < $last; $k++) {
4178
                            $answer .= $pre_array[$k];
4179
                        }
4180
                        // splits weightings that are joined with a comma
4181
                        $answerWeighting = explode(',', $is_set_switchable[0]);
4182
                        // we save the answer because it will be modified
4183
                        $temp = $answer;
4184
                        $answer = '';
4185
                        $j = 0;
4186
                        //initialise answer tags
4187
                        $user_tags = $correct_tags = $real_text = [];
4188
                        // the loop will stop at the end of the text
4189
                        while (1) {
4190
                            // quits the loop if there are no more blanks (detect '[')
4191
                            if ($temp == false || ($pos = api_strpos($temp, '[')) === false) {
4192
                                // adds the end of the text
4193
                                $answer = $temp;
4194
                                $real_text[] = $answer;
4195
                                break; //no more "blanks", quit the loop
4196
                            }
4197
                            // adds the piece of text that is before the blank
4198
                            //and ends with '[' into a general storage array
4199
                            $real_text[] = api_substr($temp, 0, $pos + 1);
4200
                            $answer .= api_substr($temp, 0, $pos + 1);
4201
                            //take the string remaining (after the last "[" we found)
4202
                            $temp = api_substr($temp, $pos + 1);
4203
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4204
                            if (($pos = api_strpos($temp, ']')) === false) {
4205
                                // adds the end of the text
4206
                                $answer .= $temp;
4207
                                break;
4208
                            }
4209
                            if ($from_database) {
4210
                                $str = $answerFromDatabase;
4211
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4212
                                $str = str_replace('\r\n', '', $str);
4213
4214
                                $choice = $arr[1];
4215
                                if (isset($choice[$j])) {
4216
                                    $tmp = api_strrpos($choice[$j], ' / ');
4217
                                    $choice[$j] = api_substr($choice[$j], 0, $tmp);
4218
                                    $choice[$j] = trim($choice[$j]);
4219
                                    // Needed to let characters ' and " to work as part of an answer
4220
                                    $choice[$j] = stripslashes($choice[$j]);
4221
                                } else {
4222
                                    $choice[$j] = null;
4223
                                }
4224
                            } else {
4225
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4226
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4227
                            }
4228
4229
                            $user_tags[] = $choice[$j];
4230
                            // Put the contents of the [] answer tag into correct_tags[]
4231
                            $correct_tags[] = api_substr($temp, 0, $pos);
4232
                            $j++;
4233
                            $temp = api_substr($temp, $pos + 1);
4234
                        }
4235
                        $answer = '';
4236
                        $real_correct_tags = $correct_tags;
4237
                        $chosen_list = [];
4238
4239
                        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...
4240
                            if (0 == $i) {
4241
                                $answer .= $real_text[0];
4242
                            }
4243
                            if (!$switchable_answer_set) {
4244
                                // Needed to parse ' and " characters
4245
                                $user_tags[$i] = stripslashes($user_tags[$i]);
4246
                                if ($correct_tags[$i] == $user_tags[$i]) {
4247
                                    // gives the related weighting to the student
4248
                                    $questionScore += $answerWeighting[$i];
4249
                                    // increments total score
4250
                                    $totalScore += $answerWeighting[$i];
4251
                                    // adds the word in green at the end of the string
4252
                                    $answer .= $correct_tags[$i];
4253
                                } elseif (!empty($user_tags[$i])) {
4254
                                    // else if the word entered by the student IS NOT the same as
4255
                                    // the one defined by the professor
4256
                                    // adds the word in red at the end of the string, and strikes it
4257
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4258
                                } else {
4259
                                    // adds a tabulation if no word has been typed by the student
4260
                                    $answer .= ''; // remove &nbsp; that causes issue
4261
                                }
4262
                            } else {
4263
                                // switchable fill in the blanks
4264
                                if (in_array($user_tags[$i], $correct_tags)) {
4265
                                    $chosen_list[] = $user_tags[$i];
4266
                                    $correct_tags = array_diff($correct_tags, $chosen_list);
4267
                                    // gives the related weighting to the student
4268
                                    $questionScore += $answerWeighting[$i];
4269
                                    // increments total score
4270
                                    $totalScore += $answerWeighting[$i];
4271
                                    // adds the word in green at the end of the string
4272
                                    $answer .= $user_tags[$i];
4273
                                } elseif (!empty($user_tags[$i])) {
4274
                                    // else if the word entered by the student IS NOT the same
4275
                                    // as the one defined by the professor
4276
                                    // adds the word in red at the end of the string, and strikes it
4277
                                    $answer .= '<font color="red"><s>'.$user_tags[$i].'</s></font>';
4278
                                } else {
4279
                                    // adds a tabulation if no word has been typed by the student
4280
                                    $answer .= ''; // remove &nbsp; that causes issue
4281
                                }
4282
                            }
4283
4284
                            // adds the correct word, followed by ] to close the blank
4285
                            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4286
                            if (isset($real_text[$i + 1])) {
4287
                                $answer .= $real_text[$i + 1];
4288
                            }
4289
                        }
4290
                    } else {
4291
                        // insert the student result in the track_e_attempt table, field answer
4292
                        // $answer is the answer like in the c_quiz_answer table for the question
4293
                        // student data are choice[]
4294
                        $listCorrectAnswers = FillBlanks::getAnswerInfo($answer);
4295
                        $switchableAnswerSet = $listCorrectAnswers['switchable'];
4296
                        $answerWeighting = $listCorrectAnswers['weighting'];
4297
                        // user choices is an array $choice
4298
4299
                        // get existing user data in n the BDD
4300
                        if ($from_database) {
4301
                            $listStudentResults = FillBlanks::getAnswerInfo(
4302
                                $answerFromDatabase,
4303
                                true
4304
                            );
4305
                            $choice = $listStudentResults['student_answer'];
4306
                        }
4307
4308
                        // loop other all blanks words
4309
                        if (!$switchableAnswerSet) {
4310
                            // not switchable answer, must be in the same place than teacher order
4311
                            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...
4312
                                $studentAnswer = isset($choice[$i]) ? $choice[$i] : '';
4313
                                $correctAnswer = $listCorrectAnswers['words'][$i];
4314
4315
                                if ($debug) {
4316
                                    error_log("Student answer: $i");
4317
                                    error_log($studentAnswer);
4318
                                }
4319
4320
                                // This value is the user input, not escaped while correct answer is escaped by ckeditor
4321
                                // Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
4322
                                // ENT_QUOTES is used in order to transform ' to &#039;
4323
                                if (!$from_database) {
4324
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4325
                                    if ($debug) {
4326
                                        error_log('Student answer cleaned:');
4327
                                        error_log($studentAnswer);
4328
                                    }
4329
                                }
4330
4331
                                $isAnswerCorrect = 0;
4332
                                if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) {
4333
                                    // gives the related weighting to the student
4334
                                    $questionScore += $answerWeighting[$i];
4335
                                    // increments total score
4336
                                    $totalScore += $answerWeighting[$i];
4337
                                    $isAnswerCorrect = 1;
4338
                                }
4339
                                if ($debug) {
4340
                                    error_log("isAnswerCorrect $i: $isAnswerCorrect");
4341
                                }
4342
4343
                                $studentAnswerToShow = $studentAnswer;
4344
                                $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4345
                                if ($debug) {
4346
                                    error_log("Fill in blank type: $type");
4347
                                }
4348
                                if ($type == FillBlanks::FILL_THE_BLANK_MENU) {
4349
                                    $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4350
                                    if ($studentAnswer != '') {
4351
                                        foreach ($listMenu as $item) {
4352
                                            if (sha1($item) == $studentAnswer) {
4353
                                                $studentAnswerToShow = $item;
4354
                                            }
4355
                                        }
4356
                                    }
4357
                                }
4358
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4359
                                $listCorrectAnswers['student_score'][$i] = $isAnswerCorrect;
4360
                            }
4361
                        } else {
4362
                            // switchable answer
4363
                            $listStudentAnswerTemp = $choice;
4364
                            $listTeacherAnswerTemp = $listCorrectAnswers['words'];
4365
4366
                            // for every teacher answer, check if there is a student answer
4367
                            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...
4368
                                $studentAnswer = trim($listStudentAnswerTemp[$i]);
4369
                                $studentAnswerToShow = $studentAnswer;
4370
4371
                                if (empty($studentAnswer)) {
4372
                                    break;
4373
                                }
4374
4375
                                if ($debug) {
4376
                                    error_log("Student answer: $i");
4377
                                    error_log($studentAnswer);
4378
                                }
4379
4380
                                if (!$from_database) {
4381
                                    $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
4382
                                    if ($debug) {
4383
                                        error_log("Student answer cleaned:");
4384
                                        error_log($studentAnswer);
4385
                                    }
4386
                                }
4387
4388
                                $found = false;
4389
                                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...
4390
                                    $correctAnswer = $listTeacherAnswerTemp[$j];
4391
4392
                                    if (!$found) {
4393
                                        if (FillBlanks::isStudentAnswerGood(
4394
                                            $studentAnswer,
4395
                                            $correctAnswer,
4396
                                            $from_database
4397
                                        )) {
4398
                                            $questionScore += $answerWeighting[$i];
4399
                                            $totalScore += $answerWeighting[$i];
4400
                                            $listTeacherAnswerTemp[$j] = '';
4401
                                            $found = true;
4402
                                        }
4403
                                    }
4404
4405
                                    $type = FillBlanks::getFillTheBlankAnswerType($correctAnswer);
4406
                                    if ($type == FillBlanks::FILL_THE_BLANK_MENU) {
4407
                                        $listMenu = FillBlanks::getFillTheBlankMenuAnswers($correctAnswer, false);
4408
                                        if (!empty($studentAnswer)) {
4409
                                            foreach ($listMenu as $key => $item) {
4410
                                                if ($key == $correctAnswer) {
4411
                                                    $studentAnswerToShow = $item;
4412
                                                    break;
4413
                                                }
4414
                                            }
4415
                                        }
4416
                                    }
4417
                                }
4418
                                $listCorrectAnswers['student_answer'][$i] = $studentAnswerToShow;
4419
                                $listCorrectAnswers['student_score'][$i] = $found ? 1 : 0;
4420
                            }
4421
                        }
4422
                        $answer = FillBlanks::getAnswerInStudentAttempt($listCorrectAnswers);
4423
                    }
4424
                    break;
4425
                case CALCULATED_ANSWER:
4426
                    $calculatedAnswerList = Session::read('calculatedAnswerId');
4427
                    if (!empty($calculatedAnswerList)) {
4428
                        $answer = $objAnswerTmp->selectAnswer($calculatedAnswerList[$questionId]);
4429
                        $preArray = explode('@@', $answer);
4430
                        $last = count($preArray) - 1;
4431
                        $answer = '';
4432
                        for ($k = 0; $k < $last; $k++) {
4433
                            $answer .= $preArray[$k];
4434
                        }
4435
                        $answerWeighting = [$answerWeighting];
4436
                        // we save the answer because it will be modified
4437
                        $temp = $answer;
4438
                        $answer = '';
4439
                        $j = 0;
4440
                        // initialise answer tags
4441
                        $userTags = $correctTags = $realText = [];
4442
                        // the loop will stop at the end of the text
4443
                        while (1) {
4444
                            // quits the loop if there are no more blanks (detect '[')
4445
                            if ($temp == false || ($pos = api_strpos($temp, '[')) === false) {
4446
                                // adds the end of the text
4447
                                $answer = $temp;
4448
                                $realText[] = $answer;
4449
                                break; //no more "blanks", quit the loop
4450
                            }
4451
                            // adds the piece of text that is before the blank
4452
                            // and ends with '[' into a general storage array
4453
                            $realText[] = api_substr($temp, 0, $pos + 1);
4454
                            $answer .= api_substr($temp, 0, $pos + 1);
4455
                            // take the string remaining (after the last "[" we found)
4456
                            $temp = api_substr($temp, $pos + 1);
4457
                            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4458
                            if (false === ($pos = api_strpos($temp, ']'))) {
4459
                                // adds the end of the text
4460
                                $answer .= $temp;
4461
                                break;
4462
                            }
4463
4464
                            if ($from_database) {
4465
                                $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4466
                                        WHERE
4467
                                            exe_id = $exeId AND
4468
                                            question_id = $questionId ";
4469
                                $result = Database::query($sql);
4470
                                $str = Database::result($result, 0, 'answer');
4471
                                api_preg_match_all('#\[([^[]*)\]#', $str, $arr);
4472
                                $str = str_replace('\r\n', '', $str);
4473
                                $choice = $arr[1];
4474
                                if (isset($choice[$j])) {
4475
                                    $tmp = api_strrpos($choice[$j], ' / ');
4476
                                    if ($tmp) {
4477
                                        $choice[$j] = api_substr($choice[$j], 0, $tmp);
4478
                                    } else {
4479
                                        $tmp = ltrim($tmp, '[');
4480
                                        $tmp = rtrim($tmp, ']');
4481
                                    }
4482
                                    $choice[$j] = trim($choice[$j]);
4483
                                    // Needed to let characters ' and " to work as part of an answer
4484
                                    $choice[$j] = stripslashes($choice[$j]);
4485
                                } else {
4486
                                    $choice[$j] = null;
4487
                                }
4488
                            } else {
4489
                                // This value is the user input not escaped while correct answer is escaped by ckeditor
4490
                                $choice[$j] = api_htmlentities(trim($choice[$j]));
4491
                            }
4492
                            $userTags[] = $choice[$j];
4493
                            // put the contents of the [] answer tag into correct_tags[]
4494
                            $correctTags[] = api_substr($temp, 0, $pos);
4495
                            $j++;
4496
                            $temp = api_substr($temp, $pos + 1);
4497
                        }
4498
                        $answer = '';
4499
                        $realCorrectTags = $correctTags;
4500
                        $calculatedStatus = Display::label(get_lang('Incorrect'), 'danger');
4501
                        $expectedAnswer = '';
4502
                        $calculatedChoice = '';
4503
4504
                        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...
4505
                            if (0 == $i) {
4506
                                $answer .= $realText[0];
4507
                            }
4508
                            // Needed to parse ' and " characters
4509
                            $userTags[$i] = stripslashes($userTags[$i]);
4510
                            if ($correctTags[$i] == $userTags[$i]) {
4511
                                // gives the related weighting to the student
4512
                                $questionScore += $answerWeighting[$i];
4513
                                // increments total score
4514
                                $totalScore += $answerWeighting[$i];
4515
                                // adds the word in green at the end of the string
4516
                                $answer .= $correctTags[$i];
4517
                                $calculatedChoice = $correctTags[$i];
4518
                            } elseif (!empty($userTags[$i])) {
4519
                                // else if the word entered by the student IS NOT the same as
4520
                                // the one defined by the professor
4521
                                // adds the word in red at the end of the string, and strikes it
4522
                                $answer .= '<font color="red"><s>'.$userTags[$i].'</s></font>';
4523
                                $calculatedChoice = $userTags[$i];
4524
                            } else {
4525
                                // adds a tabulation if no word has been typed by the student
4526
                                $answer .= ''; // remove &nbsp; that causes issue
4527
                            }
4528
                            // adds the correct word, followed by ] to close the blank
4529
                            if (EXERCISE_FEEDBACK_TYPE_EXAM != $this->results_disabled) {
4530
                                $answer .= ' / <font color="green"><b>'.$realCorrectTags[$i].'</b></font>';
4531
                                $calculatedStatus = Display::label(get_lang('Correct'), 'success');
4532
                                $expectedAnswer = $realCorrectTags[$i];
4533
                            }
4534
                            $answer .= ']';
4535
                            if (isset($realText[$i + 1])) {
4536
                                $answer .= $realText[$i + 1];
4537
                            }
4538
                        }
4539
                    } else {
4540
                        if ($from_database) {
4541
                            $sql = "SELECT *
4542
                                    FROM $TBL_TRACK_ATTEMPT
4543
                                    WHERE
4544
                                        exe_id = $exeId AND
4545
                                        question_id = $questionId ";
4546
                            $result = Database::query($sql);
4547
                            $resultData = Database::fetch_array($result, 'ASSOC');
4548
                            $answer = $resultData['answer'];
4549
                            $questionScore = $resultData['marks'];
4550
                        }
4551
                    }
4552
                    break;
4553
                case FREE_ANSWER:
4554
                    if ($from_database) {
4555
                        $sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
4556
                                 WHERE
4557
                                    exe_id = $exeId AND
4558
                                    question_id= ".$questionId;
4559
                        $result = Database::query($sql);
4560
                        $data = Database::fetch_array($result);
4561
                        $choice = '';
4562
                        $questionScore = 0;
4563
                        if ($data) {
4564
                            $choice = $data['answer'];
4565
                            $questionScore = $data['marks'];
4566
                        }
4567
                        $choice = str_replace('\r\n', '', $choice);
4568
                        $choice = stripslashes($choice);
4569
                        if (-1 == $questionScore) {
4570
                            $totalScore += 0;
4571
                        } else {
4572
                            $totalScore += $questionScore;
4573
                        }
4574
                        if ('' == $questionScore) {
4575
                            $questionScore = 0;
4576
                        }
4577
                        $arrques = $questionName;
4578
                        $arrans = $choice;
4579
                    } else {
4580
                        $studentChoice = $choice;
4581
                        if ($studentChoice) {
4582
                            //Fixing negative puntation see #2193
4583
                            $questionScore = 0;
4584
                            $totalScore += 0;
4585
                        }
4586
                    }
4587
                    break;
4588
                case ORAL_EXPRESSION:
4589
                    if ($from_database) {
4590
                        $query = "SELECT answer, marks
4591
                                  FROM $TBL_TRACK_ATTEMPT
4592
                                  WHERE
4593
                                        exe_id = $exeId AND
4594
                                        question_id = $questionId
4595
                                 ";
4596
                        $resq = Database::query($query);
4597
                        $row = Database::fetch_assoc($resq);
4598
4599
                        $choice = [
4600
                            'answer' => '',
4601
                            'marks' => 0,
4602
                        ];
4603
                        $questionScore = 0;
4604
4605
                        if (is_array($row)) {
4606
                            $choice = $row['answer'];
4607
                            $choice = str_replace('\r\n', '', $choice);
4608
                            $choice = stripslashes($choice);
4609
                            $questionScore = $row['marks'];
4610
                        }
4611
4612
                        if ($questionScore == -1) {
4613
                            $totalScore += 0;
4614
                        } else {
4615
                            $totalScore += $questionScore;
4616
                        }
4617
                        $arrques = $questionName;
4618
                        $arrans = $choice;
4619
                    } else {
4620
                        $studentChoice = $choice;
4621
                        if ($studentChoice) {
4622
                            //Fixing negative puntation see #2193
4623
                            $questionScore = 0;
4624
                            $totalScore += 0;
4625
                        }
4626
                    }
4627
                    break;
4628
                case DRAGGABLE:
4629
                case MATCHING_DRAGGABLE:
4630
                case MATCHING:
4631
                    if ($from_database) {
4632
                        $sql = "SELECT id, answer, id_auto
4633
                                FROM $table_ans
4634
                                WHERE
4635
                                    c_id = $course_id AND
4636
                                    question_id = $questionId AND
4637
                                    correct = 0
4638
                                ";
4639
                        $result = Database::query($sql);
4640
                        // Getting the real answer
4641
                        $real_list = [];
4642
                        while ($realAnswer = Database::fetch_array($result)) {
4643
                            $real_list[$realAnswer['id_auto']] = $realAnswer['answer'];
4644
                        }
4645
4646
                        $orderBy = ' ORDER BY id_auto ';
4647
                        if (DRAGGABLE == $answerType) {
4648
                            $orderBy = ' ORDER BY correct ';
4649
                        }
4650
4651
                        $sql = "SELECT id, answer, correct, id_auto, ponderation
4652
                                FROM $table_ans
4653
                                WHERE
4654
                                    c_id = $course_id AND
4655
                                    question_id = $questionId AND
4656
                                    correct <> 0
4657
                                $orderBy";
4658
                        $result = Database::query($sql);
4659
                        $options = [];
4660
                        $correctAnswers = [];
4661
                        while ($row = Database::fetch_array($result, 'ASSOC')) {
4662
                            $options[] = $row;
4663
                            $correctAnswers[$row['correct']] = $row['answer'];
4664
                        }
4665
4666
                        $questionScore = 0;
4667
                        $counterAnswer = 1;
4668
                        foreach ($options as $a_answers) {
4669
                            $i_answer_id = $a_answers['id']; //3
4670
                            $s_answer_label = $a_answers['answer']; // your daddy - your mother
4671
                            $i_answer_correct_answer = $a_answers['correct']; //1 - 2
4672
                            $i_answer_id_auto = $a_answers['id_auto']; // 3 - 4
4673
4674
                            $sql = "SELECT answer FROM $TBL_TRACK_ATTEMPT
4675
                                    WHERE
4676
                                        exe_id = '$exeId' AND
4677
                                        question_id = '$questionId' AND
4678
                                        position = '$i_answer_id_auto'";
4679
                            $result = Database::query($sql);
4680
                            $s_user_answer = 0;
4681
                            if (Database::num_rows($result) > 0) {
4682
                                //  rich - good looking
4683
                                $s_user_answer = Database::result($result, 0, 0);
4684
                            }
4685
                            $i_answerWeighting = $a_answers['ponderation'];
4686
                            $user_answer = '';
4687
                            $status = Display::label(get_lang('Incorrect'), 'danger');
4688
4689
                            if (!empty($s_user_answer)) {
4690
                                if (DRAGGABLE == $answerType) {
4691
                                    if ($s_user_answer == $i_answer_correct_answer) {
4692
                                        $questionScore += $i_answerWeighting;
4693
                                        $totalScore += $i_answerWeighting;
4694
                                        $user_answer = Display::label(get_lang('Correct'), 'success');
4695
                                        if ($this->showExpectedChoice() && !empty($i_answer_id_auto)) {
4696
                                            $user_answer = $answerMatching[$i_answer_id_auto];
4697
                                        }
4698
                                        $status = Display::label(get_lang('Correct'), 'success');
4699
                                    } else {
4700
                                        $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4701
                                        if ($this->showExpectedChoice() && !empty($s_user_answer)) {
4702
                                            /*$data = $options[$real_list[$s_user_answer] - 1];
4703
                                            $user_answer = $data['answer'];*/
4704
                                            $user_answer = $correctAnswers[$s_user_answer] ?? '';
4705
                                        }
4706
                                    }
4707
                                } else {
4708
                                    if ($s_user_answer == $i_answer_correct_answer) {
4709
                                        $questionScore += $i_answerWeighting;
4710
                                        $totalScore += $i_answerWeighting;
4711
                                        $status = Display::label(get_lang('Correct'), 'success');
4712
                                        // Try with id
4713
                                        if (isset($real_list[$i_answer_id])) {
4714
                                            $user_answer = Display::span(
4715
                                                $real_list[$i_answer_id],
4716
                                                ['style' => 'color: #008000; font-weight: bold;']
4717
                                            );
4718
                                        }
4719
4720
                                        // Try with $i_answer_id_auto
4721
                                        if (empty($user_answer)) {
4722
                                            if (isset($real_list[$i_answer_id_auto])) {
4723
                                                $user_answer = Display::span(
4724
                                                    $real_list[$i_answer_id_auto],
4725
                                                    ['style' => 'color: #008000; font-weight: bold;']
4726
                                                );
4727
                                            }
4728
                                        }
4729
4730
                                        if (isset($real_list[$i_answer_correct_answer])) {
4731
                                            $user_answer = Display::span(
4732
                                                $real_list[$i_answer_correct_answer],
4733
                                                ['style' => 'color: #008000; font-weight: bold;']
4734
                                            );
4735
                                        }
4736
                                    } else {
4737
                                        $user_answer = Display::span(
4738
                                            $real_list[$s_user_answer],
4739
                                            ['style' => 'color: #FF0000; text-decoration: line-through;']
4740
                                        );
4741
                                        if ($this->showExpectedChoice()) {
4742
                                            if (isset($real_list[$s_user_answer])) {
4743
                                                $user_answer = Display::span($real_list[$s_user_answer]);
4744
                                            }
4745
                                        }
4746
                                    }
4747
                                }
4748
                            } elseif (DRAGGABLE == $answerType) {
4749
                                $user_answer = Display::label(get_lang('Incorrect'), 'danger');
4750
                                if ($this->showExpectedChoice()) {
4751
                                    $user_answer = '';
4752
                                }
4753
                            } else {
4754
                                $user_answer = Display::span(
4755
                                    get_lang('Incorrect').' &nbsp;',
4756
                                    ['style' => 'color: #FF0000; text-decoration: line-through;']
4757
                                );
4758
                                if ($this->showExpectedChoice()) {
4759
                                    $user_answer = '';
4760
                                }
4761
                            }
4762
4763
                            if ($show_result) {
4764
                                if (false === $this->showExpectedChoice() &&
4765
                                    false === $showTotalScoreAndUserChoicesInLastAttempt
4766
                                ) {
4767
                                    $user_answer = '';
4768
                                }
4769
                                switch ($answerType) {
4770
                                    case MATCHING:
4771
                                    case MATCHING_DRAGGABLE:
4772
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4773
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4774
                                                break;
4775
                                            }
4776
                                        }
4777
4778
                                        echo '<tr>';
4779
                                        if (!in_array(
4780
                                            $this->results_disabled,
4781
                                            [
4782
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4783
                                            ]
4784
                                        )
4785
                                        ) {
4786
                                            echo '<td>'.$s_answer_label.'</td>';
4787
                                            echo '<td>'.$user_answer.'</td>';
4788
                                        } else {
4789
                                            echo '<td>'.$s_answer_label.'</td>';
4790
                                            $status = Display::label(get_lang('Correct'), 'success');
4791
                                        }
4792
4793
                                        if ($this->showExpectedChoice()) {
4794
                                            if ($this->showExpectedChoiceColumn()) {
4795
                                                echo '<td>';
4796
                                                if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4797
                                                    if (isset($real_list[$i_answer_correct_answer]) &&
4798
                                                        $showTotalScoreAndUserChoicesInLastAttempt == true
4799
                                                    ) {
4800
                                                        echo Display::span(
4801
                                                            $real_list[$i_answer_correct_answer]
4802
                                                        );
4803
                                                    }
4804
                                                }
4805
                                                echo '</td>';
4806
                                            }
4807
                                            echo '<td>'.$status.'</td>';
4808
                                        } else {
4809
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4810
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4811
                                                    $showTotalScoreAndUserChoicesInLastAttempt === true
4812
                                                ) {
4813
                                                    if ($this->showExpectedChoiceColumn()) {
4814
                                                        echo '<td>';
4815
                                                        echo Display::span(
4816
                                                            $real_list[$i_answer_correct_answer],
4817
                                                            ['style' => 'color: #008000; font-weight: bold;']
4818
                                                        );
4819
                                                        echo '</td>';
4820
                                                    }
4821
                                                }
4822
                                            }
4823
                                        }
4824
                                        echo '</tr>';
4825
                                        break;
4826
                                    case DRAGGABLE:
4827
                                        if (false == $showTotalScoreAndUserChoicesInLastAttempt) {
4828
                                            $s_answer_label = '';
4829
                                        }
4830
                                        if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
4831
                                            if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
4832
                                                break;
4833
                                            }
4834
                                        }
4835
4836
                                        echo '<tr>';
4837
                                        if ($this->showExpectedChoice()) {
4838
                                            if (!in_array($this->results_disabled, [
4839
                                                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4840
                                                //RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4841
                                            ])
4842
                                            ) {
4843
                                                echo '<td>'.$user_answer.'</td>';
4844
                                            } else {
4845
                                                $status = Display::label(get_lang('Correct'), 'success');
4846
                                            }
4847
                                            echo '<td>'.$s_answer_label.'</td>';
4848
                                            echo '<td>'.$status.'</td>';
4849
                                        } else {
4850
                                            echo '<td>'.$s_answer_label.'</td>';
4851
                                            echo '<td>'.$user_answer.'</td>';
4852
                                            echo '<td>';
4853
                                            if (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
4854
                                                if (isset($real_list[$i_answer_correct_answer]) &&
4855
                                                    $showTotalScoreAndUserChoicesInLastAttempt === true
4856
                                                ) {
4857
                                                    echo Display::span(
4858
                                                        $real_list[$i_answer_correct_answer],
4859
                                                        ['style' => 'color: #008000; font-weight: bold;']
4860
                                                    );
4861
                                                }
4862
                                            }
4863
                                            echo '</td>';
4864
                                        }
4865
                                        echo '</tr>';
4866
                                        break;
4867
                                }
4868
                            }
4869
                            $counterAnswer++;
4870
                        }
4871
                        break 2; // break the switch and the "for" condition
4872
                    } else {
4873
                        if ($answerCorrect) {
4874
                            if (isset($choice[$answerAutoId]) &&
4875
                                $answerCorrect == $choice[$answerAutoId]
4876
                            ) {
4877
                                $correctAnswerId[] = $answerAutoId;
4878
                                $questionScore += $answerWeighting;
4879
                                $totalScore += $answerWeighting;
4880
                                $user_answer = Display::span($answerMatching[$choice[$answerAutoId]]);
4881
                            } else {
4882
                                if (isset($answerMatching[$choice[$answerAutoId]])) {
4883
                                    $user_answer = Display::span(
4884
                                        $answerMatching[$choice[$answerAutoId]],
4885
                                        ['style' => 'color: #FF0000; text-decoration: line-through;']
4886
                                    );
4887
                                }
4888
                            }
4889
                            $matching[$answerAutoId] = $choice[$answerAutoId];
4890
                        }
4891
                    }
4892
                    break;
4893
                case HOT_SPOT:
4894
                    if ($from_database) {
4895
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4896
                        // Check auto id
4897
                        $foundAnswerId = $answerAutoId;
4898
                        $sql = "SELECT hotspot_correct
4899
                                FROM $TBL_TRACK_HOTSPOT
4900
                                WHERE
4901
                                    hotspot_exe_id = $exeId AND
4902
                                    hotspot_question_id= $questionId AND
4903
                                    hotspot_answer_id = $answerAutoId
4904
                                ORDER BY hotspot_id ASC";
4905
                        $result = Database::query($sql);
4906
                        if (Database::num_rows($result)) {
4907
                            $studentChoice = Database::result(
4908
                                $result,
4909
                                0,
4910
                                'hotspot_correct'
4911
                            );
4912
4913
                            if ($studentChoice) {
4914
                                $questionScore += $answerWeighting;
4915
                                $totalScore += $answerWeighting;
4916
                            }
4917
                        } else {
4918
                            // If answer.id is different:
4919
                            $sql = "SELECT hotspot_correct
4920
                                FROM $TBL_TRACK_HOTSPOT
4921
                                WHERE
4922
                                    hotspot_exe_id = $exeId AND
4923
                                    hotspot_question_id= $questionId AND
4924
                                    hotspot_answer_id = ".intval($answerId)."
4925
                                ORDER BY hotspot_id ASC";
4926
                            $result = Database::query($sql);
4927
                            $foundAnswerId = $answerId;
4928
                            if (Database::num_rows($result)) {
4929
                                $studentChoice = Database::result(
4930
                                    $result,
4931
                                    0,
4932
                                    'hotspot_correct'
4933
                                );
4934
4935
                                if ($studentChoice) {
4936
                                    $questionScore += $answerWeighting;
4937
                                    $totalScore += $answerWeighting;
4938
                                }
4939
                            } else {
4940
                                // check answer.iid
4941
                                if (!empty($answerIid)) {
4942
                                    $sql = "SELECT hotspot_correct
4943
                                            FROM $TBL_TRACK_HOTSPOT
4944
                                            WHERE
4945
                                                hotspot_exe_id = $exeId AND
4946
                                                hotspot_question_id= $questionId AND
4947
                                                hotspot_answer_id = $answerIid
4948
                                            ORDER BY hotspot_id ASC";
4949
                                    $result = Database::query($sql);
4950
                                    $foundAnswerId = $answerIid;
4951
                                    $studentChoice = Database::result(
4952
                                        $result,
4953
                                        0,
4954
                                        'hotspot_correct'
4955
                                    );
4956
4957
                                    if ($studentChoice) {
4958
                                        $questionScore += $answerWeighting;
4959
                                        $totalScore += $answerWeighting;
4960
                                    }
4961
                                }
4962
                            }
4963
                        }
4964
                    } else {
4965
                        if (!isset($choice[$answerAutoId]) && !isset($choice[$answerIid])) {
4966
                            $choice[$answerAutoId] = 0;
4967
                            $choice[$answerIid] = 0;
4968
                        } else {
4969
                            $studentChoice = $choice[$answerAutoId];
4970
                            if (empty($studentChoice)) {
4971
                                $studentChoice = $choice[$answerIid];
4972
                            }
4973
                            $choiceIsValid = false;
4974
                            if (!empty($studentChoice)) {
4975
                                $hotspotType = $objAnswerTmp->selectHotspotType($answerId);
4976
                                $hotspotCoordinates = $objAnswerTmp->selectHotspotCoordinates($answerId);
4977
                                $choicePoint = Geometry::decodePoint($studentChoice);
4978
4979
                                switch ($hotspotType) {
4980
                                    case 'square':
4981
                                        $hotspotProperties = Geometry::decodeSquare($hotspotCoordinates);
4982
                                        $choiceIsValid = Geometry::pointIsInSquare($hotspotProperties, $choicePoint);
4983
                                        break;
4984
                                    case 'circle':
4985
                                        $hotspotProperties = Geometry::decodeEllipse($hotspotCoordinates);
4986
                                        $choiceIsValid = Geometry::pointIsInEllipse($hotspotProperties, $choicePoint);
4987
                                        break;
4988
                                    case 'poly':
4989
                                        $hotspotProperties = Geometry::decodePolygon($hotspotCoordinates);
4990
                                        $choiceIsValid = Geometry::pointIsInPolygon($hotspotProperties, $choicePoint);
4991
                                        break;
4992
                                }
4993
                            }
4994
4995
                            $choice[$answerAutoId] = 0;
4996
                            if ($choiceIsValid) {
4997
                                $questionScore += $answerWeighting;
4998
                                $totalScore += $answerWeighting;
4999
                                $choice[$answerAutoId] = 1;
5000
                                $choice[$answerIid] = 1;
5001
                            }
5002
                        }
5003
                    }
5004
                    break;
5005
                case HOT_SPOT_ORDER:
5006
                    // @todo never added to chamilo
5007
                    // for hotspot with fixed order
5008
                    $studentChoice = $choice['order'][$answerId];
5009
                    if ($studentChoice == $answerId) {
5010
                        $questionScore += $answerWeighting;
5011
                        $totalScore += $answerWeighting;
5012
                        $studentChoice = true;
5013
                    } else {
5014
                        $studentChoice = false;
5015
                    }
5016
                    break;
5017
                case HOT_SPOT_DELINEATION:
5018
                    // for hotspot with delineation
5019
                    if ($from_database) {
5020
                        // getting the user answer
5021
                        $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
5022
                        $query = "SELECT hotspot_correct, hotspot_coordinate
5023
                                    FROM $TBL_TRACK_HOTSPOT
5024
                                    WHERE
5025
                                        hotspot_exe_id = $exeId AND
5026
                                        hotspot_question_id= $questionId AND
5027
                                        hotspot_answer_id = '1'";
5028
                        // By default we take 1 because it's a delineation
5029
                        $resq = Database::query($query);
5030
                        $row = Database::fetch_array($resq, 'ASSOC');
5031
5032
                        $choice = $row['hotspot_correct'];
5033
                        $user_answer = $row['hotspot_coordinate'];
5034
5035
                        // THIS is very important otherwise the poly_compile will throw an error!!
5036
                        // round-up the coordinates
5037
                        $coords = explode('/', $user_answer);
5038
                        $coords = array_filter($coords);
5039
                        $user_array = '';
5040
                        foreach ($coords as $coord) {
5041
                            [$x, $y] = explode(';', $coord);
5042
                            $user_array .= round($x).';'.round($y).'/';
5043
                        }
5044
                        $user_array = substr($user_array, 0, -1) ?: '';
5045
                    } else {
5046
                        if (!empty($studentChoice)) {
5047
                            $correctAnswerId[] = $answerAutoId;
5048
                            $newquestionList[] = $questionId;
5049
                        }
5050
5051
                        if (1 === $answerId) {
5052
                            $studentChoice = $choice[$answerId];
5053
                            $questionScore += $answerWeighting;
5054
                        }
5055
                        if (isset($_SESSION['exerciseResultCoordinates'][$questionId])) {
5056
                            $user_array = $_SESSION['exerciseResultCoordinates'][$questionId];
5057
                        }
5058
                    }
5059
                    $_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
5060
                    $_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
5061
                    break;
5062
                case ANNOTATION:
5063
                    if ($from_database) {
5064
                        $sql = "SELECT answer, marks
5065
                                FROM $TBL_TRACK_ATTEMPT
5066
                                WHERE
5067
                                  exe_id = $exeId AND
5068
                                  question_id = $questionId ";
5069
                        $resq = Database::query($sql);
5070
                        $data = Database::fetch_array($resq);
5071
5072
                        $questionScore = empty($data['marks']) ? 0 : $data['marks'];
5073
                        $arrques = $questionName;
5074
                        break;
5075
                    }
5076
                    $studentChoice = $choice;
5077
                    if ($studentChoice) {
5078
                        $questionScore = 0;
5079
                    }
5080
                    break;
5081
            }
5082
5083
            if ($show_result) {
5084
                if ('exercise_result' === $from) {
5085
                    // Display answers (if not matching type, or if the answer is correct)
5086
                    if (!in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE]) ||
5087
                        $answerCorrect
5088
                    ) {
5089
                        if (in_array(
5090
                            $answerType,
5091
                            [
5092
                                UNIQUE_ANSWER,
5093
                                UNIQUE_ANSWER_IMAGE,
5094
                                UNIQUE_ANSWER_NO_OPTION,
5095
                                MULTIPLE_ANSWER,
5096
                                MULTIPLE_ANSWER_COMBINATION,
5097
                                GLOBAL_MULTIPLE_ANSWER,
5098
                                READING_COMPREHENSION,
5099
                            ]
5100
                        )) {
5101
                            ExerciseShowFunctions::display_unique_or_multiple_answer(
5102
                                $this,
5103
                                $feedback_type,
5104
                                $answerType,
5105
                                $studentChoice,
5106
                                $answer,
5107
                                $answerComment,
5108
                                $answerCorrect,
5109
                                0,
5110
                                0,
5111
                                0,
5112
                                $results_disabled,
5113
                                $showTotalScoreAndUserChoicesInLastAttempt,
5114
                                $this->export
5115
                            );
5116
                        } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE) {
5117
                            ExerciseShowFunctions::display_multiple_answer_true_false(
5118
                                $this,
5119
                                $feedback_type,
5120
                                $answerType,
5121
                                $studentChoice,
5122
                                $answer,
5123
                                $answerComment,
5124
                                $answerCorrect,
5125
                                0,
5126
                                $questionId,
5127
                                0,
5128
                                $results_disabled,
5129
                                $showTotalScoreAndUserChoicesInLastAttempt
5130
                            );
5131
                        } elseif ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
5132
                            ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5133
                                $this,
5134
                                $feedback_type,
5135
                                $studentChoice,
5136
                                $studentChoiceDegree,
5137
                                $answer,
5138
                                $answerComment,
5139
                                $answerCorrect,
5140
                                $questionId,
5141
                                $results_disabled
5142
                            );
5143
                        } elseif ($answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE) {
5144
                            ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5145
                                $this,
5146
                                $feedback_type,
5147
                                $answerType,
5148
                                $studentChoice,
5149
                                $answer,
5150
                                $answerComment,
5151
                                $answerCorrect,
5152
                                0,
5153
                                0,
5154
                                0,
5155
                                $results_disabled,
5156
                                $showTotalScoreAndUserChoicesInLastAttempt
5157
                            );
5158
                        } elseif ($answerType == FILL_IN_BLANKS) {
5159
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5160
                                $this,
5161
                                $feedback_type,
5162
                                $answer,
5163
                                0,
5164
                                0,
5165
                                $results_disabled,
5166
                                '',
5167
                                $showTotalScoreAndUserChoicesInLastAttempt
5168
                            );
5169
                        } elseif ($answerType == CALCULATED_ANSWER) {
5170
                            ExerciseShowFunctions::display_calculated_answer(
5171
                                $this,
5172
                                $feedback_type,
5173
                                $answer,
5174
                                0,
5175
                                0,
5176
                                $results_disabled,
5177
                                $showTotalScoreAndUserChoicesInLastAttempt,
5178
                                $expectedAnswer,
5179
                                $calculatedChoice,
5180
                                $calculatedStatus
5181
                            );
5182
                        } elseif ($answerType == FREE_ANSWER) {
5183
                            ExerciseShowFunctions::display_free_answer(
5184
                                $feedback_type,
5185
                                $choice,
5186
                                $exeId,
5187
                                $questionId,
5188
                                $questionScore,
5189
                                $results_disabled
5190
                            );
5191
                        } elseif ($answerType == ORAL_EXPRESSION) {
5192
                            // to store the details of open questions in an array to be used in mail
5193
                            /** @var OralExpression $objQuestionTmp */
5194
                            ExerciseShowFunctions::display_oral_expression_answer(
5195
                                $feedback_type,
5196
                                $choice,
5197
                                0,
5198
                                0,
5199
                                $objQuestionTmp->getFileUrl(true),
5200
                                $results_disabled,
5201
                                $questionScore
5202
                            );
5203
                        } elseif ($answerType == HOT_SPOT) {
5204
                            $correctAnswerId = 0;
5205
                            /** @var TrackEHotspot $hotspot */
5206
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5207
                                if ($hotspot->getHotspotAnswerId() == $answerIid) {
5208
                                    break;
5209
                                }
5210
                            }
5211
5212
                            // force to show whether the choice is correct or not
5213
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
5214
                            ExerciseShowFunctions::display_hotspot_answer(
5215
                                $this,
5216
                                $feedback_type,
5217
                                $answerId,
5218
                                $answer,
5219
                                $studentChoice,
5220
                                $answerComment,
5221
                                $results_disabled,
5222
                                $answerId,
5223
                                $showTotalScoreAndUserChoicesInLastAttempt
5224
                            );
5225
                        } elseif ($answerType == HOT_SPOT_ORDER) {
5226
                            ExerciseShowFunctions::display_hotspot_order_answer(
5227
                                $feedback_type,
5228
                                $answerId,
5229
                                $answer,
5230
                                $studentChoice,
5231
                                $answerComment
5232
                            );
5233
                        } elseif ($answerType == HOT_SPOT_DELINEATION) {
5234
                            $user_answer = $_SESSION['exerciseResultCoordinates'][$questionId];
5235
5236
                            // Round-up the coordinates
5237
                            $coords = explode('/', $user_answer);
5238
                            $coords = array_filter($coords);
5239
                            $user_array = '';
5240
                            foreach ($coords as $coord) {
5241
                                if (!empty($coord)) {
5242
                                    $parts = explode(';', $coord);
5243
                                    if (!empty($parts)) {
5244
                                        $user_array .= round($parts[0]).';'.round($parts[1]).'/';
5245
                                    }
5246
                                }
5247
                            }
5248
                            $user_array = substr($user_array, 0, -1) ?: '';
5249
                            if ($next) {
5250
                                $user_answer = $user_array;
5251
                                // We compare only the delineation not the other points
5252
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5253
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5254
5255
                                // Calculating the area
5256
                                $poly_user = convert_coordinates($user_answer, '/');
5257
                                $poly_answer = convert_coordinates($answer_question, '|');
5258
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5259
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5260
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5261
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5262
5263
                                $overlap = $poly_results['both'];
5264
                                $poly_answer_area = $poly_results['s1'];
5265
                                $poly_user_area = $poly_results['s2'];
5266
                                $missing = $poly_results['s1Only'];
5267
                                $excess = $poly_results['s2Only'];
5268
5269
                                // //this is an area in pixels
5270
                                if ($debug > 0) {
5271
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5272
                                }
5273
5274
                                if ($overlap < 1) {
5275
                                    // Shortcut to avoid complicated calculations
5276
                                    $final_overlap = 0;
5277
                                    $final_missing = 100;
5278
                                    $final_excess = 100;
5279
                                } else {
5280
                                    // the final overlap is the percentage of the initial polygon
5281
                                    // that is overlapped by the user's polygon
5282
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5283
                                    if ($debug > 1) {
5284
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap, 0);
5285
                                    }
5286
                                    // the final missing area is the percentage of the initial polygon
5287
                                    // that is not overlapped by the user's polygon
5288
                                    $final_missing = 100 - $final_overlap;
5289
                                    if ($debug > 1) {
5290
                                        error_log(__LINE__.' - Final missing is '.$final_missing, 0);
5291
                                    }
5292
                                    // the final excess area is the percentage of the initial polygon's size
5293
                                    // that is covered by the user's polygon outside of the initial polygon
5294
                                    $final_excess = round((((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100);
5295
                                    if ($debug > 1) {
5296
                                        error_log(__LINE__.' - Final excess is '.$final_excess, 0);
5297
                                    }
5298
                                }
5299
5300
                                // Checking the destination parameters parsing the "@@"
5301
                                $destination_items = explode('@@', $answerDestination);
5302
                                $threadhold_total = $destination_items[0];
5303
                                $threadhold_items = explode(';', $threadhold_total);
5304
                                $threadhold1 = $threadhold_items[0]; // overlap
5305
                                $threadhold2 = $threadhold_items[1]; // excess
5306
                                $threadhold3 = $threadhold_items[2]; // missing
5307
5308
                                // if is delineation
5309
                                if ($answerId === 1) {
5310
                                    //setting colors
5311
                                    if ($final_overlap >= $threadhold1) {
5312
                                        $overlap_color = true;
5313
                                    }
5314
                                    if ($final_excess <= $threadhold2) {
5315
                                        $excess_color = true;
5316
                                    }
5317
                                    if ($final_missing <= $threadhold3) {
5318
                                        $missing_color = true;
5319
                                    }
5320
5321
                                    // if pass
5322
                                    if ($final_overlap >= $threadhold1 &&
5323
                                        $final_missing <= $threadhold3 &&
5324
                                        $final_excess <= $threadhold2
5325
                                    ) {
5326
                                        $next = 1; //go to the oars
5327
                                        $result_comment = get_lang('Acceptable');
5328
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5329
                                    } else {
5330
                                        $next = 0;
5331
                                        $result_comment = get_lang('Unacceptable');
5332
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5333
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5334
                                        // checking the destination parameters parsing the "@@"
5335
                                        $destination_items = explode('@@', $answerDestination);
5336
                                    }
5337
                                } elseif ($answerId > 1) {
5338
                                    if ($objAnswerTmp->selectHotspotType($answerId) == 'noerror') {
5339
                                        if ($debug > 0) {
5340
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5341
                                        }
5342
                                        //type no error shouldn't be treated
5343
                                        $next = 1;
5344
                                        continue;
5345
                                    }
5346
                                    if ($debug > 0) {
5347
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5348
                                    }
5349
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5350
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5351
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5352
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5353
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5354
5355
                                    if ($overlap == false) {
5356
                                        //all good, no overlap
5357
                                        $next = 1;
5358
                                        continue;
5359
                                    } else {
5360
                                        if ($debug > 0) {
5361
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5362
                                        }
5363
                                        $organs_at_risk_hit++;
5364
                                        //show the feedback
5365
                                        $next = 0;
5366
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5367
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5368
5369
                                        $destination_items = explode('@@', $answerDestination);
5370
                                        $try_hotspot = $destination_items[1];
5371
                                        $lp_hotspot = $destination_items[2];
5372
                                        $select_question_hotspot = $destination_items[3];
5373
                                        $url_hotspot = $destination_items[4];
5374
                                    }
5375
                                }
5376
                            } else {
5377
                                // the first delineation feedback
5378
                                if ($debug > 0) {
5379
                                    error_log(__LINE__.' first', 0);
5380
                                }
5381
                            }
5382
                        } elseif (in_array($answerType, [MATCHING, MATCHING_DRAGGABLE])) {
5383
                            echo '<tr>';
5384
                            echo Display::tag('td', $answerMatching[$answerId]);
5385
                            echo Display::tag(
5386
                                'td',
5387
                                "$user_answer / ".Display::tag(
5388
                                    'strong',
5389
                                    $answerMatching[$answerCorrect],
5390
                                    ['style' => 'color: #008000; font-weight: bold;']
5391
                                )
5392
                            );
5393
                            echo '</tr>';
5394
                        } elseif ($answerType == ANNOTATION) {
5395
                            ExerciseShowFunctions::displayAnnotationAnswer(
5396
                                $feedback_type,
5397
                                $exeId,
5398
                                $questionId,
5399
                                $questionScore,
5400
                                $results_disabled
5401
                            );
5402
                        }
5403
                    }
5404
                } else {
5405
                    if ($debug) {
5406
                        error_log('Showing questions $from '.$from);
5407
                    }
5408
5409
                    switch ($answerType) {
5410
                        case UNIQUE_ANSWER:
5411
                        case UNIQUE_ANSWER_IMAGE:
5412
                        case UNIQUE_ANSWER_NO_OPTION:
5413
                        case MULTIPLE_ANSWER:
5414
                        case GLOBAL_MULTIPLE_ANSWER:
5415
                        case MULTIPLE_ANSWER_COMBINATION:
5416
                        case READING_COMPREHENSION:
5417
                            if ($answerId == 1) {
5418
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5419
                                    $this,
5420
                                    $feedback_type,
5421
                                    $answerType,
5422
                                    $studentChoice,
5423
                                    $answer,
5424
                                    $answerComment,
5425
                                    $answerCorrect,
5426
                                    $exeId,
5427
                                    $questionId,
5428
                                    $answerId,
5429
                                    $results_disabled,
5430
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5431
                                    $this->export
5432
                                );
5433
                            } else {
5434
                                ExerciseShowFunctions::display_unique_or_multiple_answer(
5435
                                    $this,
5436
                                    $feedback_type,
5437
                                    $answerType,
5438
                                    $studentChoice,
5439
                                    $answer,
5440
                                    $answerComment,
5441
                                    $answerCorrect,
5442
                                    $exeId,
5443
                                    $questionId,
5444
                                    '',
5445
                                    $results_disabled,
5446
                                    $showTotalScoreAndUserChoicesInLastAttempt,
5447
                                    $this->export
5448
                                );
5449
                            }
5450
                            break;
5451
                        case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
5452
                            if ($answerId == 1) {
5453
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5454
                                    $this,
5455
                                    $feedback_type,
5456
                                    $answerType,
5457
                                    $studentChoice,
5458
                                    $answer,
5459
                                    $answerComment,
5460
                                    $answerCorrect,
5461
                                    $exeId,
5462
                                    $questionId,
5463
                                    $answerId,
5464
                                    $results_disabled,
5465
                                    $showTotalScoreAndUserChoicesInLastAttempt
5466
                                );
5467
                            } else {
5468
                                ExerciseShowFunctions::display_multiple_answer_combination_true_false(
5469
                                    $this,
5470
                                    $feedback_type,
5471
                                    $answerType,
5472
                                    $studentChoice,
5473
                                    $answer,
5474
                                    $answerComment,
5475
                                    $answerCorrect,
5476
                                    $exeId,
5477
                                    $questionId,
5478
                                    '',
5479
                                    $results_disabled,
5480
                                    $showTotalScoreAndUserChoicesInLastAttempt
5481
                                );
5482
                            }
5483
                            break;
5484
                        case MULTIPLE_ANSWER_TRUE_FALSE:
5485
                            if ($answerId == 1) {
5486
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5487
                                    $this,
5488
                                    $feedback_type,
5489
                                    $answerType,
5490
                                    $studentChoice,
5491
                                    $answer,
5492
                                    $answerComment,
5493
                                    $answerCorrect,
5494
                                    $exeId,
5495
                                    $questionId,
5496
                                    $answerId,
5497
                                    $results_disabled,
5498
                                    $showTotalScoreAndUserChoicesInLastAttempt
5499
                                );
5500
                            } else {
5501
                                ExerciseShowFunctions::display_multiple_answer_true_false(
5502
                                    $this,
5503
                                    $feedback_type,
5504
                                    $answerType,
5505
                                    $studentChoice,
5506
                                    $answer,
5507
                                    $answerComment,
5508
                                    $answerCorrect,
5509
                                    $exeId,
5510
                                    $questionId,
5511
                                    '',
5512
                                    $results_disabled,
5513
                                    $showTotalScoreAndUserChoicesInLastAttempt
5514
                                );
5515
                            }
5516
                            break;
5517
                        case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
5518
                            if ($answerId == 1) {
5519
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5520
                                    $this,
5521
                                    $feedback_type,
5522
                                    $studentChoice,
5523
                                    $studentChoiceDegree,
5524
                                    $answer,
5525
                                    $answerComment,
5526
                                    $answerCorrect,
5527
                                    $questionId,
5528
                                    $results_disabled
5529
                                );
5530
                            } else {
5531
                                ExerciseShowFunctions::displayMultipleAnswerTrueFalseDegreeCertainty(
5532
                                    $this,
5533
                                    $feedback_type,
5534
                                    $studentChoice,
5535
                                    $studentChoiceDegree,
5536
                                    $answer,
5537
                                    $answerComment,
5538
                                    $answerCorrect,
5539
                                    $questionId,
5540
                                    $results_disabled
5541
                                );
5542
                            }
5543
                            break;
5544
                        case FILL_IN_BLANKS:
5545
                            ExerciseShowFunctions::display_fill_in_blanks_answer(
5546
                                $this,
5547
                                $feedback_type,
5548
                                $answer,
5549
                                $exeId,
5550
                                $questionId,
5551
                                $results_disabled,
5552
                                $str,
5553
                                $showTotalScoreAndUserChoicesInLastAttempt
5554
                            );
5555
                            break;
5556
                        case CALCULATED_ANSWER:
5557
                            ExerciseShowFunctions::display_calculated_answer(
5558
                                $this,
5559
                                $feedback_type,
5560
                                $answer,
5561
                                $exeId,
5562
                                $questionId,
5563
                                $results_disabled,
5564
                                '',
5565
                                $showTotalScoreAndUserChoicesInLastAttempt
5566
                            );
5567
                            break;
5568
                        case FREE_ANSWER:
5569
                            echo ExerciseShowFunctions::display_free_answer(
5570
                                $feedback_type,
5571
                                $choice,
5572
                                $exeId,
5573
                                $questionId,
5574
                                $questionScore,
5575
                                $results_disabled
5576
                            );
5577
                            break;
5578
                        case ORAL_EXPRESSION:
5579
                            echo '<tr>
5580
                                <td valign="top">'.
5581
                                ExerciseShowFunctions::display_oral_expression_answer(
5582
                                    $feedback_type,
5583
                                    $choice,
5584
                                    $exeId,
5585
                                    $questionId,
5586
                                    $objQuestionTmp->getFileUrl(),
5587
                                    $results_disabled,
5588
                                    $questionScore
5589
                                ).'</td>
5590
                                </tr>
5591
                                </table>';
5592
                            break;
5593
                        case HOT_SPOT:
5594
                            $correctAnswerId = 0;
5595
                            /** @var TrackEHotspot $hotspot */
5596
                            foreach ($orderedHotSpots as $correctAnswerId => $hotspot) {
5597
                                if ($hotspot->getHotspotAnswerId() == $foundAnswerId) {
5598
                                    break;
5599
                                }
5600
                            }
5601
5602
                            ExerciseShowFunctions::display_hotspot_answer(
5603
                                $this,
5604
                                $feedback_type,
5605
                                $answerId,
5606
                                $answer,
5607
                                $studentChoice,
5608
                                $answerComment,
5609
                                $results_disabled,
5610
                                $answerId,
5611
                                $showTotalScoreAndUserChoicesInLastAttempt
5612
                            );
5613
                            break;
5614
                        case HOT_SPOT_DELINEATION:
5615
                            $user_answer = $user_array;
5616
                            if ($next) {
5617
                                $user_answer = $user_array;
5618
                                // we compare only the delineation not the other points
5619
                                $answer_question = $_SESSION['hotspot_coord'][$questionId][1];
5620
                                $answerDestination = $_SESSION['hotspot_dest'][$questionId][1];
5621
5622
                                // calculating the area
5623
                                $poly_user = convert_coordinates($user_answer, '/');
5624
                                $poly_answer = convert_coordinates($answer_question, '|');
5625
                                $max_coord = poly_get_max($poly_user, $poly_answer);
5626
                                $poly_user_compiled = poly_compile($poly_user, $max_coord);
5627
                                $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5628
                                $poly_results = poly_result($poly_answer_compiled, $poly_user_compiled, $max_coord);
5629
5630
                                $overlap = $poly_results['both'];
5631
                                $poly_answer_area = $poly_results['s1'];
5632
                                $poly_user_area = $poly_results['s2'];
5633
                                $missing = $poly_results['s1Only'];
5634
                                $excess = $poly_results['s2Only'];
5635
                                if ($debug > 0) {
5636
                                    error_log(__LINE__.' - Polygons results are '.print_r($poly_results, 1), 0);
5637
                                }
5638
                                if ($overlap < 1) {
5639
                                    //shortcut to avoid complicated calculations
5640
                                    $final_overlap = 0;
5641
                                    $final_missing = 100;
5642
                                    $final_excess = 100;
5643
                                } else {
5644
                                    // the final overlap is the percentage of the initial polygon
5645
                                    // that is overlapped by the user's polygon
5646
                                    $final_overlap = round(((float) $overlap / (float) $poly_answer_area) * 100);
5647
5648
                                    // the final missing area is the percentage of the initial polygon that
5649
                                    // is not overlapped by the user's polygon
5650
                                    $final_missing = 100 - $final_overlap;
5651
                                    // the final excess area is the percentage of the initial polygon's size that is
5652
                                    // covered by the user's polygon outside of the initial polygon
5653
                                    $final_excess = round((((float) $poly_user_area - (float) $overlap) / (float) $poly_answer_area) * 100);
5654
5655
                                    if ($debug > 1) {
5656
                                        error_log(__LINE__.' - Final overlap is '.$final_overlap);
5657
                                        error_log(__LINE__.' - Final excess is '.$final_excess);
5658
                                        error_log(__LINE__.' - Final missing is '.$final_missing);
5659
                                    }
5660
                                }
5661
5662
                                // Checking the destination parameters parsing the "@@"
5663
                                $destination_items = explode('@@', $answerDestination);
5664
                                $threadhold_total = $destination_items[0];
5665
                                $threadhold_items = explode(';', $threadhold_total);
5666
                                $threadhold1 = $threadhold_items[0]; // overlap
5667
                                $threadhold2 = $threadhold_items[1]; // excess
5668
                                $threadhold3 = $threadhold_items[2]; //missing
5669
                                // if is delineation
5670
                                if ($answerId === 1) {
5671
                                    //setting colors
5672
                                    if ($final_overlap >= $threadhold1) {
5673
                                        $overlap_color = true;
5674
                                    }
5675
                                    if ($final_excess <= $threadhold2) {
5676
                                        $excess_color = true;
5677
                                    }
5678
                                    if ($final_missing <= $threadhold3) {
5679
                                        $missing_color = true;
5680
                                    }
5681
5682
                                    // if pass
5683
                                    if ($final_overlap >= $threadhold1 &&
5684
                                        $final_missing <= $threadhold3 &&
5685
                                        $final_excess <= $threadhold2
5686
                                    ) {
5687
                                        $next = 1; //go to the oars
5688
                                        $result_comment = get_lang('Acceptable');
5689
                                        $final_answer = 1; // do not update with  update_exercise_attempt
5690
                                    } else {
5691
                                        $next = 0;
5692
                                        $result_comment = get_lang('Unacceptable');
5693
                                        $comment = $answerDestination = $objAnswerTmp->selectComment(1);
5694
                                        $answerDestination = $objAnswerTmp->selectDestination(1);
5695
                                        //checking the destination parameters parsing the "@@"
5696
                                        $destination_items = explode('@@', $answerDestination);
5697
                                    }
5698
                                } elseif ($answerId > 1) {
5699
                                    if ($objAnswerTmp->selectHotspotType($answerId) === 'noerror') {
5700
                                        if ($debug > 0) {
5701
                                            error_log(__LINE__.' - answerId is of type noerror', 0);
5702
                                        }
5703
                                        //type no error shouldn't be treated
5704
                                        $next = 1;
5705
                                        break;
5706
                                    }
5707
                                    if ($debug > 0) {
5708
                                        error_log(__LINE__.' - answerId is >1 so we\'re probably in OAR', 0);
5709
                                    }
5710
                                    $delineation_cord = $objAnswerTmp->selectHotspotCoordinates($answerId);
5711
                                    $poly_answer = convert_coordinates($delineation_cord, '|');
5712
                                    $max_coord = poly_get_max($poly_user, $poly_answer);
5713
                                    $poly_answer_compiled = poly_compile($poly_answer, $max_coord);
5714
                                    $overlap = poly_touch($poly_user_compiled, $poly_answer_compiled, $max_coord);
5715
5716
                                    if ($overlap == false) {
5717
                                        //all good, no overlap
5718
                                        $next = 1;
5719
                                        break;
5720
                                    } else {
5721
                                        if ($debug > 0) {
5722
                                            error_log(__LINE__.' - Overlap is '.$overlap.': OAR hit', 0);
5723
                                        }
5724
                                        $organs_at_risk_hit++;
5725
                                        //show the feedback
5726
                                        $next = 0;
5727
                                        $comment = $answerDestination = $objAnswerTmp->selectComment($answerId);
5728
                                        $answerDestination = $objAnswerTmp->selectDestination($answerId);
5729
5730
                                        $destination_items = explode('@@', $answerDestination);
5731
                                        $try_hotspot = $destination_items[1];
5732
                                        $lp_hotspot = $destination_items[2];
5733
                                        $select_question_hotspot = $destination_items[3];
5734
                                        $url_hotspot = $destination_items[4];
5735
                                    }
5736
                                }
5737
                            }
5738
                            break;
5739
                        case HOT_SPOT_ORDER:
5740
                            ExerciseShowFunctions::display_hotspot_order_answer(
5741
                                $feedback_type,
5742
                                $answerId,
5743
                                $answer,
5744
                                $studentChoice,
5745
                                $answerComment
5746
                            );
5747
                            break;
5748
                        case DRAGGABLE:
5749
                        case MATCHING_DRAGGABLE:
5750
                        case MATCHING:
5751
                            echo '<tr>';
5752
                            echo Display::tag('td', $answerMatching[$answerId]);
5753
                            echo Display::tag(
5754
                                'td',
5755
                                "$user_answer / ".Display::tag(
5756
                                    'strong',
5757
                                    $answerMatching[$answerCorrect],
5758
                                    ['style' => 'color: #008000; font-weight: bold;']
5759
                                )
5760
                            );
5761
                            echo '</tr>';
5762
                            break;
5763
                        case ANNOTATION:
5764
                            ExerciseShowFunctions::displayAnnotationAnswer(
5765
                                $feedback_type,
5766
                                $exeId,
5767
                                $questionId,
5768
                                $questionScore,
5769
                                $results_disabled
5770
                            );
5771
                            break;
5772
                    }
5773
                }
5774
            }
5775
        } // end for that loops over all answers of the current question
5776
5777
        if ($debug) {
5778
            error_log('-- End answer loop --');
5779
        }
5780
5781
        $final_answer = true;
5782
5783
        foreach ($real_answers as $my_answer) {
5784
            if (!$my_answer) {
5785
                $final_answer = false;
5786
            }
5787
        }
5788
5789
        // We add the total score after dealing with the answers.
5790
        if ($answerType == MULTIPLE_ANSWER_COMBINATION ||
5791
            $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE
5792
        ) {
5793
            if ($final_answer) {
5794
                //getting only the first score where we save the weight of all the question
5795
                $answerWeighting = $objAnswerTmp->selectWeighting(1);
5796
                if (empty($answerWeighting) && !empty($firstAnswer) && isset($firstAnswer['ponderation'])) {
5797
                    $answerWeighting = $firstAnswer['ponderation'];
5798
                }
5799
                $questionScore += $answerWeighting;
5800
            }
5801
        }
5802
5803
        $extra_data = [
5804
            'final_overlap' => $final_overlap,
5805
            'final_missing' => $final_missing,
5806
            'final_excess' => $final_excess,
5807
            'overlap_color' => $overlap_color,
5808
            'missing_color' => $missing_color,
5809
            'excess_color' => $excess_color,
5810
            'threadhold1' => $threadhold1,
5811
            'threadhold2' => $threadhold2,
5812
            'threadhold3' => $threadhold3,
5813
        ];
5814
5815
        if ($from === 'exercise_result') {
5816
            // if answer is hotspot. To the difference of exercise_show.php,
5817
            //  we use the results from the session (from_db=0)
5818
            // TODO Change this, because it is wrong to show the user
5819
            //  some results that haven't been stored in the database yet
5820
            if ($answerType == HOT_SPOT || $answerType == HOT_SPOT_ORDER || $answerType == HOT_SPOT_DELINEATION) {
5821
                if ($debug) {
5822
                    error_log('$from AND this is a hotspot kind of question ');
5823
                }
5824
                if ($answerType === HOT_SPOT_DELINEATION) {
5825
                    if ($showHotSpotDelineationTable) {
5826
                        if (!is_numeric($final_overlap)) {
5827
                            $final_overlap = 0;
5828
                        }
5829
                        if (!is_numeric($final_missing)) {
5830
                            $final_missing = 0;
5831
                        }
5832
                        if (!is_numeric($final_excess)) {
5833
                            $final_excess = 0;
5834
                        }
5835
5836
                        if ($final_overlap > 100) {
5837
                            $final_overlap = 100;
5838
                        }
5839
5840
                        $table_resume = '<table class="table table-hover table-striped data_table">
5841
                                <tr class="row_odd" >
5842
                                    <td></td>
5843
                                    <td ><b>'.get_lang('Requirements').'</b></td>
5844
                                    <td><b>'.get_lang('YourAnswer').'</b></td>
5845
                                </tr>
5846
                                <tr class="row_even">
5847
                                    <td><b>'.get_lang('Overlap').'</b></td>
5848
                                    <td>'.get_lang('Min').' '.$threadhold1.'</td>
5849
                                    <td class="text-right '.($overlap_color ? 'text-success' : 'text-danger').'">'
5850
                            .(($final_overlap < 0) ? 0 : intval($final_overlap)).'</td>
5851
                                </tr>
5852
                                <tr>
5853
                                    <td><b>'.get_lang('Excess').'</b></td>
5854
                                    <td>'.get_lang('Max').' '.$threadhold2.'</td>
5855
                                    <td class="text-right '.($excess_color ? 'text-success' : 'text-danger').'">'
5856
                            .(($final_excess < 0) ? 0 : intval($final_excess)).'</td>
5857
                                </tr>
5858
                                <tr class="row_even">
5859
                                    <td><b>'.get_lang('Missing').'</b></td>
5860
                                    <td>'.get_lang('Max').' '.$threadhold3.'</td>
5861
                                    <td class="text-right '.($missing_color ? 'text-success' : 'text-danger').'">'
5862
                            .(($final_missing < 0) ? 0 : intval($final_missing)).'</td>
5863
                                </tr>
5864
                            </table>';
5865
                        if ($next == 0) {
5866
                        } else {
5867
                            $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
5868
                            $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
5869
                        }
5870
5871
                        $message = '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>
5872
                                    <p style="text-align:center">';
5873
                        $message .= '<p>'.get_lang('YourDelineation').'</p>';
5874
                        $message .= $table_resume;
5875
                        $message .= '<br />'.get_lang('ResultIs').' '.$result_comment.'<br />';
5876
                        if ($organs_at_risk_hit > 0) {
5877
                            $message .= '<p><b>'.get_lang('OARHit').'</b></p>';
5878
                        }
5879
                        $message .= '<p>'.$comment.'</p>';
5880
                        echo $message;
5881
5882
                        $_SESSION['hotspot_delineation_result'][$this->selectId()][$questionId][0] = $message;
5883
                        $_SESSION['hotspot_delineation_result'][$this->selectId()][$questionId][1] = $_SESSION['exerciseResultCoordinates'][$questionId];
5884
                    } else {
5885
                        echo $hotspot_delineation_result[0];
5886
                    }
5887
5888
                    // Save the score attempts
5889
                    if (1) {
5890
                        //getting the answer 1 or 0 comes from exercise_submit_modal.php
5891
                        $final_answer = isset($hotspot_delineation_result[1]) ? $hotspot_delineation_result[1] : '';
5892
                        if ($final_answer == 0) {
5893
                            $questionScore = 0;
5894
                        }
5895
                        // we always insert the answer_id 1 = delineation
5896
                        Event::saveQuestionAttempt($questionScore, 1, $quesId, $exeId, 0);
5897
                        //in delineation mode, get the answer from $hotspot_delineation_result[1]
5898
                        $hotspotValue = isset($hotspot_delineation_result[1]) ? (int) $hotspot_delineation_result[1] === 1 ? 1 : 0 : 0;
5899
                        Event::saveExerciseAttemptHotspot(
5900
                            $exeId,
5901
                            $quesId,
5902
                            1,
5903
                            $hotspotValue,
5904
                            $exerciseResultCoordinates[$quesId],
5905
                            false,
5906
                            0,
5907
                            $learnpath_id,
5908
                            $learnpath_item_id
5909
                        );
5910
                    } else {
5911
                        if ($final_answer == 0) {
5912
                            $questionScore = 0;
5913
                            $answer = 0;
5914
                            Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0);
5915
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5916
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5917
                                    Event::saveExerciseAttemptHotspot(
5918
                                        $exeId,
5919
                                        $quesId,
5920
                                        $idx,
5921
                                        0,
5922
                                        $val,
5923
                                        false,
5924
                                        0,
5925
                                        $learnpath_id,
5926
                                        $learnpath_item_id
5927
                                    );
5928
                                }
5929
                            }
5930
                        } else {
5931
                            Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0);
5932
                            if (is_array($exerciseResultCoordinates[$quesId])) {
5933
                                foreach ($exerciseResultCoordinates[$quesId] as $idx => $val) {
5934
                                    $hotspotValue = (int) $choice[$idx] === 1 ? 1 : 0;
5935
                                    Event::saveExerciseAttemptHotspot(
5936
                                        $exeId,
5937
                                        $quesId,
5938
                                        $idx,
5939
                                        $hotspotValue,
5940
                                        $val,
5941
                                        false,
5942
                                        0,
5943
                                        $learnpath_id,
5944
                                        $learnpath_item_id
5945
                                    );
5946
                                }
5947
                            }
5948
                        }
5949
                    }
5950
                }
5951
            }
5952
5953
            $relPath = api_get_path(WEB_CODE_PATH);
5954
5955
            if ($answerType == HOT_SPOT || $answerType == HOT_SPOT_ORDER) {
5956
                // We made an extra table for the answers
5957
                if ($show_result) {
5958
                    echo '</table></td></tr>';
5959
                    echo "
5960
                        <tr>
5961
                            <td>
5962
                                <p><em>".get_lang('HotSpot')."</em></p>
5963
                                <div id=\"hotspot-solution-$questionId\"></div>
5964
                                <script>
5965
                                    $(function() {
5966
                                        new HotspotQuestion({
5967
                                            questionId: $questionId,
5968
                                            exerciseId: {$this->id},
5969
                                            exeId: $exeId,
5970
                                            selector: '#hotspot-solution-$questionId',
5971
                                            for: 'solution',
5972
                                            relPath: '$relPath'
5973
                                        });
5974
                                    });
5975
                                </script>
5976
                            </td>
5977
                        </tr>
5978
                    ";
5979
                }
5980
            } elseif ($answerType == ANNOTATION) {
5981
                if ($show_result) {
5982
                    echo '
5983
                        <p><em>'.get_lang('Annotation').'</em></p>
5984
                        <div id="annotation-canvas-'.$questionId.'"></div>
5985
                        <script>
5986
                            AnnotationQuestion({
5987
                                questionId: parseInt('.$questionId.'),
5988
                                exerciseId: parseInt('.$exeId.'),
5989
                                relPath: \''.$relPath.'\',
5990
                                courseId: parseInt('.$course_id.')
5991
                            });
5992
                        </script>
5993
                    ';
5994
                }
5995
            }
5996
5997
            if ($show_result && $answerType != ANNOTATION) {
5998
                echo '</table>';
5999
            }
6000
        }
6001
        unset($objAnswerTmp);
6002
6003
        $totalWeighting += $questionWeighting;
6004
        // Store results directly in the database
6005
        // For all in one page exercises, the results will be
6006
        // stored by exercise_results.php (using the session)
6007
        if ($save_results) {
6008
            if ($debug) {
6009
                error_log("Save question results $save_results");
6010
                error_log("Question score: $questionScore");
6011
                error_log('choice: ');
6012
                error_log(print_r($choice, 1));
6013
            }
6014
6015
            if (empty($choice)) {
6016
                $choice = 0;
6017
            }
6018
            // with certainty degree
6019
            if (empty($choiceDegreeCertainty)) {
6020
                $choiceDegreeCertainty = 0;
6021
            }
6022
            if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE ||
6023
                $answerType == MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE ||
6024
                $answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY
6025
            ) {
6026
                if ($choice != 0) {
6027
                    $reply = array_keys($choice);
6028
                    $countReply = count($reply);
6029
                    for ($i = 0; $i < $countReply; $i++) {
6030
                        $chosenAnswer = $reply[$i];
6031
                        if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
6032
                            if ($choiceDegreeCertainty != 0) {
6033
                                $replyDegreeCertainty = array_keys($choiceDegreeCertainty);
6034
                                $answerDegreeCertainty = isset($replyDegreeCertainty[$i]) ? $replyDegreeCertainty[$i] : '';
6035
                                $answerValue = isset($choiceDegreeCertainty[$answerDegreeCertainty]) ? $choiceDegreeCertainty[$answerDegreeCertainty] : '';
6036
                                Event::saveQuestionAttempt(
6037
                                    $questionScore,
6038
                                    $chosenAnswer.':'.$choice[$chosenAnswer].':'.$answerValue,
6039
                                    $quesId,
6040
                                    $exeId,
6041
                                    $i,
6042
                                    $this->id,
6043
                                    $updateResults,
6044
                                    $questionDuration
6045
                                );
6046
                            }
6047
                        } else {
6048
                            Event::saveQuestionAttempt(
6049
                                $questionScore,
6050
                                $chosenAnswer.':'.$choice[$chosenAnswer],
6051
                                $quesId,
6052
                                $exeId,
6053
                                $i,
6054
                                $this->id,
6055
                                $updateResults,
6056
                                $questionDuration
6057
                            );
6058
                        }
6059
                        if ($debug) {
6060
                            error_log('result =>'.$questionScore.' '.$chosenAnswer.':'.$choice[$chosenAnswer]);
6061
                        }
6062
                    }
6063
                } else {
6064
                    Event::saveQuestionAttempt(
6065
                        $questionScore,
6066
                        0,
6067
                        $quesId,
6068
                        $exeId,
6069
                        0,
6070
                        $this->id,
6071
                        false,
6072
                        $questionDuration
6073
                    );
6074
                }
6075
            } elseif ($answerType == MULTIPLE_ANSWER || $answerType == GLOBAL_MULTIPLE_ANSWER) {
6076
                if ($choice != 0) {
6077
                    $reply = array_keys($choice);
6078
                    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...
6079
                        $ans = $reply[$i];
6080
                        Event::saveQuestionAttempt(
6081
                            $questionScore,
6082
                            $ans,
6083
                            $quesId,
6084
                            $exeId,
6085
                            $i,
6086
                            $this->id,
6087
                            false,
6088
                            $questionDuration
6089
                        );
6090
                    }
6091
                } else {
6092
                    Event::saveQuestionAttempt(
6093
                        $questionScore,
6094
                        0,
6095
                        $quesId,
6096
                        $exeId,
6097
                        0,
6098
                        $this->id,
6099
                        false,
6100
                        $questionDuration
6101
                    );
6102
                }
6103
            } elseif ($answerType == MULTIPLE_ANSWER_COMBINATION) {
6104
                if ($choice != 0) {
6105
                    $reply = array_keys($choice);
6106
                    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...
6107
                        $ans = $reply[$i];
6108
                        Event::saveQuestionAttempt(
6109
                            $questionScore,
6110
                            $ans,
6111
                            $quesId,
6112
                            $exeId,
6113
                            $i,
6114
                            $this->id,
6115
                            false,
6116
                            $questionDuration
6117
                        );
6118
                    }
6119
                } else {
6120
                    Event::saveQuestionAttempt(
6121
                        $questionScore,
6122
                        0,
6123
                        $quesId,
6124
                        $exeId,
6125
                        0,
6126
                        $this->id,
6127
                        false,
6128
                        $questionDuration
6129
                    );
6130
                }
6131
            } elseif (in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE])) {
6132
                if (isset($matching)) {
6133
                    foreach ($matching as $j => $val) {
6134
                        Event::saveQuestionAttempt(
6135
                            $questionScore,
6136
                            $val,
6137
                            $quesId,
6138
                            $exeId,
6139
                            $j,
6140
                            $this->id,
6141
                            false,
6142
                            $questionDuration
6143
                        );
6144
                    }
6145
                }
6146
            } elseif ($answerType == FREE_ANSWER) {
6147
                $answer = $choice;
6148
                Event::saveQuestionAttempt(
6149
                    $questionScore,
6150
                    $answer,
6151
                    $quesId,
6152
                    $exeId,
6153
                    0,
6154
                    $this->id,
6155
                    false,
6156
                    $questionDuration
6157
                );
6158
            } elseif ($answerType == ORAL_EXPRESSION) {
6159
                $answer = $choice;
6160
                Event::saveQuestionAttempt(
6161
                    $questionScore,
6162
                    $answer,
6163
                    $quesId,
6164
                    $exeId,
6165
                    0,
6166
                    $this->id,
6167
                    false,
6168
                    $questionDuration,
6169
                    $objQuestionTmp->getAbsoluteFilePath()
6170
                );
6171
            } elseif (
6172
                in_array(
6173
                    $answerType,
6174
                    [UNIQUE_ANSWER, UNIQUE_ANSWER_IMAGE, UNIQUE_ANSWER_NO_OPTION, READING_COMPREHENSION]
6175
                )
6176
            ) {
6177
                $answer = $choice;
6178
                Event::saveQuestionAttempt($questionScore, $answer, $quesId, $exeId, 0, $this->id, false, $questionDuration);
6179
            } elseif ($answerType == HOT_SPOT || $answerType == ANNOTATION) {
6180
                $answer = [];
6181
                if (isset($exerciseResultCoordinates[$questionId]) && !empty($exerciseResultCoordinates[$questionId])) {
6182
                    if ($debug) {
6183
                        error_log('Checking result coordinates');
6184
                    }
6185
                    Database::delete(
6186
                        Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT),
6187
                        [
6188
                            'hotspot_exe_id = ? AND hotspot_question_id = ? AND c_id = ?' => [
6189
                                $exeId,
6190
                                $questionId,
6191
                                api_get_course_int_id(),
6192
                            ],
6193
                        ]
6194
                    );
6195
6196
                    foreach ($exerciseResultCoordinates[$questionId] as $idx => $val) {
6197
                        $answer[] = $val;
6198
                        $hotspotValue = (int) $choice[$idx] === 1 ? 1 : 0;
6199
                        if ($debug) {
6200
                            error_log('Hotspot value: '.$hotspotValue);
6201
                        }
6202
                        Event::saveExerciseAttemptHotspot(
6203
                            $exeId,
6204
                            $quesId,
6205
                            $idx,
6206
                            $hotspotValue,
6207
                            $val,
6208
                            false,
6209
                            $this->id,
6210
                            $learnpath_id,
6211
                            $learnpath_item_id
6212
                        );
6213
                    }
6214
                } else {
6215
                    if ($debug) {
6216
                        error_log('Empty: exerciseResultCoordinates');
6217
                    }
6218
                }
6219
                Event::saveQuestionAttempt(
6220
                    $questionScore,
6221
                    implode('|', $answer),
6222
                    $quesId,
6223
                    $exeId,
6224
                    0,
6225
                    $this->id,
6226
                    false,
6227
                    $questionDuration
6228
                );
6229
            } else {
6230
                Event::saveQuestionAttempt(
6231
                    $questionScore,
6232
                    $answer,
6233
                    $quesId,
6234
                    $exeId,
6235
                    0,
6236
                    $this->id,
6237
                    false,
6238
                    $questionDuration
6239
                );
6240
            }
6241
        }
6242
6243
        if ($propagate_neg == 0 && $questionScore < 0) {
6244
            $questionScore = 0;
6245
        }
6246
6247
        if ($save_results) {
6248
            $statsTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6249
            $sql = "UPDATE $statsTable SET
6250
                        exe_result = exe_result + ".floatval($questionScore)."
6251
                    WHERE exe_id = $exeId";
6252
            Database::query($sql);
6253
        }
6254
6255
        return [
6256
            'score' => $questionScore,
6257
            'weight' => $questionWeighting,
6258
            'extra' => $extra_data,
6259
            'open_question' => $arrques,
6260
            'open_answer' => $arrans,
6261
            'answer_type' => $answerType,
6262
            'generated_oral_file' => $generatedFile,
6263
            'user_answered' => $userAnsweredQuestion,
6264
            'correct_answer_id' => $correctAnswerId,
6265
            'answer_destination' => $answerDestination,
6266
        ];
6267
    }
6268
6269
    /**
6270
     * Sends a notification when a user ends an examn.
6271
     *
6272
     * @param string $type                  'start' or 'end' of an exercise
6273
     * @param array  $question_list_answers
6274
     * @param string $origin
6275
     * @param int    $exe_id
6276
     * @param float  $score
6277
     * @param float  $weight
6278
     *
6279
     * @return bool
6280
     */
6281
    public function send_mail_notification_for_exam(
6282
        $type,
6283
        $question_list_answers,
6284
        $origin,
6285
        $exe_id,
6286
        $score = null,
6287
        $weight = null
6288
    ) {
6289
        $setting = api_get_course_setting('email_alert_manager_on_new_quiz');
6290
6291
        if (empty($setting) && empty($this->getNotifications())) {
6292
            return false;
6293
        }
6294
6295
        $settingFromExercise = $this->getNotifications();
6296
        if (!empty($settingFromExercise)) {
6297
            $setting = $settingFromExercise;
6298
        }
6299
6300
        // Email configuration settings
6301
        $courseCode = api_get_course_id();
6302
        $courseInfo = api_get_course_info($courseCode);
6303
6304
        if (empty($courseInfo)) {
6305
            return false;
6306
        }
6307
6308
        $sessionId = api_get_session_id();
6309
6310
        $sessionData = '';
6311
        if (!empty($sessionId)) {
6312
            $sessionInfo = api_get_session_info($sessionId);
6313
            if (!empty($sessionInfo)) {
6314
                $sessionData = '<tr>'
6315
                    .'<td>'.get_lang('SessionName').'</td>'
6316
                    .'<td>'.$sessionInfo['name'].'</td>'
6317
                    .'</tr>';
6318
            }
6319
        }
6320
6321
        $sendStart = false;
6322
        $sendEnd = false;
6323
        $sendEndOpenQuestion = false;
6324
        $sendEndOralQuestion = false;
6325
6326
        foreach ($setting as $option) {
6327
            switch ($option) {
6328
                case 0:
6329
                    return false;
6330
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
6331
                case 1: // End
6332
                    if ($type === 'end') {
6333
                        $sendEnd = true;
6334
                    }
6335
                    break;
6336
                case 2: // start
6337
                    if ($type === 'start') {
6338
                        $sendStart = true;
6339
                    }
6340
                    break;
6341
                case 3: // end + open
6342
                    if ($type === 'end') {
6343
                        $sendEndOpenQuestion = true;
6344
                    }
6345
                    break;
6346
                case 4: // end + oral
6347
                    if ($type === 'end') {
6348
                        $sendEndOralQuestion = true;
6349
                    }
6350
                    break;
6351
            }
6352
        }
6353
6354
        $user_info = api_get_user_info(api_get_user_id());
6355
        $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_show.php?'.
6356
            api_get_cidreq(true, true, 'qualify').'&id='.$exe_id.'&action=qualify';
6357
6358
        if (!empty($sessionId)) {
6359
            $addGeneralCoach = true;
6360
            $setting = api_get_configuration_value('block_quiz_mail_notification_general_coach');
6361
            if ($setting === true) {
6362
                $addGeneralCoach = false;
6363
            }
6364
            $teachers = CourseManager::get_coach_list_from_course_code(
6365
                $courseCode,
6366
                $sessionId,
6367
                $addGeneralCoach
6368
            );
6369
        } else {
6370
            $teachers = CourseManager::get_teacher_list_from_course_code($courseCode);
6371
        }
6372
6373
        if ($sendEndOpenQuestion) {
6374
            $this->sendNotificationForOpenQuestions(
6375
                $question_list_answers,
6376
                $origin,
6377
                $user_info,
6378
                $url,
6379
                $teachers
6380
            );
6381
        }
6382
6383
        if ($sendEndOralQuestion) {
6384
            $this->sendNotificationForOralQuestions(
6385
                $question_list_answers,
6386
                $origin,
6387
                $exe_id,
6388
                $user_info,
6389
                $url,
6390
                $teachers
6391
            );
6392
        }
6393
6394
        if (!$sendEnd && !$sendStart) {
6395
            return false;
6396
        }
6397
6398
        $scoreLabel = '';
6399
        if ($sendEnd &&
6400
            api_get_configuration_value('send_score_in_exam_notification_mail_to_manager') == true
6401
        ) {
6402
            $notificationPercentage = api_get_configuration_value('send_notification_score_in_percentage');
6403
            $scoreLabel = ExerciseLib::show_score($score, $weight, $notificationPercentage, true);
6404
            $scoreLabel = "<tr>
6405
                            <td>".get_lang('Score')."</td>
6406
                            <td>&nbsp;$scoreLabel</td>
6407
                        </tr>";
6408
        }
6409
6410
        if ($sendEnd) {
6411
            $msg = get_lang('ExerciseAttempted').'<br /><br />';
6412
        } else {
6413
            $msg = get_lang('StudentStartExercise').'<br /><br />';
6414
        }
6415
6416
        $msg .= get_lang('AttemptDetails').' : <br /><br />
6417
                    <table>
6418
                        <tr>
6419
                            <td>'.get_lang('CourseName').'</td>
6420
                            <td>#course#</td>
6421
                        </tr>
6422
                        '.$sessionData.'
6423
                        <tr>
6424
                            <td>'.get_lang('Exercise').'</td>
6425
                            <td>&nbsp;#exercise#</td>
6426
                        </tr>
6427
                        <tr>
6428
                            <td>'.get_lang('StudentName').'</td>
6429
                            <td>&nbsp;#student_complete_name#</td>
6430
                        </tr>
6431
                        <tr>
6432
                            <td>'.get_lang('StudentEmail').'</td>
6433
                            <td>&nbsp;#email#</td>
6434
                        </tr>
6435
                        '.$scoreLabel.'
6436
                    </table>';
6437
6438
        $variables = [
6439
            '#email#' => $user_info['email'],
6440
            '#exercise#' => $this->exercise,
6441
            '#student_complete_name#' => $user_info['complete_name'],
6442
            '#course#' => Display::url(
6443
                $courseInfo['title'],
6444
                $courseInfo['course_public_url'].'?id_session='.$sessionId
6445
            ),
6446
        ];
6447
6448
        if ($sendEnd) {
6449
            $msg .= '<br /><a href="#url#">'.get_lang('ClickToCommentAndGiveFeedback').'</a>';
6450
            $variables['#url#'] = $url;
6451
        }
6452
6453
        $content = str_replace(array_keys($variables), array_values($variables), $msg);
6454
6455
        if ($sendEnd) {
6456
            $subject = get_lang('ExerciseAttempted');
6457
        } else {
6458
            $subject = get_lang('StudentStartExercise');
6459
        }
6460
6461
        if (!empty($teachers)) {
6462
            foreach ($teachers as $user_id => $teacher_data) {
6463
                MessageManager::send_message_simple(
6464
                    $user_id,
6465
                    $subject,
6466
                    $content
6467
                );
6468
            }
6469
        }
6470
    }
6471
6472
    /**
6473
     * @param array $user_data         result of api_get_user_info()
6474
     * @param array $trackExerciseInfo result of get_stat_track_exercise_info
6475
     * @param bool  $saveUserResult
6476
     * @param bool  $allowSignature
6477
     * @param bool  $allowExportPdf
6478
     *
6479
     * @return string
6480
     */
6481
    public function showExerciseResultHeader(
6482
        $user_data,
6483
        $trackExerciseInfo,
6484
        $saveUserResult,
6485
        $allowSignature = false,
6486
        $allowExportPdf = false
6487
    ) {
6488
        if (api_get_configuration_value('hide_user_info_in_quiz_result')) {
6489
            return '';
6490
        }
6491
6492
        $start_date = null;
6493
        if (isset($trackExerciseInfo['start_date'])) {
6494
            $start_date = api_convert_and_format_date($trackExerciseInfo['start_date']);
6495
        }
6496
        $duration = isset($trackExerciseInfo['duration_formatted']) ? $trackExerciseInfo['duration_formatted'] : null;
6497
        $ip = isset($trackExerciseInfo['user_ip']) ? $trackExerciseInfo['user_ip'] : null;
6498
6499
        if (!empty($user_data)) {
6500
            $userFullName = $user_data['complete_name'];
6501
            if (api_is_teacher() || api_is_platform_admin(true, true)) {
6502
                $userFullName = '<a href="'.$user_data['profile_url'].'" title="'.get_lang('GoToStudentDetails').'">'.
6503
                    $user_data['complete_name'].'</a>';
6504
            }
6505
6506
            $data = [
6507
                'name_url' => $userFullName,
6508
                'complete_name' => $user_data['complete_name'],
6509
                'username' => $user_data['username'],
6510
                'avatar' => $user_data['avatar_medium'],
6511
                'url' => $user_data['profile_url'],
6512
            ];
6513
6514
            if (!empty($user_data['official_code'])) {
6515
                $data['code'] = $user_data['official_code'];
6516
            }
6517
        }
6518
        // Description can be very long and is generally meant to explain
6519
        //   rules *before* the exam. Leaving here to make display easier if
6520
        //   necessary
6521
        /*
6522
        if (!empty($this->description)) {
6523
            $array[] = array('title' => get_lang("Description"), 'content' => $this->description);
6524
        }
6525
        */
6526
        if (!empty($start_date)) {
6527
            $data['start_date'] = $start_date;
6528
        }
6529
6530
        if (!empty($duration)) {
6531
            $data['duration'] = $duration;
6532
        }
6533
6534
        if (!empty($ip)) {
6535
            $data['ip'] = $ip;
6536
        }
6537
6538
        if (api_get_configuration_value('save_titles_as_html')) {
6539
            $data['title'] = $this->get_formated_title().get_lang('Result');
6540
        } else {
6541
            $data['title'] = PHP_EOL.$this->exercise.' : '.get_lang('Result');
6542
        }
6543
6544
        $questionsCount = count(explode(',', $trackExerciseInfo['data_tracking']));
6545
        $savedAnswersCount = $this->countUserAnswersSavedInExercise($trackExerciseInfo['exe_id']);
6546
6547
        $data['number_of_answers'] = $questionsCount;
6548
        $data['number_of_answers_saved'] = $savedAnswersCount;
6549
        $exeId = $trackExerciseInfo['exe_id'];
6550
6551
        if (false !== api_get_configuration_value('quiz_confirm_saved_answers')) {
6552
            $em = Database::getManager();
6553
6554
            if ($saveUserResult) {
6555
                $trackConfirmation = new TrackEExerciseConfirmation();
6556
                $trackConfirmation
6557
                    ->setUserId($trackExerciseInfo['exe_user_id'])
6558
                    ->setQuizId($trackExerciseInfo['exe_exo_id'])
6559
                    ->setAttemptId($trackExerciseInfo['exe_id'])
6560
                    ->setQuestionsCount($questionsCount)
6561
                    ->setSavedAnswersCount($savedAnswersCount)
6562
                    ->setCourseId($trackExerciseInfo['c_id'])
6563
                    ->setSessionId($trackExerciseInfo['session_id'])
6564
                    ->setCreatedAt(api_get_utc_datetime(null, false, true));
6565
6566
                $em->persist($trackConfirmation);
6567
                $em->flush();
6568
            } else {
6569
                $trackConfirmation = $em
6570
                    ->getRepository('ChamiloCoreBundle:TrackEExerciseConfirmation')
6571
                    ->findOneBy(
6572
                        [
6573
                            'attemptId' => $trackExerciseInfo['exe_id'],
6574
                            'quizId' => $trackExerciseInfo['exe_exo_id'],
6575
                            'courseId' => $trackExerciseInfo['c_id'],
6576
                            'sessionId' => $trackExerciseInfo['session_id'],
6577
                        ]
6578
                    );
6579
            }
6580
6581
            $data['track_confirmation'] = $trackConfirmation;
6582
        }
6583
6584
        $signature = '';
6585
        if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($this)) {
6586
            $signature = ExerciseSignaturePlugin::getSignature($trackExerciseInfo['exe_user_id'], $trackExerciseInfo);
6587
        }
6588
6589
        $tpl = new Template(null, false, false, false, false, false, false);
6590
        $tpl->assign('data', $data);
6591
        $tpl->assign('allow_signature', $allowSignature);
6592
        $tpl->assign('signature', $signature);
6593
        $tpl->assign('allow_export_pdf', $allowExportPdf);
6594
        $tpl->assign('export_url', api_get_path(WEB_CODE_PATH).'exercise/result.php?action=export&id='.$exeId.'&'.api_get_cidreq());
6595
6596
        $layoutTemplate = $tpl->get_template('exercise/partials/result_exercise.tpl');
6597
6598
        return $tpl->fetch($layoutTemplate);
6599
    }
6600
6601
    /**
6602
     * Returns the exercise result.
6603
     *
6604
     * @param 	int		attempt id
6605
     *
6606
     * @return array
6607
     */
6608
    public function get_exercise_result($exe_id)
6609
    {
6610
        $result = [];
6611
        $track_exercise_info = ExerciseLib::get_exercise_track_exercise_info($exe_id);
6612
6613
        if (!empty($track_exercise_info)) {
6614
            $totalScore = 0;
6615
            $objExercise = new self();
6616
            $objExercise->read($track_exercise_info['exe_exo_id']);
6617
            if (!empty($track_exercise_info['data_tracking'])) {
6618
                $question_list = explode(',', $track_exercise_info['data_tracking']);
6619
            }
6620
            foreach ($question_list as $questionId) {
6621
                $question_result = $objExercise->manage_answer(
6622
                    $exe_id,
6623
                    $questionId,
6624
                    '',
6625
                    'exercise_show',
6626
                    [],
6627
                    false,
6628
                    true,
6629
                    false,
6630
                    $objExercise->selectPropagateNeg()
6631
                );
6632
                $totalScore += $question_result['score'];
6633
            }
6634
6635
            if ($objExercise->selectPropagateNeg() == 0 && $totalScore < 0) {
6636
                $totalScore = 0;
6637
            }
6638
            $result = [
6639
                'score' => $totalScore,
6640
                'weight' => $track_exercise_info['exe_weighting'],
6641
            ];
6642
        }
6643
6644
        return $result;
6645
    }
6646
6647
    /**
6648
     * Checks if the exercise is visible due a lot of conditions
6649
     * visibility, time limits, student attempts
6650
     * Return associative array
6651
     * value : true if exercise visible
6652
     * message : HTML formatted message
6653
     * rawMessage : text message.
6654
     *
6655
     * @param int  $lpId
6656
     * @param int  $lpItemId
6657
     * @param int  $lpItemViewId
6658
     * @param bool $filterByAdmin
6659
     *
6660
     * @return array
6661
     */
6662
    public function is_visible(
6663
        $lpId = 0,
6664
        $lpItemId = 0,
6665
        $lpItemViewId = 0,
6666
        $filterByAdmin = true,
6667
        $sessionId = 0
6668
    ) {
6669
        $sessionId = (int) $sessionId;
6670
        if ($sessionId == 0) {
6671
            $sessionId = $this->sessionId;
6672
        }
6673
        // 1. By default the exercise is visible
6674
        $isVisible = true;
6675
        $message = null;
6676
6677
        // 1.1 Admins, teachers and tutors can access to the exercise
6678
        if ($filterByAdmin) {
6679
            if (api_is_platform_admin() || api_is_course_admin() || api_is_course_tutor()) {
6680
                return ['value' => true, 'message' => ''];
6681
            }
6682
        }
6683
6684
        // Deleted exercise.
6685
        if ($this->active == -1) {
6686
            return [
6687
                'value' => false,
6688
                'message' => Display::return_message(
6689
                    get_lang('ExerciseNotFound'),
6690
                    'warning',
6691
                    false
6692
                ),
6693
                'rawMessage' => get_lang('ExerciseNotFound'),
6694
            ];
6695
        }
6696
6697
        // Checking visibility in the item_property table.
6698
        $visibility = api_get_item_visibility(
6699
            api_get_course_info(),
6700
            TOOL_QUIZ,
6701
            $this->id,
6702
            api_get_session_id()
6703
        );
6704
6705
        if ($visibility == 0 || $visibility == 2) {
6706
            $this->active = 0;
6707
        }
6708
6709
        // 2. If the exercise is not active.
6710
        if (empty($lpId)) {
6711
            // 2.1 LP is OFF
6712
            if ($this->active == 0) {
6713
                return [
6714
                    'value' => false,
6715
                    'message' => Display::return_message(
6716
                        get_lang('ExerciseNotFound'),
6717
                        'warning',
6718
                        false
6719
                    ),
6720
                    'rawMessage' => get_lang('ExerciseNotFound'),
6721
                ];
6722
            }
6723
        } else {
6724
            // 2.1 LP is loaded
6725
            if ($this->active == 0 &&
6726
                !learnpath::is_lp_visible_for_student($lpId, api_get_user_id())
6727
            ) {
6728
                return [
6729
                    'value' => false,
6730
                    'message' => Display::return_message(
6731
                        get_lang('ExerciseNotFound'),
6732
                        'warning',
6733
                        false
6734
                    ),
6735
                    'rawMessage' => get_lang('ExerciseNotFound'),
6736
                ];
6737
            }
6738
        }
6739
6740
        // 3. We check if the time limits are on
6741
        $limitTimeExists = false;
6742
        if (!empty($this->start_time) || !empty($this->end_time)) {
6743
            $limitTimeExists = true;
6744
        }
6745
6746
        if ($limitTimeExists) {
6747
            $timeNow = time();
6748
            $existsStartDate = false;
6749
            $nowIsAfterStartDate = true;
6750
            $existsEndDate = false;
6751
            $nowIsBeforeEndDate = true;
6752
6753
            if (!empty($this->start_time)) {
6754
                $existsStartDate = true;
6755
            }
6756
6757
            if (!empty($this->end_time)) {
6758
                $existsEndDate = true;
6759
            }
6760
6761
            // check if we are before-or-after end-or-start date
6762
            if ($existsStartDate && $timeNow < api_strtotime($this->start_time, 'UTC')) {
6763
                $nowIsAfterStartDate = false;
6764
            }
6765
6766
            if ($existsEndDate & $timeNow >= api_strtotime($this->end_time, 'UTC')) {
6767
                $nowIsBeforeEndDate = false;
6768
            }
6769
6770
            // lets check all cases
6771
            if ($existsStartDate && !$existsEndDate) {
6772
                // exists start date and dont exists end date
6773
                if ($nowIsAfterStartDate) {
6774
                    // after start date, no end date
6775
                    $isVisible = true;
6776
                    $message = sprintf(
6777
                        get_lang('ExerciseAvailableSinceX'),
6778
                        api_convert_and_format_date($this->start_time)
6779
                    );
6780
                } else {
6781
                    // before start date, no end date
6782
                    $isVisible = false;
6783
                    $message = sprintf(
6784
                        get_lang('ExerciseAvailableFromX'),
6785
                        api_convert_and_format_date($this->start_time)
6786
                    );
6787
                }
6788
            } elseif (!$existsStartDate && $existsEndDate) {
6789
                // doesnt exist start date, exists end date
6790
                if ($nowIsBeforeEndDate) {
6791
                    // before end date, no start date
6792
                    $isVisible = true;
6793
                    $message = sprintf(
6794
                        get_lang('ExerciseAvailableUntilX'),
6795
                        api_convert_and_format_date($this->end_time)
6796
                    );
6797
                } else {
6798
                    // after end date, no start date
6799
                    $isVisible = false;
6800
                    $message = sprintf(
6801
                        get_lang('ExerciseAvailableUntilX'),
6802
                        api_convert_and_format_date($this->end_time)
6803
                    );
6804
                }
6805
            } elseif ($existsStartDate && $existsEndDate) {
6806
                // exists start date and end date
6807
                if ($nowIsAfterStartDate) {
6808
                    if ($nowIsBeforeEndDate) {
6809
                        // after start date and before end date
6810
                        $isVisible = true;
6811
                        $message = sprintf(
6812
                            get_lang('ExerciseIsActivatedFromXToY'),
6813
                            api_convert_and_format_date($this->start_time),
6814
                            api_convert_and_format_date($this->end_time)
6815
                        );
6816
                    } else {
6817
                        // after start date and after end date
6818
                        $isVisible = false;
6819
                        $message = sprintf(
6820
                            get_lang('ExerciseWasActivatedFromXToY'),
6821
                            api_convert_and_format_date($this->start_time),
6822
                            api_convert_and_format_date($this->end_time)
6823
                        );
6824
                    }
6825
                } else {
6826
                    if ($nowIsBeforeEndDate) {
6827
                        // before start date and before end date
6828
                        $isVisible = false;
6829
                        $message = sprintf(
6830
                            get_lang('ExerciseWillBeActivatedFromXToY'),
6831
                            api_convert_and_format_date($this->start_time),
6832
                            api_convert_and_format_date($this->end_time)
6833
                        );
6834
                    }
6835
                    // case before start date and after end date is impossible
6836
                }
6837
            } elseif (!$existsStartDate && !$existsEndDate) {
6838
                // doesnt exist start date nor end date
6839
                $isVisible = true;
6840
                $message = '';
6841
            }
6842
        }
6843
6844
        $remedialCoursePlugin = RemedialCoursePlugin::create();
6845
6846
        // BT#18165
6847
        $exerciseAttempts = $this->selectAttempts();
6848
        if ($exerciseAttempts > 0) {
6849
            $userId = api_get_user_id();
6850
            $attemptCount = Event::get_attempt_count_not_finished(
6851
                $userId,
6852
                $this->id,
6853
                $lpId,
6854
                $lpItemId,
6855
                $lpItemViewId
6856
            );
6857
            $message .= RemedialCoursePlugin::create()->getAdvancedCourseList($this, $userId, api_get_session_id());
6858
            if ($attemptCount >= $exerciseAttempts) {
6859
                $message .= $remedialCoursePlugin->getRemedialCourseList($this, $userId, api_get_session_id());
6860
            }
6861
        }
6862
        // 4. We check if the student have attempts
6863
        if ($isVisible) {
6864
            $exerciseAttempts = $this->selectAttempts();
6865
6866
            if ($exerciseAttempts > 0) {
6867
                $attemptCount = Event::get_attempt_count_not_finished(
6868
                    api_get_user_id(),
6869
                    $this->id,
6870
                    $lpId,
6871
                    $lpItemId,
6872
                    $lpItemViewId
6873
                );
6874
6875
                if ($attemptCount >= $exerciseAttempts) {
6876
                    $message = sprintf(
6877
                        get_lang('ReachedMaxAttempts'),
6878
                        $this->name,
6879
                        $exerciseAttempts
6880
                    );
6881
                    $isVisible = false;
6882
                } else {
6883
                    // Check blocking exercise.
6884
                    $extraFieldValue = new ExtraFieldValue('exercise');
6885
                    $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
6886
                        $this->iId,
6887
                        'blocking_percentage'
6888
                    );
6889
                    if ($blockExercise && isset($blockExercise['value']) && !empty($blockExercise['value'])) {
6890
                        $blockPercentage = (int) $blockExercise['value'];
6891
                        $userAttempts = Event::getExerciseResultsByUser(
6892
                            api_get_user_id(),
6893
                            $this->iId,
6894
                            $this->course_id,
6895
                            $sessionId,
6896
                            $lpId,
6897
                            $lpItemId
6898
                        );
6899
6900
                        if (!empty($userAttempts)) {
6901
                            $currentAttempt = current($userAttempts);
6902
                            if ($currentAttempt['total_percentage'] <= $blockPercentage) {
6903
                                $message = sprintf(
6904
                                    get_lang('ExerciseBlockBecausePercentageX'),
6905
                                    $blockPercentage
6906
                                );
6907
                                $isVisible = false; // See BT#18165
6908
                                $message .= $remedialCoursePlugin->getRemedialCourseList(
6909
                                    $this,
6910
                                    api_get_user_id(),
6911
                                    api_get_session_id()
6912
                                );
6913
                            }
6914
                        }
6915
                    }
6916
                }
6917
            }
6918
        }
6919
6920
        $rawMessage = '';
6921
        if (!empty($message)) {
6922
            $rawMessage = $message;
6923
            $message = Display::return_message($message, 'warning', false);
6924
        }
6925
6926
        return [
6927
            'value' => $isVisible,
6928
            'message' => $message,
6929
            'rawMessage' => $rawMessage,
6930
        ];
6931
    }
6932
6933
    /**
6934
     * @return bool
6935
     */
6936
    public function added_in_lp()
6937
    {
6938
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
6939
        $sql = "SELECT max_score FROM $TBL_LP_ITEM
6940
                WHERE
6941
                    c_id = {$this->course_id} AND
6942
                    item_type = '".TOOL_QUIZ."' AND
6943
                    path = '{$this->id}'";
6944
        $result = Database::query($sql);
6945
        if (Database::num_rows($result) > 0) {
6946
            return true;
6947
        }
6948
6949
        return false;
6950
    }
6951
6952
    /**
6953
     * Returns an array with this form.
6954
     *
6955
     * @example
6956
     * <code>
6957
     * array (size=3)
6958
     * 999 =>
6959
     * array (size=3)
6960
     * 0 => int 3422
6961
     * 1 => int 3423
6962
     * 2 => int 3424
6963
     * 100 =>
6964
     * array (size=2)
6965
     * 0 => int 3469
6966
     * 1 => int 3470
6967
     * 101 =>
6968
     * array (size=1)
6969
     * 0 => int 3482
6970
     * </code>
6971
     * The array inside the key 999 means the question list that belongs to the media id = 999,
6972
     * this case is special because 999 means "no media".
6973
     *
6974
     * @return array
6975
     */
6976
    public function getMediaList()
6977
    {
6978
        return $this->mediaList;
6979
    }
6980
6981
    /**
6982
     * Is media question activated?
6983
     *
6984
     * @return bool
6985
     */
6986
    public function mediaIsActivated()
6987
    {
6988
        $mediaQuestions = $this->getMediaList();
6989
        $active = false;
6990
        if (isset($mediaQuestions) && !empty($mediaQuestions)) {
6991
            $media_count = count($mediaQuestions);
6992
            if ($media_count > 1) {
6993
                return true;
6994
            } elseif ($media_count == 1) {
6995
                if (isset($mediaQuestions[999])) {
6996
                    return false;
6997
                } else {
6998
                    return true;
6999
                }
7000
            }
7001
        }
7002
7003
        return $active;
7004
    }
7005
7006
    /**
7007
     * Gets question list from the exercise.
7008
     *
7009
     * @return array
7010
     */
7011
    public function getQuestionList()
7012
    {
7013
        return $this->questionList;
7014
    }
7015
7016
    /**
7017
     * Question list with medias compressed like this.
7018
     *
7019
     * @example
7020
     * <code>
7021
     * array(
7022
     *      question_id_1,
7023
     *      question_id_2,
7024
     *      media_id, <- this media id contains question ids
7025
     *      question_id_3,
7026
     * )
7027
     * </code>
7028
     *
7029
     * @return array
7030
     */
7031
    public function getQuestionListWithMediasCompressed()
7032
    {
7033
        return $this->questionList;
7034
    }
7035
7036
    /**
7037
     * Question list with medias uncompressed like this.
7038
     *
7039
     * @example
7040
     * <code>
7041
     * array(
7042
     *      question_id,
7043
     *      question_id,
7044
     *      question_id, <- belongs to a media id
7045
     *      question_id, <- belongs to a media id
7046
     *      question_id,
7047
     * )
7048
     * </code>
7049
     *
7050
     * @return array
7051
     */
7052
    public function getQuestionListWithMediasUncompressed()
7053
    {
7054
        return $this->questionListUncompressed;
7055
    }
7056
7057
    /**
7058
     * Sets the question list when the exercise->read() is executed.
7059
     *
7060
     * @param bool $adminView Whether to view the set the list of *all* questions or just the normal student view
7061
     */
7062
    public function setQuestionList($adminView = false)
7063
    {
7064
        // Getting question list.
7065
        $questionList = $this->selectQuestionList(true, $adminView);
7066
        $this->setMediaList($questionList);
7067
        $this->questionList = $this->transformQuestionListWithMedias($questionList, false);
7068
        $this->questionListUncompressed = $this->transformQuestionListWithMedias(
7069
            $questionList,
7070
            true
7071
        );
7072
    }
7073
7074
    /**
7075
     * @params array question list
7076
     * @params bool expand or not question list (true show all questions,
7077
     * false show media question id instead of the question ids)
7078
     */
7079
    public function transformQuestionListWithMedias(
7080
        $question_list,
7081
        $expand_media_questions = false
7082
    ) {
7083
        $new_question_list = [];
7084
        if (!empty($question_list)) {
7085
            $media_questions = $this->getMediaList();
7086
            $media_active = $this->mediaIsActivated($media_questions);
7087
7088
            if ($media_active) {
7089
                $counter = 1;
7090
                foreach ($question_list as $question_id) {
7091
                    $add_question = true;
7092
                    foreach ($media_questions as $media_id => $question_list_in_media) {
7093
                        if ($media_id != 999 && in_array($question_id, $question_list_in_media)) {
7094
                            $add_question = false;
7095
                            if (!in_array($media_id, $new_question_list)) {
7096
                                $new_question_list[$counter] = $media_id;
7097
                                $counter++;
7098
                            }
7099
                            break;
7100
                        }
7101
                    }
7102
                    if ($add_question) {
7103
                        $new_question_list[$counter] = $question_id;
7104
                        $counter++;
7105
                    }
7106
                }
7107
                if ($expand_media_questions) {
7108
                    $media_key_list = array_keys($media_questions);
7109
                    foreach ($new_question_list as &$question_id) {
7110
                        if (in_array($question_id, $media_key_list)) {
7111
                            $question_id = $media_questions[$question_id];
7112
                        }
7113
                    }
7114
                    $new_question_list = array_flatten($new_question_list);
7115
                }
7116
            } else {
7117
                $new_question_list = $question_list;
7118
            }
7119
        }
7120
7121
        return $new_question_list;
7122
    }
7123
7124
    /**
7125
     * Get question list depend on the random settings.
7126
     *
7127
     * @return array
7128
     */
7129
    public function get_validated_question_list()
7130
    {
7131
        $isRandomByCategory = $this->isRandomByCat();
7132
        if ($isRandomByCategory == 0) {
7133
            if ($this->isRandom()) {
7134
                return $this->getRandomList();
7135
            }
7136
7137
            return $this->selectQuestionList();
7138
        }
7139
7140
        if ($this->isRandom()) {
7141
            // USE question categories
7142
            // get questions by category for this exercise
7143
            // we have to choice $objExercise->random question in each array values of $categoryQuestions
7144
            // key of $categoryQuestions are the categopy id (0 for not in a category)
7145
            // value is the array of question id of this category
7146
            $questionList = [];
7147
            $categoryQuestions = TestCategory::getQuestionsByCat($this->id);
7148
            $isRandomByCategory = $this->getRandomByCategory();
7149
            // We sort categories based on the term between [] in the head
7150
            // of the category's description
7151
            /* examples of categories :
7152
             * [biologie] Maitriser les mecanismes de base de la genetique
7153
             * [biologie] Relier les moyens de depenses et les agents infectieux
7154
             * [biologie] Savoir ou est produite l'enrgie dans les cellules et sous quelle forme
7155
             * [chimie] Classer les molles suivant leur pouvoir oxydant ou reacteur
7156
             * [chimie] Connaître la denition de la theoie acide/base selon Brönsted
7157
             * [chimie] Connaître les charges des particules
7158
             * We want that in the order of the groups defined by the term
7159
             * between brackets at the beginning of the category title
7160
            */
7161
            // If test option is Grouped By Categories
7162
            if ($isRandomByCategory == 2) {
7163
                $categoryQuestions = TestCategory::sortTabByBracketLabel($categoryQuestions);
7164
            }
7165
            foreach ($categoryQuestions as $question) {
7166
                $number_of_random_question = $this->random;
7167
                if ($this->random == -1) {
7168
                    $number_of_random_question = count($this->questionList);
7169
                }
7170
                $questionList = array_merge(
7171
                    $questionList,
7172
                    TestCategory::getNElementsFromArray(
7173
                        $question,
7174
                        $number_of_random_question
7175
                    )
7176
                );
7177
            }
7178
            // shuffle the question list if test is not grouped by categories
7179
            if ($isRandomByCategory == 1) {
7180
                shuffle($questionList); // or not
7181
            }
7182
7183
            return $questionList;
7184
        }
7185
7186
        // Problem, random by category has been selected and
7187
        // we have no $this->isRandom number of question selected
7188
        // Should not happened
7189
7190
        return [];
7191
    }
7192
7193
    public function get_question_list($expand_media_questions = false)
7194
    {
7195
        $question_list = $this->get_validated_question_list();
7196
        $question_list = $this->transform_question_list_with_medias($question_list, $expand_media_questions);
7197
7198
        return $question_list;
7199
    }
7200
7201
    public function transform_question_list_with_medias($question_list, $expand_media_questions = false)
7202
    {
7203
        $new_question_list = [];
7204
        if (!empty($question_list)) {
7205
            $media_questions = $this->getMediaList();
7206
            $media_active = $this->mediaIsActivated($media_questions);
7207
7208
            if ($media_active) {
7209
                $counter = 1;
7210
                foreach ($question_list as $question_id) {
7211
                    $add_question = true;
7212
                    foreach ($media_questions as $media_id => $question_list_in_media) {
7213
                        if ($media_id != 999 && in_array($question_id, $question_list_in_media)) {
7214
                            $add_question = false;
7215
                            if (!in_array($media_id, $new_question_list)) {
7216
                                $new_question_list[$counter] = $media_id;
7217
                                $counter++;
7218
                            }
7219
                            break;
7220
                        }
7221
                    }
7222
                    if ($add_question) {
7223
                        $new_question_list[$counter] = $question_id;
7224
                        $counter++;
7225
                    }
7226
                }
7227
                if ($expand_media_questions) {
7228
                    $media_key_list = array_keys($media_questions);
7229
                    foreach ($new_question_list as &$question_id) {
7230
                        if (in_array($question_id, $media_key_list)) {
7231
                            $question_id = $media_questions[$question_id];
7232
                        }
7233
                    }
7234
                    $new_question_list = array_flatten($new_question_list);
7235
                }
7236
            } else {
7237
                $new_question_list = $question_list;
7238
            }
7239
        }
7240
7241
        return $new_question_list;
7242
    }
7243
7244
    /**
7245
     * @param int $exe_id
7246
     *
7247
     * @return array
7248
     */
7249
    public function get_stat_track_exercise_info_by_exe_id($exe_id)
7250
    {
7251
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7252
        $exe_id = (int) $exe_id;
7253
        $sql_track = "SELECT * FROM $table WHERE exe_id = $exe_id ";
7254
        $result = Database::query($sql_track);
7255
        $new_array = [];
7256
        if (Database::num_rows($result) > 0) {
7257
            $new_array = Database::fetch_array($result, 'ASSOC');
7258
            $start_date = api_get_utc_datetime($new_array['start_date'], true);
7259
            $end_date = api_get_utc_datetime($new_array['exe_date'], true);
7260
            $new_array['duration_formatted'] = '';
7261
            if (!empty($new_array['exe_duration']) && !empty($start_date) && !empty($end_date)) {
7262
                $time = api_format_time($new_array['exe_duration'], 'js');
7263
                $new_array['duration_formatted'] = $time;
7264
            }
7265
        }
7266
7267
        return $new_array;
7268
    }
7269
7270
    /**
7271
     * @param int $exeId
7272
     *
7273
     * @return bool
7274
     */
7275
    public function removeAllQuestionToRemind($exeId)
7276
    {
7277
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7278
        $exeId = (int) $exeId;
7279
        if (empty($exeId)) {
7280
            return false;
7281
        }
7282
        $sql = "UPDATE $table
7283
                SET questions_to_check = ''
7284
                WHERE exe_id = $exeId ";
7285
        Database::query($sql);
7286
7287
        return true;
7288
    }
7289
7290
    /**
7291
     * @param int   $exeId
7292
     * @param array $questionList
7293
     *
7294
     * @return bool
7295
     */
7296
    public function addAllQuestionToRemind($exeId, $questionList = [])
7297
    {
7298
        $exeId = (int) $exeId;
7299
        if (empty($questionList)) {
7300
            return false;
7301
        }
7302
7303
        $questionListToString = implode(',', $questionList);
7304
        $questionListToString = Database::escape_string($questionListToString);
7305
7306
        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7307
        $sql = "UPDATE $table
7308
                SET questions_to_check = '$questionListToString'
7309
                WHERE exe_id = $exeId";
7310
        Database::query($sql);
7311
7312
        return true;
7313
    }
7314
7315
    /**
7316
     * @param int    $exeId
7317
     * @param int    $questionId
7318
     * @param string $action
7319
     */
7320
    public function editQuestionToRemind($exeId, $questionId, $action = 'add')
7321
    {
7322
        $exercise_info = self::get_stat_track_exercise_info_by_exe_id($exeId);
7323
        $questionId = (int) $questionId;
7324
        $exeId = (int) $exeId;
7325
7326
        if ($exercise_info) {
7327
            $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
7328
            if (empty($exercise_info['questions_to_check'])) {
7329
                if ($action === 'add') {
7330
                    $sql = "UPDATE $track_exercises
7331
                            SET questions_to_check = '$questionId'
7332
                            WHERE exe_id = $exeId ";
7333
                    Database::query($sql);
7334
                }
7335
            } else {
7336
                $remind_list = explode(',', $exercise_info['questions_to_check']);
7337
                $remind_list_string = '';
7338
                if ($action === 'add') {
7339
                    if (!in_array($questionId, $remind_list)) {
7340
                        $newRemindList = [];
7341
                        $remind_list[] = $questionId;
7342
                        $questionListInSession = Session::read('questionList');
7343
                        if (!empty($questionListInSession)) {
7344
                            foreach ($questionListInSession as $originalQuestionId) {
7345
                                if (in_array($originalQuestionId, $remind_list)) {
7346
                                    $newRemindList[] = $originalQuestionId;
7347
                                }
7348
                            }
7349
                        }
7350
                        $remind_list_string = implode(',', $newRemindList);
7351
                    }
7352
                } elseif ($action === 'delete') {
7353
                    if (!empty($remind_list)) {
7354
                        if (in_array($questionId, $remind_list)) {
7355
                            $remind_list = array_flip($remind_list);
7356
                            unset($remind_list[$questionId]);
7357
                            $remind_list = array_flip($remind_list);
7358
7359
                            if (!empty($remind_list)) {
7360
                                sort($remind_list);
7361
                                array_filter($remind_list);
7362
                                $remind_list_string = implode(',', $remind_list);
7363
                            }
7364
                        }
7365
                    }
7366
                }
7367
                $value = Database::escape_string($remind_list_string);
7368
                $sql = "UPDATE $track_exercises
7369
                        SET questions_to_check = '$value'
7370
                        WHERE exe_id = $exeId ";
7371
                Database::query($sql);
7372
            }
7373
        }
7374
    }
7375
7376
    /**
7377
     * @param string $answer
7378
     *
7379
     * @return mixed
7380
     */
7381
    public function fill_in_blank_answer_to_array($answer)
7382
    {
7383
        api_preg_match_all('/\[[^]]+\]/', $answer, $teacher_answer_list);
7384
        $teacher_answer_list = $teacher_answer_list[0];
7385
7386
        return $teacher_answer_list;
7387
    }
7388
7389
    /**
7390
     * @param string $answer
7391
     *
7392
     * @return string
7393
     */
7394
    public function fill_in_blank_answer_to_string($answer)
7395
    {
7396
        $teacher_answer_list = $this->fill_in_blank_answer_to_array($answer);
7397
        $result = '';
7398
        if (!empty($teacher_answer_list)) {
7399
            foreach ($teacher_answer_list as $teacher_item) {
7400
                // Cleaning student answer list
7401
                $value = strip_tags($teacher_item);
7402
                $value = api_substr($value, 1, api_strlen($value) - 2);
7403
                $value = explode('/', $value);
7404
                if (!empty($value[0])) {
7405
                    $value = trim($value[0]);
7406
                    $value = str_replace('&nbsp;', '', $value);
7407
                    $result .= $value;
7408
                }
7409
            }
7410
        }
7411
7412
        return $result;
7413
    }
7414
7415
    /**
7416
     * @return string
7417
     */
7418
    public function returnTimeLeftDiv()
7419
    {
7420
        $html = '<div id="clock_warning" style="display:none">';
7421
        $html .= Display::return_message(
7422
            get_lang('ReachedTimeLimit'),
7423
            'warning'
7424
        );
7425
        $html .= ' ';
7426
        $html .= sprintf(
7427
            get_lang('YouWillBeRedirectedInXSeconds'),
7428
            '<span id="counter_to_redirect" class="red_alert"></span>'
7429
        );
7430
        $html .= '</div>';
7431
7432
        $icon = Display::returnFontAwesomeIcon('clock-o');
7433
        $html .= '<div class="count_down">
7434
                    '.get_lang('RemainingTimeToFinishExercise').'
7435
                    '.$icon.'<span id="exercise_clock_warning"></span>
7436
                </div>';
7437
7438
        return $html;
7439
    }
7440
7441
    /**
7442
     * Get categories added in the exercise--category matrix.
7443
     *
7444
     * @return array
7445
     */
7446
    public function getCategoriesInExercise()
7447
    {
7448
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7449
        if (!empty($this->id)) {
7450
            $sql = "SELECT * FROM $table
7451
                    WHERE exercise_id = {$this->id} AND c_id = {$this->course_id} ";
7452
            $result = Database::query($sql);
7453
            $list = [];
7454
            if (Database::num_rows($result)) {
7455
                while ($row = Database::fetch_array($result, 'ASSOC')) {
7456
                    $list[$row['category_id']] = $row;
7457
                }
7458
7459
                return $list;
7460
            }
7461
        }
7462
7463
        return [];
7464
    }
7465
7466
    /**
7467
     * Get total number of question that will be parsed when using the category/exercise.
7468
     *
7469
     * @return int
7470
     */
7471
    public function getNumberQuestionExerciseCategory()
7472
    {
7473
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7474
        if (!empty($this->id)) {
7475
            $sql = "SELECT SUM(count_questions) count_questions
7476
                    FROM $table
7477
                    WHERE exercise_id = {$this->id} AND c_id = {$this->course_id}";
7478
            $result = Database::query($sql);
7479
            if (Database::num_rows($result)) {
7480
                $row = Database::fetch_array($result);
7481
7482
                return (int) $row['count_questions'];
7483
            }
7484
        }
7485
7486
        return 0;
7487
    }
7488
7489
    /**
7490
     * Save categories in the TABLE_QUIZ_REL_CATEGORY table.
7491
     *
7492
     * @param array $categories
7493
     */
7494
    public function save_categories_in_exercise($categories)
7495
    {
7496
        if (!empty($categories) && !empty($this->id)) {
7497
            $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
7498
            $sql = "DELETE FROM $table
7499
                    WHERE exercise_id = {$this->id} AND c_id = {$this->course_id}";
7500
            Database::query($sql);
7501
            if (!empty($categories)) {
7502
                foreach ($categories as $categoryId => $countQuestions) {
7503
                    $params = [
7504
                        'c_id' => $this->course_id,
7505
                        'exercise_id' => $this->id,
7506
                        'category_id' => $categoryId,
7507
                        'count_questions' => $countQuestions,
7508
                    ];
7509
                    Database::insert($table, $params);
7510
                }
7511
            }
7512
        }
7513
    }
7514
7515
    /**
7516
     * @param array  $questionList
7517
     * @param int    $currentQuestion
7518
     * @param array  $conditions
7519
     * @param string $link
7520
     *
7521
     * @return string
7522
     */
7523
    public function progressExercisePaginationBar(
7524
        $questionList,
7525
        $currentQuestion,
7526
        $conditions,
7527
        $link
7528
    ) {
7529
        $mediaQuestions = $this->getMediaList();
7530
7531
        $html = '<div class="exercise_pagination pagination pagination-mini"><ul>';
7532
        $counter = 0;
7533
        $nextValue = 0;
7534
        $wasMedia = false;
7535
        $before = 0;
7536
        $counterNoMedias = 0;
7537
        foreach ($questionList as $questionId) {
7538
            $isCurrent = $currentQuestion == ($counterNoMedias + 1) ? true : false;
7539
7540
            if (!empty($nextValue)) {
7541
                if ($wasMedia) {
7542
                    $nextValue = $nextValue - $before + 1;
7543
                }
7544
            }
7545
7546
            if (isset($mediaQuestions) && isset($mediaQuestions[$questionId])) {
7547
                $fixedValue = $counterNoMedias;
7548
7549
                $html .= Display::progressPaginationBar(
7550
                    $nextValue,
7551
                    $mediaQuestions[$questionId],
7552
                    $currentQuestion,
7553
                    $fixedValue,
7554
                    $conditions,
7555
                    $link,
7556
                    true,
7557
                    true
7558
                );
7559
7560
                $counter += count($mediaQuestions[$questionId]) - 1;
7561
                $before = count($questionList);
7562
                $wasMedia = true;
7563
                $nextValue += count($questionList);
7564
            } else {
7565
                $html .= Display::parsePaginationItem(
7566
                    $questionId,
7567
                    $isCurrent,
7568
                    $conditions,
7569
                    $link,
7570
                    $counter
7571
                );
7572
                $counter++;
7573
                $nextValue++;
7574
                $wasMedia = false;
7575
            }
7576
            $counterNoMedias++;
7577
        }
7578
        $html .= '</ul></div>';
7579
7580
        return $html;
7581
    }
7582
7583
    /**
7584
     *  Shows a list of numbers that represents the question to answer in a exercise.
7585
     *
7586
     * @param array  $categories
7587
     * @param int    $current
7588
     * @param array  $conditions
7589
     * @param string $link
7590
     *
7591
     * @return string
7592
     */
7593
    public function progressExercisePaginationBarWithCategories(
7594
        $categories,
7595
        $current,
7596
        $conditions = [],
7597
        $link = null
7598
    ) {
7599
        $html = null;
7600
        $counterNoMedias = 0;
7601
        $nextValue = 0;
7602
        $wasMedia = false;
7603
        $before = 0;
7604
7605
        if (!empty($categories)) {
7606
            $selectionType = $this->getQuestionSelectionType();
7607
            $useRootAsCategoryTitle = false;
7608
7609
            // Grouping questions per parent category see BT#6540
7610
            if (in_array(
7611
                $selectionType,
7612
                [
7613
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_ORDERED,
7614
                    EX_Q_SELECTION_CATEGORIES_ORDERED_BY_PARENT_QUESTIONS_RANDOM,
7615
                ]
7616
            )) {
7617
                $useRootAsCategoryTitle = true;
7618
            }
7619
7620
            // If the exercise is set to only show the titles of the categories
7621
            // at the root of the tree, then pre-order the categories tree by
7622
            // removing children and summing their questions into the parent
7623
            // categories
7624
            if ($useRootAsCategoryTitle) {
7625
                // The new categories list starts empty
7626
                $newCategoryList = [];
7627
                foreach ($categories as $category) {
7628
                    $rootElement = $category['root'];
7629
7630
                    if (isset($category['parent_info'])) {
7631
                        $rootElement = $category['parent_info']['id'];
7632
                    }
7633
7634
                    //$rootElement = $category['id'];
7635
                    // If the current category's ancestor was never seen
7636
                    // before, then declare it and assign the current
7637
                    // category to it.
7638
                    if (!isset($newCategoryList[$rootElement])) {
7639
                        $newCategoryList[$rootElement] = $category;
7640
                    } else {
7641
                        // If it was already seen, then merge the previous with
7642
                        // the current category
7643
                        $oldQuestionList = $newCategoryList[$rootElement]['question_list'];
7644
                        $category['question_list'] = array_merge($oldQuestionList, $category['question_list']);
7645
                        $newCategoryList[$rootElement] = $category;
7646
                    }
7647
                }
7648
                // Now use the newly built categories list, with only parents
7649
                $categories = $newCategoryList;
7650
            }
7651
7652
            foreach ($categories as $category) {
7653
                $questionList = $category['question_list'];
7654
                // Check if in this category there questions added in a media
7655
                $mediaQuestionId = $category['media_question'];
7656
                $isMedia = false;
7657
                $fixedValue = null;
7658
7659
                // Media exists!
7660
                if ($mediaQuestionId != 999) {
7661
                    $isMedia = true;
7662
                    $fixedValue = $counterNoMedias;
7663
                }
7664
7665
                //$categoryName = $category['path']; << show the path
7666
                $categoryName = $category['name'];
7667
7668
                if ($useRootAsCategoryTitle) {
7669
                    if (isset($category['parent_info'])) {
7670
                        $categoryName = $category['parent_info']['title'];
7671
                    }
7672
                }
7673
                $html .= '<div class="row">';
7674
                $html .= '<div class="span2">'.$categoryName.'</div>';
7675
                $html .= '<div class="span8">';
7676
7677
                if (!empty($nextValue)) {
7678
                    if ($wasMedia) {
7679
                        $nextValue = $nextValue - $before + 1;
7680
                    }
7681
                }
7682
                $html .= Display::progressPaginationBar(
7683
                    $nextValue,
7684
                    $questionList,
7685
                    $current,
7686
                    $fixedValue,
7687
                    $conditions,
7688
                    $link,
7689
                    $isMedia,
7690
                    true
7691
                );
7692
                $html .= '</div>';
7693
                $html .= '</div>';
7694
7695
                if ($mediaQuestionId == 999) {
7696
                    $counterNoMedias += count($questionList);
7697
                } else {
7698
                    $counterNoMedias++;
7699
                }
7700
7701
                $nextValue += count($questionList);
7702
                $before = count($questionList);
7703
7704
                if ($mediaQuestionId != 999) {
7705
                    $wasMedia = true;
7706
                } else {
7707
                    $wasMedia = false;
7708
                }
7709
            }
7710
        }
7711
7712
        return $html;
7713
    }
7714
7715
    /**
7716
     * Renders a question list.
7717
     *
7718
     * @param array $questionList    (with media questions compressed)
7719
     * @param int   $currentQuestion
7720
     * @param array $exerciseResult
7721
     * @param array $attemptList
7722
     * @param array $remindList
7723
     */
7724
    public function renderQuestionList(
7725
        $questionList,
7726
        $currentQuestion,
7727
        $exerciseResult,
7728
        $attemptList,
7729
        $remindList
7730
    ) {
7731
        $mediaQuestions = $this->getMediaList();
7732
        $i = 0;
7733
7734
        // Normal question list render (medias compressed)
7735
        foreach ($questionList as $questionId) {
7736
            $i++;
7737
            // For sequential exercises
7738
7739
            if ($this->type == ONE_PER_PAGE) {
7740
                // If it is not the right question, goes to the next loop iteration
7741
                if ($currentQuestion != $i) {
7742
                    continue;
7743
                } else {
7744
                    if (!in_array(
7745
                        $this->getFeedbackType(),
7746
                        [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
7747
                    )) {
7748
                        // if the user has already answered this question
7749
                        if (isset($exerciseResult[$questionId])) {
7750
                            echo Display::return_message(
7751
                                get_lang('AlreadyAnswered'),
7752
                                'normal'
7753
                            );
7754
                            break;
7755
                        }
7756
                    }
7757
                }
7758
            }
7759
7760
            // The $questionList contains the media id we check
7761
            // if this questionId is a media question type
7762
            if (isset($mediaQuestions[$questionId]) &&
7763
                $mediaQuestions[$questionId] != 999
7764
            ) {
7765
                // The question belongs to a media
7766
                $mediaQuestionList = $mediaQuestions[$questionId];
7767
                $objQuestionTmp = Question::read($questionId);
7768
7769
                $counter = 1;
7770
                if ($objQuestionTmp->type == MEDIA_QUESTION) {
7771
                    echo $objQuestionTmp->show_media_content();
7772
7773
                    $countQuestionsInsideMedia = count($mediaQuestionList);
7774
7775
                    // Show questions that belongs to a media
7776
                    if (!empty($mediaQuestionList)) {
7777
                        // In order to parse media questions we use letters a, b, c, etc.
7778
                        $letterCounter = 97;
7779
                        foreach ($mediaQuestionList as $questionIdInsideMedia) {
7780
                            $isLastQuestionInMedia = false;
7781
                            if ($counter == $countQuestionsInsideMedia) {
7782
                                $isLastQuestionInMedia = true;
7783
                            }
7784
                            $this->renderQuestion(
7785
                                $questionIdInsideMedia,
7786
                                $attemptList,
7787
                                $remindList,
7788
                                chr($letterCounter),
7789
                                $currentQuestion,
7790
                                $mediaQuestionList,
7791
                                $isLastQuestionInMedia,
7792
                                $questionList
7793
                            );
7794
                            $letterCounter++;
7795
                            $counter++;
7796
                        }
7797
                    }
7798
                } else {
7799
                    $this->renderQuestion(
7800
                        $questionId,
7801
                        $attemptList,
7802
                        $remindList,
7803
                        $i,
7804
                        $currentQuestion,
7805
                        null,
7806
                        null,
7807
                        $questionList
7808
                    );
7809
                    $i++;
7810
                }
7811
            } else {
7812
                // Normal question render.
7813
                $this->renderQuestion(
7814
                    $questionId,
7815
                    $attemptList,
7816
                    $remindList,
7817
                    $i,
7818
                    $currentQuestion,
7819
                    null,
7820
                    null,
7821
                    $questionList
7822
                );
7823
            }
7824
7825
            // For sequential exercises.
7826
            if ($this->type == ONE_PER_PAGE) {
7827
                // quits the loop
7828
                break;
7829
            }
7830
        }
7831
        // end foreach()
7832
7833
        if ($this->type == ALL_ON_ONE_PAGE) {
7834
            $exercise_actions = $this->show_button($questionId, $currentQuestion);
7835
            echo Display::div($exercise_actions, ['class' => 'exercise_actions']);
7836
        }
7837
    }
7838
7839
    /**
7840
     * @param int   $questionId
7841
     * @param array $attemptList
7842
     * @param array $remindList
7843
     * @param int   $i
7844
     * @param int   $current_question
7845
     * @param array $questions_in_media
7846
     * @param bool  $last_question_in_media
7847
     * @param array $realQuestionList
7848
     * @param bool  $generateJS
7849
     */
7850
    public function renderQuestion(
7851
        $questionId,
7852
        $attemptList,
7853
        $remindList,
7854
        $i,
7855
        $current_question,
7856
        $questions_in_media = [],
7857
        $last_question_in_media = false,
7858
        $realQuestionList = [],
7859
        $generateJS = true
7860
    ) {
7861
        // With this option on the question is loaded via AJAX
7862
        //$generateJS = true;
7863
        //$this->loadQuestionAJAX = true;
7864
7865
        if ($generateJS && $this->loadQuestionAJAX) {
7866
            $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?a=get_question&id='.$questionId.'&'.api_get_cidreq();
7867
            $params = [
7868
                'questionId' => $questionId,
7869
                'attemptList' => $attemptList,
7870
                'remindList' => $remindList,
7871
                'i' => $i,
7872
                'current_question' => $current_question,
7873
                'questions_in_media' => $questions_in_media,
7874
                'last_question_in_media' => $last_question_in_media,
7875
            ];
7876
            $params = json_encode($params);
7877
7878
            $script = '<script>
7879
            $(function(){
7880
                var params = '.$params.';
7881
                $.ajax({
7882
                    type: "GET",
7883
                    data: params,
7884
                    url: "'.$url.'",
7885
                    success: function(return_value) {
7886
                        $("#ajaxquestiondiv'.$questionId.'").html(return_value);
7887
                    }
7888
                });
7889
            });
7890
            </script>
7891
            <div id="ajaxquestiondiv'.$questionId.'"></div>';
7892
            echo $script;
7893
        } else {
7894
            global $origin;
7895
            $question_obj = Question::read($questionId);
7896
            $user_choice = isset($attemptList[$questionId]) ? $attemptList[$questionId] : null;
7897
            $remind_highlight = null;
7898
7899
            // Hides questions when reviewing a ALL_ON_ONE_PAGE exercise
7900
            // see #4542 no_remind_highlight class hide with jquery
7901
            if ($this->type == ALL_ON_ONE_PAGE && isset($_GET['reminder']) && $_GET['reminder'] == 2) {
7902
                $remind_highlight = 'no_remind_highlight';
7903
                if (in_array($question_obj->type, Question::question_type_no_review())) {
7904
                    return null;
7905
                }
7906
            }
7907
7908
            $attributes = ['id' => 'remind_list['.$questionId.']', 'data-question-id' => $questionId];
7909
7910
            // Showing the question
7911
            $exercise_actions = null;
7912
            echo '<a id="questionanchor'.$questionId.'"></a><br />';
7913
            echo '<div id="question_div_'.$questionId.'" class="main_question '.$remind_highlight.'" >';
7914
7915
            // Shows the question + possible answers
7916
            $showTitle = $this->getHideQuestionTitle() == 1 ? false : true;
7917
            echo $this->showQuestion(
7918
                $question_obj,
7919
                false,
7920
                $origin,
7921
                $i,
7922
                $showTitle,
7923
                false,
7924
                $user_choice,
7925
                false,
7926
                null,
7927
                false,
7928
                $this->getModelType(),
7929
                $this->categoryMinusOne
7930
            );
7931
7932
            // Button save and continue
7933
            switch ($this->type) {
7934
                case ONE_PER_PAGE:
7935
                    $exercise_actions .= $this->show_button(
7936
                        $questionId,
7937
                        $current_question,
7938
                        null,
7939
                        $remindList
7940
                    );
7941
                    break;
7942
                case ALL_ON_ONE_PAGE:
7943
                    if (api_is_allowed_to_session_edit()) {
7944
                        $button = [
7945
                            Display::button(
7946
                                'save_now',
7947
                                get_lang('SaveForNow'),
7948
                                ['type' => 'button', 'class' => 'btn btn-primary', 'data-question' => $questionId]
7949
                            ),
7950
                            '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>',
7951
                        ];
7952
                        $exercise_actions .= Display::div(
7953
                            implode(PHP_EOL, $button),
7954
                            ['class' => 'exercise_save_now_button']
7955
                        );
7956
                    }
7957
                    break;
7958
            }
7959
7960
            if (!empty($questions_in_media)) {
7961
                $count_of_questions_inside_media = count($questions_in_media);
7962
                if ($count_of_questions_inside_media > 1 && api_is_allowed_to_session_edit()) {
7963
                    $button = [
7964
                        Display::button(
7965
                            'save_now',
7966
                            get_lang('SaveForNow'),
7967
                            ['type' => 'button', 'class' => 'btn btn-primary', 'data-question' => $questionId]
7968
                        ),
7969
                        '<span id="save_for_now_'.$questionId.'" class="exercise_save_mini_message"></span>&nbsp;',
7970
                    ];
7971
                    $exercise_actions = Display::div(
7972
                        implode(PHP_EOL, $button),
7973
                        ['class' => 'exercise_save_now_button']
7974
                    );
7975
                }
7976
7977
                if ($last_question_in_media && $this->type == ONE_PER_PAGE) {
7978
                    $exercise_actions = $this->show_button($questionId, $current_question, $questions_in_media);
7979
                }
7980
            }
7981
7982
            // Checkbox review answers
7983
            if ($this->review_answers &&
7984
                !in_array($question_obj->type, Question::question_type_no_review())
7985
            ) {
7986
                $remind_question_div = Display::tag(
7987
                    'label',
7988
                    Display::input(
7989
                        'checkbox',
7990
                        'remind_list['.$questionId.']',
7991
                        '',
7992
                        $attributes
7993
                    ).get_lang('ReviewQuestionLater'),
7994
                    [
7995
                        'class' => 'checkbox',
7996
                        'for' => 'remind_list['.$questionId.']',
7997
                    ]
7998
                );
7999
                $exercise_actions .= Display::div(
8000
                    $remind_question_div,
8001
                    ['class' => 'exercise_save_now_button']
8002
                );
8003
            }
8004
8005
            echo Display::div(' ', ['class' => 'clear']);
8006
8007
            $paginationCounter = null;
8008
            if ($this->type == ONE_PER_PAGE) {
8009
                if (empty($questions_in_media)) {
8010
                    $paginationCounter = Display::paginationIndicator(
8011
                        $current_question,
8012
                        count($realQuestionList)
8013
                    );
8014
                } else {
8015
                    if ($last_question_in_media) {
8016
                        $paginationCounter = Display::paginationIndicator(
8017
                            $current_question,
8018
                            count($realQuestionList)
8019
                        );
8020
                    }
8021
                }
8022
            }
8023
8024
            echo '<div class="row"><div class="pull-right">'.$paginationCounter.'</div></div>';
8025
            echo Display::div($exercise_actions, ['class' => 'form-actions']);
8026
            echo '</div>';
8027
        }
8028
    }
8029
8030
    /**
8031
     * Returns an array of categories details for the questions of the current
8032
     * exercise.
8033
     *
8034
     * @return array
8035
     */
8036
    public function getQuestionWithCategories()
8037
    {
8038
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
8039
        $categoryRelTable = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
8040
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8041
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
8042
        $sql = "SELECT DISTINCT cat.*
8043
                FROM $TBL_EXERCICE_QUESTION e
8044
                INNER JOIN $TBL_QUESTIONS q
8045
                ON (e.question_id = q.id AND e.c_id = q.c_id)
8046
                INNER JOIN $categoryRelTable catRel
8047
                ON (catRel.question_id = e.question_id AND catRel.c_id = e.c_id)
8048
                INNER JOIN $categoryTable cat
8049
                ON (cat.id = catRel.category_id AND cat.c_id = e.c_id)
8050
                WHERE
8051
                  e.c_id = {$this->course_id} AND
8052
                  e.exercice_id	= ".intval($this->id);
8053
8054
        $result = Database::query($sql);
8055
        $categoriesInExercise = [];
8056
        if (Database::num_rows($result)) {
8057
            $categoriesInExercise = Database::store_result($result, 'ASSOC');
8058
        }
8059
8060
        return $categoriesInExercise;
8061
    }
8062
8063
    /**
8064
     * Calculate the max_score of the quiz, depending of question inside, and quiz advanced option.
8065
     */
8066
    public function get_max_score()
8067
    {
8068
        $out_max_score = 0;
8069
        // list of question's id !!! the array key start at 1 !!!
8070
        $questionList = $this->selectQuestionList(true);
8071
8072
        // test is randomQuestions - see field random of test
8073
        if ($this->random > 0 && 0 == $this->randomByCat) {
8074
            $numberRandomQuestions = $this->random;
8075
            $questionScoreList = [];
8076
            foreach ($questionList as $questionId) {
8077
                $tmpobj_question = Question::read($questionId);
8078
                if (is_object($tmpobj_question)) {
8079
                    $questionScoreList[] = $tmpobj_question->weighting;
8080
                }
8081
            }
8082
8083
            rsort($questionScoreList);
8084
            // add the first $numberRandomQuestions value of score array to get max_score
8085
            for ($i = 0; $i < min($numberRandomQuestions, count($questionScoreList)); $i++) {
8086
                $out_max_score += $questionScoreList[$i];
8087
            }
8088
        } elseif ($this->random > 0 && $this->randomByCat > 0) {
8089
            // test is random by category
8090
            // get the $numberRandomQuestions best score question of each category
8091
            $numberRandomQuestions = $this->random;
8092
            $tab_categories_scores = [];
8093
            foreach ($questionList as $questionId) {
8094
                $question_category_id = TestCategory::getCategoryForQuestion($questionId);
8095
                if (!is_array($tab_categories_scores[$question_category_id])) {
8096
                    $tab_categories_scores[$question_category_id] = [];
8097
                }
8098
                $tmpobj_question = Question::read($questionId);
8099
                if (is_object($tmpobj_question)) {
8100
                    $tab_categories_scores[$question_category_id][] = $tmpobj_question->weighting;
8101
                }
8102
            }
8103
8104
            // here we've got an array with first key, the category_id, second key, score of question for this cat
8105
            foreach ($tab_categories_scores as $tab_scores) {
8106
                rsort($tab_scores);
8107
                for ($i = 0; $i < min($numberRandomQuestions, count($tab_scores)); $i++) {
8108
                    $out_max_score += $tab_scores[$i];
8109
                }
8110
            }
8111
        } else {
8112
            // standard test, just add each question score
8113
            foreach ($questionList as $questionId) {
8114
                $question = Question::read($questionId, $this->course);
8115
                $out_max_score += $question->weighting;
8116
            }
8117
        }
8118
8119
        return $out_max_score;
8120
    }
8121
8122
    /**
8123
     * @return string
8124
     */
8125
    public function get_formated_title()
8126
    {
8127
        if (api_get_configuration_value('save_titles_as_html')) {
8128
        }
8129
8130
        return api_html_entity_decode($this->selectTitle());
8131
    }
8132
8133
    /**
8134
     * @param string $title
8135
     *
8136
     * @return string
8137
     */
8138
    public static function get_formated_title_variable($title)
8139
    {
8140
        return api_html_entity_decode($title);
8141
    }
8142
8143
    /**
8144
     * @return string
8145
     */
8146
    public function format_title()
8147
    {
8148
        return api_htmlentities($this->title);
8149
    }
8150
8151
    /**
8152
     * @param string $title
8153
     *
8154
     * @return string
8155
     */
8156
    public static function format_title_variable($title)
8157
    {
8158
        return api_htmlentities($title);
8159
    }
8160
8161
    /**
8162
     * @param int $courseId
8163
     * @param int $sessionId
8164
     *
8165
     * @return array exercises
8166
     */
8167
    public function getExercisesByCourseSession($courseId, $sessionId)
8168
    {
8169
        $courseId = (int) $courseId;
8170
        $sessionId = (int) $sessionId;
8171
8172
        $tbl_quiz = Database::get_course_table(TABLE_QUIZ_TEST);
8173
        $sql = "SELECT * FROM $tbl_quiz cq
8174
                WHERE
8175
                    cq.c_id = %s AND
8176
                    (cq.session_id = %s OR cq.session_id = 0) AND
8177
                    cq.active = 0
8178
                ORDER BY cq.id";
8179
        $sql = sprintf($sql, $courseId, $sessionId);
8180
8181
        $result = Database::query($sql);
8182
8183
        $rows = [];
8184
        while ($row = Database::fetch_array($result, 'ASSOC')) {
8185
            $rows[] = $row;
8186
        }
8187
8188
        return $rows;
8189
    }
8190
8191
    /**
8192
     * @param int   $courseId
8193
     * @param int   $sessionId
8194
     * @param array $quizId
8195
     *
8196
     * @return array exercises
8197
     */
8198
    public function getExerciseAndResult($courseId, $sessionId, $quizId = [])
8199
    {
8200
        if (empty($quizId)) {
8201
            return [];
8202
        }
8203
8204
        $sessionId = (int) $sessionId;
8205
        $courseId = (int) $courseId;
8206
8207
        $ids = is_array($quizId) ? $quizId : [$quizId];
8208
        $ids = array_map('intval', $ids);
8209
        $ids = implode(',', $ids);
8210
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8211
        if (0 != $sessionId) {
8212
            $sql = "SELECT * FROM $track_exercises te
8213
              INNER JOIN c_quiz cq ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id
8214
              WHERE
8215
              te.id = %s AND
8216
              te.session_id = %s AND
8217
              cq.id IN (%s)
8218
              ORDER BY cq.id";
8219
8220
            $sql = sprintf($sql, $courseId, $sessionId, $ids);
8221
        } else {
8222
            $sql = "SELECT * FROM $track_exercises te
8223
              INNER JOIN c_quiz cq ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id
8224
              WHERE
8225
              te.id = %s AND
8226
              cq.id IN (%s)
8227
              ORDER BY cq.id";
8228
            $sql = sprintf($sql, $courseId, $ids);
8229
        }
8230
        $result = Database::query($sql);
8231
        $rows = [];
8232
        while ($row = Database::fetch_array($result, 'ASSOC')) {
8233
            $rows[] = $row;
8234
        }
8235
8236
        return $rows;
8237
    }
8238
8239
    /**
8240
     * @param $exeId
8241
     * @param $exercise_stat_info
8242
     * @param $remindList
8243
     * @param $currentQuestion
8244
     *
8245
     * @return int|null
8246
     */
8247
    public static function getNextQuestionId(
8248
        $exeId,
8249
        $exercise_stat_info,
8250
        $remindList,
8251
        $currentQuestion
8252
    ) {
8253
        $result = Event::get_exercise_results_by_attempt($exeId, 'incomplete');
8254
8255
        if (isset($result[$exeId])) {
8256
            $result = $result[$exeId];
8257
        } else {
8258
            return null;
8259
        }
8260
8261
        $data_tracking = $exercise_stat_info['data_tracking'];
8262
        $data_tracking = explode(',', $data_tracking);
8263
8264
        // if this is the final question do nothing.
8265
        if ($currentQuestion == count($data_tracking)) {
8266
            return null;
8267
        }
8268
8269
        $currentQuestion--;
8270
8271
        if (!empty($result['question_list'])) {
8272
            $answeredQuestions = [];
8273
            foreach ($result['question_list'] as $question) {
8274
                if (!empty($question['answer'])) {
8275
                    $answeredQuestions[] = $question['question_id'];
8276
                }
8277
            }
8278
8279
            // Checking answered questions
8280
            $counterAnsweredQuestions = 0;
8281
            foreach ($data_tracking as $questionId) {
8282
                if (!in_array($questionId, $answeredQuestions)) {
8283
                    if ($currentQuestion != $counterAnsweredQuestions) {
8284
                        break;
8285
                    }
8286
                }
8287
                $counterAnsweredQuestions++;
8288
            }
8289
8290
            $counterRemindListQuestions = 0;
8291
            // Checking questions saved in the reminder list
8292
            if (!empty($remindList)) {
8293
                foreach ($data_tracking as $questionId) {
8294
                    if (in_array($questionId, $remindList)) {
8295
                        // Skip the current question
8296
                        if ($currentQuestion != $counterRemindListQuestions) {
8297
                            break;
8298
                        }
8299
                    }
8300
                    $counterRemindListQuestions++;
8301
                }
8302
8303
                if ($counterRemindListQuestions < $currentQuestion) {
8304
                    return null;
8305
                }
8306
8307
                if (!empty($counterRemindListQuestions)) {
8308
                    if ($counterRemindListQuestions > $counterAnsweredQuestions) {
8309
                        return $counterAnsweredQuestions;
8310
                    } else {
8311
                        return $counterRemindListQuestions;
8312
                    }
8313
                }
8314
            }
8315
8316
            return $counterAnsweredQuestions;
8317
        }
8318
    }
8319
8320
    /**
8321
     * Gets the position of a questionId in the question list.
8322
     *
8323
     * @param $questionId
8324
     *
8325
     * @return int
8326
     */
8327
    public function getPositionInCompressedQuestionList($questionId)
8328
    {
8329
        $questionList = $this->getQuestionListWithMediasCompressed();
8330
        $mediaQuestions = $this->getMediaList();
8331
        $position = 1;
8332
        foreach ($questionList as $id) {
8333
            if (isset($mediaQuestions[$id]) && in_array($questionId, $mediaQuestions[$id])) {
8334
                $mediaQuestionList = $mediaQuestions[$id];
8335
                if (in_array($questionId, $mediaQuestionList)) {
8336
                    return $position;
8337
                } else {
8338
                    $position++;
8339
                }
8340
            } else {
8341
                if ($id == $questionId) {
8342
                    return $position;
8343
                } else {
8344
                    $position++;
8345
                }
8346
            }
8347
        }
8348
8349
        return 1;
8350
    }
8351
8352
    /**
8353
     * Get the correct answers in all attempts.
8354
     *
8355
     * @param int  $learnPathId
8356
     * @param int  $learnPathItemId
8357
     * @param bool $onlyCorrect
8358
     *
8359
     * @return array
8360
     */
8361
    public function getAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0, $onlyCorrect = true)
8362
    {
8363
        $attempts = Event::getExerciseResultsByUser(
8364
            api_get_user_id(),
8365
            $this->id,
8366
            api_get_course_int_id(),
8367
            api_get_session_id(),
8368
            $learnPathId,
8369
            $learnPathItemId,
8370
            'DESC'
8371
        );
8372
8373
        $list = [];
8374
        foreach ($attempts as $attempt) {
8375
            foreach ($attempt['question_list'] as $answers) {
8376
                foreach ($answers as $answer) {
8377
                    $objAnswer = new Answer($answer['question_id']);
8378
                    if ($onlyCorrect) {
8379
                        switch ($objAnswer->getQuestionType()) {
8380
                            case FILL_IN_BLANKS:
8381
                                $isCorrect = FillBlanks::isCorrect($answer['answer']);
8382
                                break;
8383
                            case MATCHING:
8384
                            case DRAGGABLE:
8385
                            case MATCHING_DRAGGABLE:
8386
                                $isCorrect = Matching::isCorrect(
8387
                                    $answer['position'],
8388
                                    $answer['answer'],
8389
                                    $answer['question_id']
8390
                                );
8391
                                break;
8392
                            case ORAL_EXPRESSION:
8393
                                $isCorrect = false;
8394
                                break;
8395
                            default:
8396
                                $isCorrect = $objAnswer->isCorrectByAutoId($answer['answer']);
8397
                        }
8398
                        if ($isCorrect) {
8399
                            $list[$answer['question_id']][] = $answer;
8400
                        }
8401
                    } else {
8402
                        $list[$answer['question_id']][] = $answer;
8403
                    }
8404
                }
8405
            }
8406
8407
            if (false === $onlyCorrect) {
8408
                // Only take latest attempt
8409
                break;
8410
            }
8411
        }
8412
8413
        return $list;
8414
    }
8415
8416
    /**
8417
     * Get the correct answers in all attempts.
8418
     *
8419
     * @param int $learnPathId
8420
     * @param int $learnPathItemId
8421
     *
8422
     * @return array
8423
     */
8424
    public function getCorrectAnswersInAllAttempts($learnPathId = 0, $learnPathItemId = 0)
8425
    {
8426
        return $this->getAnswersInAllAttempts($learnPathId, $learnPathItemId);
8427
    }
8428
8429
    /**
8430
     * @return bool
8431
     */
8432
    public function showPreviousButton()
8433
    {
8434
        $allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
8435
        if (false === $allow) {
8436
            return true;
8437
        }
8438
8439
        return $this->showPreviousButton;
8440
    }
8441
8442
    public function getPreventBackwards()
8443
    {
8444
        $allow = api_get_configuration_value('quiz_prevent_backwards_move');
8445
        if (false === $allow) {
8446
            return 0;
8447
        }
8448
8449
        return (int) $this->preventBackwards;
8450
    }
8451
8452
    /**
8453
     * @return int
8454
     */
8455
    public function getExerciseCategoryId()
8456
    {
8457
        if (empty($this->exerciseCategoryId)) {
8458
            return null;
8459
        }
8460
8461
        return (int) $this->exerciseCategoryId;
8462
    }
8463
8464
    /**
8465
     * @param int $value
8466
     */
8467
    public function setExerciseCategoryId($value)
8468
    {
8469
        if (!empty($value)) {
8470
            $this->exerciseCategoryId = (int) $value;
8471
        }
8472
    }
8473
8474
    /**
8475
     * Set the value to 1 to hide the question number.
8476
     *
8477
     * @param int $value
8478
     */
8479
    public function setHideQuestionNumber($value = 0)
8480
    {
8481
        $showHideConfiguration = api_get_configuration_value('quiz_hide_question_number');
8482
        if ($showHideConfiguration) {
8483
            $this->hideQuestionNumber = (int) $value;
8484
        }
8485
    }
8486
8487
    /**
8488
     * Gets the value to hide or show the question number. If it does not exist, it is set to 0.
8489
     *
8490
     * @return int 1 if the question number must be hidden
8491
     */
8492
    public function getHideQuestionNumber()
8493
    {
8494
        $showHideConfiguration = api_get_configuration_value('quiz_hide_question_number');
8495
        if ($showHideConfiguration) {
8496
            return (int) $this->hideQuestionNumber;
8497
        }
8498
8499
        return 0;
8500
    }
8501
8502
    /**
8503
     * @param array $values
8504
     */
8505
    public function setPageResultConfiguration($values)
8506
    {
8507
        $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
8508
        if ($pageConfig) {
8509
            $params = [
8510
                'hide_expected_answer' => $values['hide_expected_answer'] ?? '',
8511
                'hide_question_score' => $values['hide_question_score'] ?? '',
8512
                'hide_total_score' => $values['hide_total_score'] ?? '',
8513
                'hide_category_table' => $values['hide_category_table'] ?? '',
8514
                'hide_correct_answered_questions' => $values['hide_correct_answered_questions'] ?? '',
8515
            ];
8516
            $type = Type::getType('array');
8517
            $platform = Database::getManager()->getConnection()->getDatabasePlatform();
8518
            $this->pageResultConfiguration = $type->convertToDatabaseValue($params, $platform);
8519
        }
8520
    }
8521
8522
    /**
8523
     * @param array $defaults
8524
     */
8525
    public function setPageResultConfigurationDefaults(&$defaults)
8526
    {
8527
        $configuration = $this->getPageResultConfiguration();
8528
        if (!empty($configuration) && !empty($defaults)) {
8529
            $defaults = array_merge($defaults, $configuration);
8530
        }
8531
    }
8532
8533
    /**
8534
     * Sets the value to show or hide the question number in the default settings of the forms.
8535
     *
8536
     * @param array $defaults
8537
     */
8538
    public function setHideQuestionNumberDefaults(&$defaults)
8539
    {
8540
        $configuration = $this->getHideQuestionNumberConfiguration();
8541
        if (!empty($configuration) && !empty($defaults)) {
8542
            $defaults = array_merge($defaults, $configuration);
8543
        }
8544
    }
8545
8546
    /**
8547
     * @return array
8548
     */
8549
    public function getPageResultConfiguration()
8550
    {
8551
        $pageConfig = api_get_configuration_value('allow_quiz_results_page_config');
8552
        if ($pageConfig) {
8553
            $type = Type::getType('array');
8554
            $platform = Database::getManager()->getConnection()->getDatabasePlatform();
8555
8556
            return $type->convertToPHPValue($this->pageResultConfiguration, $platform);
8557
        }
8558
8559
        return [];
8560
    }
8561
8562
    /**
8563
     * Get the value to show or hide the question number in the default settings of the forms.
8564
     *
8565
     * @return array
8566
     */
8567
    public function getHideQuestionNumberConfiguration()
8568
    {
8569
        $pageConfig = api_get_configuration_value('quiz_hide_question_number');
8570
        if ($pageConfig) {
8571
            return ['hide_question_number' => $this->hideQuestionNumber];
8572
        }
8573
8574
        return [];
8575
    }
8576
8577
    /**
8578
     * @param string $attribute
8579
     *
8580
     * @return mixed|null
8581
     */
8582
    public function getPageConfigurationAttribute($attribute)
8583
    {
8584
        $result = $this->getPageResultConfiguration();
8585
8586
        if (!empty($result)) {
8587
            return isset($result[$attribute]) ? $result[$attribute] : null;
8588
        }
8589
8590
        return null;
8591
    }
8592
8593
    /**
8594
     * @param bool $showPreviousButton
8595
     *
8596
     * @return Exercise
8597
     */
8598
    public function setShowPreviousButton($showPreviousButton)
8599
    {
8600
        $this->showPreviousButton = $showPreviousButton;
8601
8602
        return $this;
8603
    }
8604
8605
    /**
8606
     * @param array $notifications
8607
     */
8608
    public function setNotifications($notifications)
8609
    {
8610
        $this->notifications = $notifications;
8611
    }
8612
8613
    /**
8614
     * @return array
8615
     */
8616
    public function getNotifications()
8617
    {
8618
        return $this->notifications;
8619
    }
8620
8621
    /**
8622
     * @return bool
8623
     */
8624
    public function showExpectedChoice()
8625
    {
8626
        return api_get_configuration_value('show_exercise_expected_choice');
8627
    }
8628
8629
    /**
8630
     * @return bool
8631
     */
8632
    public function showExpectedChoiceColumn()
8633
    {
8634
        if (true === $this->forceShowExpectedChoiceColumn) {
8635
            return true;
8636
        }
8637
8638
        if ($this->hideExpectedAnswer) {
8639
            return false;
8640
        }
8641
8642
        if (!in_array($this->results_disabled, [
8643
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
8644
        ])
8645
        ) {
8646
            $hide = (int) $this->getPageConfigurationAttribute('hide_expected_answer');
8647
            if (1 === $hide) {
8648
                return false;
8649
            }
8650
8651
            return true;
8652
        }
8653
8654
        return false;
8655
    }
8656
8657
    /**
8658
     * @param string $class
8659
     * @param string $scoreLabel
8660
     * @param string $result
8661
     * @param array
8662
     *
8663
     * @return string
8664
     */
8665
    public function getQuestionRibbon($class, $scoreLabel, $result, $array)
8666
    {
8667
        $hide = (int) $this->getPageConfigurationAttribute('hide_question_score');
8668
        if (1 === $hide) {
8669
            return '';
8670
        }
8671
8672
        if ($this->showExpectedChoice()) {
8673
            $html = null;
8674
            $hideLabel = api_get_configuration_value('exercise_hide_label');
8675
            $label = '<div class="rib rib-'.$class.'">
8676
                        <h3>'.$scoreLabel.'</h3>
8677
                      </div>';
8678
            if (!empty($result)) {
8679
                $label .= '<h4>'.get_lang('Score').': '.$result.'</h4>';
8680
            }
8681
            if (true === $hideLabel) {
8682
                $answerUsed = (int) $array['used'];
8683
                $answerMissing = (int) $array['missing'] - $answerUsed;
8684
                for ($i = 1; $i <= $answerUsed; $i++) {
8685
                    $html .= '<span class="score-img">'.
8686
                        Display::return_icon('attempt-check.png', null, null, ICON_SIZE_SMALL).
8687
                        '</span>';
8688
                }
8689
                for ($i = 1; $i <= $answerMissing; $i++) {
8690
                    $html .= '<span class="score-img">'.
8691
                        Display::return_icon('attempt-nocheck.png', null, null, ICON_SIZE_SMALL).
8692
                        '</span>';
8693
                }
8694
                $label = '<div class="score-title">'.get_lang('CorrectAnswers').': '.$result.'</div>';
8695
                $label .= '<div class="score-limits">';
8696
                $label .= $html;
8697
                $label .= '</div>';
8698
            }
8699
8700
            return '<div class="ribbon">
8701
                '.$label.'
8702
                </div>'
8703
                ;
8704
        } else {
8705
            $html = '<div class="ribbon">
8706
                        <div class="rib rib-'.$class.'">
8707
                            <h3>'.$scoreLabel.'</h3>
8708
                        </div>';
8709
            if (!empty($result)) {
8710
                $html .= '<h4>'.get_lang('Score').': '.$result.'</h4>';
8711
            }
8712
            $html .= '</div>';
8713
8714
            return $html;
8715
        }
8716
    }
8717
8718
    /**
8719
     * @return int
8720
     */
8721
    public function getAutoLaunch()
8722
    {
8723
        return $this->autolaunch;
8724
    }
8725
8726
    /**
8727
     * Clean auto launch settings for all exercise in course/course-session.
8728
     */
8729
    public function enableAutoLaunch()
8730
    {
8731
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8732
        $sql = "UPDATE $table SET autolaunch = 1
8733
                WHERE iid = ".$this->iId;
8734
        Database::query($sql);
8735
    }
8736
8737
    /**
8738
     * Clean auto launch settings for all exercise in course/course-session.
8739
     */
8740
    public function cleanCourseLaunchSettings()
8741
    {
8742
        $table = Database::get_course_table(TABLE_QUIZ_TEST);
8743
        $sql = "UPDATE $table SET autolaunch = 0
8744
                WHERE c_id = ".$this->course_id." AND session_id = ".$this->sessionId;
8745
        Database::query($sql);
8746
    }
8747
8748
    /**
8749
     * Get the title without HTML tags.
8750
     *
8751
     * @return string
8752
     */
8753
    public function getUnformattedTitle()
8754
    {
8755
        return strip_tags(api_html_entity_decode($this->title));
8756
    }
8757
8758
    /**
8759
     * Get the question IDs from quiz_rel_question for the current quiz,
8760
     * using the parameters as the arguments to the SQL's LIMIT clause.
8761
     * Because the exercise_id is known, it also comes with a filter on
8762
     * the session, so sessions are not specified here.
8763
     *
8764
     * @param int $start  At which question do we want to start the list
8765
     * @param int $length Up to how many results we want
8766
     *
8767
     * @return array A list of question IDs
8768
     */
8769
    public function getQuestionForTeacher($start = 0, $length = 10)
8770
    {
8771
        $start = (int) $start;
8772
        if ($start < 0) {
8773
            $start = 0;
8774
        }
8775
8776
        $length = (int) $length;
8777
8778
        $quizRelQuestion = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8779
        $question = Database::get_course_table(TABLE_QUIZ_QUESTION);
8780
        $sql = "SELECT DISTINCT e.question_id
8781
                FROM $quizRelQuestion e
8782
                INNER JOIN $question q
8783
                ON (e.question_id = q.id AND e.c_id = q.c_id)
8784
                WHERE
8785
                    e.c_id = {$this->course_id} AND
8786
                    e.exercice_id = '".$this->id."'
8787
                ORDER BY question_order
8788
                LIMIT $start, $length
8789
            ";
8790
        $result = Database::query($sql);
8791
        $questionList = [];
8792
        while ($object = Database::fetch_object($result)) {
8793
            $questionList[] = $object->question_id;
8794
        }
8795
8796
        return $questionList;
8797
    }
8798
8799
    /**
8800
     * @param int   $exerciseId
8801
     * @param array $courseInfo
8802
     * @param int   $sessionId
8803
     *
8804
     * @return bool
8805
     */
8806
    public function generateStats($exerciseId, $courseInfo, $sessionId)
8807
    {
8808
        $allowStats = api_get_configuration_value('allow_gradebook_stats');
8809
        if (!$allowStats) {
8810
            return false;
8811
        }
8812
8813
        if (empty($courseInfo)) {
8814
            return false;
8815
        }
8816
8817
        $courseId = $courseInfo['real_id'];
8818
8819
        $sessionId = (int) $sessionId;
8820
        $exerciseId = (int) $exerciseId;
8821
8822
        $result = $this->read($exerciseId);
8823
8824
        if (empty($result)) {
8825
            api_not_allowed(true);
8826
        }
8827
8828
        $statusToFilter = empty($sessionId) ? STUDENT : 0;
8829
8830
        $studentList = CourseManager::get_user_list_from_course_code(
8831
            $courseInfo['code'],
8832
            $sessionId,
8833
            null,
8834
            null,
8835
            $statusToFilter
8836
        );
8837
8838
        if (empty($studentList)) {
8839
            Display::addFlash(Display::return_message(get_lang('NoUsersInCourse')));
8840
            header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
8841
            exit;
8842
        }
8843
8844
        $tblStats = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8845
8846
        $studentIdList = [];
8847
        if (!empty($studentList)) {
8848
            $studentIdList = array_column($studentList, 'user_id');
8849
        }
8850
8851
        if (false == $this->exercise_was_added_in_lp) {
8852
            $sql = "SELECT * FROM $tblStats
8853
                        WHERE
8854
                            exe_exo_id = $exerciseId AND
8855
                            orig_lp_id = 0 AND
8856
                            orig_lp_item_id = 0 AND
8857
                            status <> 'incomplete' AND
8858
                            session_id = $sessionId AND
8859
                            c_id = $courseId
8860
                        ";
8861
        } else {
8862
            $lpId = null;
8863
            if (!empty($this->lpList)) {
8864
                // Taking only the first LP
8865
                $lpId = $this->getLpBySession($sessionId);
8866
                $lpId = $lpId['lp_id'];
8867
            }
8868
8869
            $sql = "SELECT *
8870
                        FROM $tblStats
8871
                        WHERE
8872
                            exe_exo_id = $exerciseId AND
8873
                            orig_lp_id = $lpId AND
8874
                            status <> 'incomplete' AND
8875
                            session_id = $sessionId AND
8876
                            c_id = $courseId ";
8877
        }
8878
8879
        $sql .= ' ORDER BY exe_id DESC';
8880
8881
        $studentCount = 0;
8882
        $sum = 0;
8883
        $bestResult = 0;
8884
        $sumResult = 0;
8885
        $result = Database::query($sql);
8886
        $students = [];
8887
        while ($data = Database::fetch_array($result, 'ASSOC')) {
8888
            // Only take into account users in the current student list.
8889
            if (!empty($studentIdList)) {
8890
                if (!in_array($data['exe_user_id'], $studentIdList)) {
8891
                    continue;
8892
                }
8893
            }
8894
8895
            if (!isset($students[$data['exe_user_id']])) {
8896
                if ($data['exe_weighting'] != 0) {
8897
                    $students[$data['exe_user_id']] = $data['exe_result'];
8898
                    if ($data['exe_result'] > $bestResult) {
8899
                        $bestResult = $data['exe_result'];
8900
                    }
8901
                    $sumResult += $data['exe_result'];
8902
                }
8903
            }
8904
        }
8905
8906
        $count = count($studentList);
8907
        $average = $sumResult / $count;
8908
        $em = Database::getManager();
8909
8910
        $links = AbstractLink::getGradebookLinksFromItem(
8911
            $this->id,
8912
            LINK_EXERCISE,
8913
            $courseInfo['code'],
8914
            $sessionId
8915
        );
8916
8917
        if (empty($links)) {
8918
            $links = AbstractLink::getGradebookLinksFromItem(
8919
                $this->iId,
8920
                LINK_EXERCISE,
8921
                $courseInfo['code'],
8922
                $sessionId
8923
            );
8924
        }
8925
8926
        if (!empty($links)) {
8927
            $repo = $em->getRepository('ChamiloCoreBundle:GradebookLink');
8928
8929
            foreach ($links as $link) {
8930
                $linkId = $link['id'];
8931
                /** @var GradebookLink $exerciseLink */
8932
                $exerciseLink = $repo->find($linkId);
8933
                if ($exerciseLink) {
8934
                    $exerciseLink
8935
                        ->setUserScoreList($students)
8936
                        ->setBestScore($bestResult)
8937
                        ->setAverageScore($average)
8938
                        ->setScoreWeight($this->get_max_score());
8939
                    $em->persist($exerciseLink);
8940
                    $em->flush();
8941
                }
8942
            }
8943
        }
8944
    }
8945
8946
    /**
8947
     * Return an HTML table of exercises for on-screen printing, including
8948
     * action icons. If no exercise is present and the user can edit the
8949
     * course, show a "create test" button.
8950
     *
8951
     * @param int    $categoryId
8952
     * @param string $keyword
8953
     * @param int    $userId
8954
     * @param int    $courseId
8955
     * @param int    $sessionId
8956
     * @param bool   $returnData
8957
     * @param int    $minCategoriesInExercise
8958
     * @param int    $filterByResultDisabled
8959
     * @param int    $filterByAttempt
8960
     *
8961
     * @return string|SortableTableFromArrayConfig
8962
     */
8963
    public static function exerciseGrid(
8964
        $categoryId,
8965
        $keyword = '',
8966
        $userId = 0,
8967
        $courseId = 0,
8968
        $sessionId = 0,
8969
        $returnData = false,
8970
        $minCategoriesInExercise = 0,
8971
        $filterByResultDisabled = 0,
8972
        $filterByAttempt = 0,
8973
        $myActions = null,
8974
        $returnTable = false
8975
    ) {
8976
        //$allowDelete = Exercise::allowAction('delete');
8977
        $allowClean = self::allowAction('clean_results');
8978
8979
        $TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
8980
        $TBL_ITEM_PROPERTY = Database::get_course_table(TABLE_ITEM_PROPERTY);
8981
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
8982
        $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
8983
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
8984
8985
        $categoryId = (int) $categoryId;
8986
        $keyword = Database::escape_string($keyword);
8987
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
8988
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
8989
8990
        $autoLaunchAvailable = false;
8991
        if (api_get_course_setting('enable_exercise_auto_launch') == 1 &&
8992
            api_get_configuration_value('allow_exercise_auto_launch')
8993
        ) {
8994
            $autoLaunchAvailable = true;
8995
        }
8996
8997
        $is_allowedToEdit = api_is_allowed_to_edit(null, true);
8998
        $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : api_get_course_info();
8999
        $sessionId = $sessionId ? (int) $sessionId : api_get_session_id();
9000
        $courseId = $courseInfo['real_id'];
9001
        $tableRows = [];
9002
        $uploadPath = DIR_HOTPOTATOES; //defined in main_api
9003
        $exercisePath = api_get_self();
9004
        $origin = api_get_origin();
9005
        $userInfo = $userId ? api_get_user_info($userId) : api_get_user_info();
9006
        $charset = 'utf-8';
9007
        $token = Security::get_token();
9008
        $userId = $userId ? (int) $userId : api_get_user_id();
9009
        $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh($userId, $courseInfo);
9010
        $documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
9011
        $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
9012
9013
        // Condition for the session
9014
        $condition_session = api_get_session_condition($sessionId, true, true, 'e.session_id');
9015
        $content = '';
9016
        $column = 0;
9017
        if ($is_allowedToEdit) {
9018
            $column = 1;
9019
        }
9020
9021
        $table = new SortableTableFromArrayConfig(
9022
            [],
9023
            $column,
9024
            self::PAGINATION_ITEMS_PER_PAGE,
9025
            'exercises_cat_'.$categoryId
9026
        );
9027
9028
        $limit = $table->per_page;
9029
        $page = $table->page_nr;
9030
        $from = $limit * ($page - 1);
9031
9032
        $categoryCondition = '';
9033
        if (api_get_configuration_value('allow_exercise_categories')) {
9034
            if (!empty($categoryId)) {
9035
                $categoryCondition = " AND exercise_category_id = $categoryId ";
9036
            } else {
9037
                $categoryCondition = ' AND exercise_category_id IS NULL ';
9038
            }
9039
        }
9040
9041
        $keywordCondition = '';
9042
        if (!empty($keyword)) {
9043
            $keywordCondition = " AND title LIKE '%$keyword%' ";
9044
        }
9045
9046
        $filterByResultDisabledCondition = '';
9047
        $filterByResultDisabled = (int) $filterByResultDisabled;
9048
        if (!empty($filterByResultDisabled)) {
9049
            $filterByResultDisabledCondition = ' AND e.results_disabled = '.$filterByResultDisabled;
9050
        }
9051
        $filterByAttemptCondition = '';
9052
        $filterByAttempt = (int) $filterByAttempt;
9053
        if (!empty($filterByAttempt)) {
9054
            $filterByAttemptCondition = ' AND e.max_attempt = '.$filterByAttempt;
9055
        }
9056
9057
        // Only for administrators
9058
        if ($is_allowedToEdit) {
9059
            $total_sql = "SELECT count(iid) as count
9060
                          FROM $TBL_EXERCISES e
9061
                          WHERE
9062
                                c_id = $courseId AND
9063
                                active <> -1
9064
                                $condition_session
9065
                                $categoryCondition
9066
                                $keywordCondition
9067
                                $filterByResultDisabledCondition
9068
                                $filterByAttemptCondition
9069
                                ";
9070
            $sql = "SELECT * FROM $TBL_EXERCISES e
9071
                    WHERE
9072
                        c_id = $courseId AND
9073
                        active <> -1
9074
                        $condition_session
9075
                        $categoryCondition
9076
                        $keywordCondition
9077
                        $filterByResultDisabledCondition
9078
                        $filterByAttemptCondition
9079
                    ORDER BY title
9080
                    LIMIT $from , $limit";
9081
        } else {
9082
            // Only for students
9083
            if (empty($sessionId)) {
9084
                $condition_session = ' AND ( session_id = 0 OR session_id IS NULL) ';
9085
                $total_sql = "SELECT count(DISTINCT(e.iid)) as count
9086
                              FROM $TBL_EXERCISES e
9087
                              WHERE
9088
                                    e.c_id = $courseId AND
9089
                                    e.active = 1
9090
                                    $condition_session
9091
                                    $categoryCondition
9092
                                    $keywordCondition
9093
                              ";
9094
9095
                $sql = "SELECT DISTINCT e.* FROM $TBL_EXERCISES e
9096
                        WHERE
9097
                             e.c_id = $courseId AND
9098
                             e.active = 1
9099
                             $condition_session
9100
                             $categoryCondition
9101
                             $keywordCondition
9102
                        ORDER BY title
9103
                        LIMIT $from , $limit";
9104
            } else {
9105
                $invisibleSql = "SELECT e.iid
9106
                                  FROM $TBL_EXERCISES e
9107
                                  INNER JOIN $TBL_ITEM_PROPERTY ip
9108
                                  ON (e.id = ip.ref AND e.c_id = ip.c_id)
9109
                                  WHERE
9110
                                        ip.tool = '".TOOL_QUIZ."' AND
9111
                                        e.c_id = $courseId AND
9112
                                        e.active = 1 AND
9113
                                        ip.visibility = 0 AND
9114
                                        ip.session_id = $sessionId
9115
                                        $categoryCondition
9116
                                        $keywordCondition
9117
                                  ";
9118
9119
                $result = Database::query($invisibleSql);
9120
                $result = Database::store_result($result);
9121
                $hiddenFromSessionCondition = ' 1=1 ';
9122
                if (!empty($result)) {
9123
                    $hiddenFromSession = implode("','", array_column($result, 'iid'));
9124
                    $hiddenFromSessionCondition = " e.iid not in ('$hiddenFromSession')";
9125
                }
9126
9127
                $condition_session = " AND (
9128
                        (e.session_id = $sessionId OR e.session_id = 0 OR e.session_id IS NULL) AND
9129
                        $hiddenFromSessionCondition
9130
                )
9131
                ";
9132
9133
                // Only for students
9134
                $total_sql = "SELECT count(DISTINCT(e.iid)) as count
9135
                              FROM $TBL_EXERCISES e
9136
                              WHERE
9137
                                    e.c_id = $courseId AND
9138
                                    e.active = 1
9139
                                    $condition_session
9140
                                    $categoryCondition
9141
                                    $keywordCondition
9142
                              ";
9143
                $sql = "SELECT DISTINCT e.* FROM $TBL_EXERCISES e
9144
                        WHERE
9145
                             e.c_id = $courseId AND
9146
                             e.active = 1
9147
                             $condition_session
9148
                             $categoryCondition
9149
                             $keywordCondition
9150
                        ORDER BY title
9151
                        LIMIT $from , $limit";
9152
            }
9153
        }
9154
9155
        $result = Database::query($sql);
9156
        $result_total = Database::query($total_sql);
9157
9158
        $total_exercises = 0;
9159
        if (Database::num_rows($result_total)) {
9160
            $result_total = Database::fetch_array($result_total);
9161
            $total_exercises = $result_total['count'];
9162
        }
9163
9164
        //get HotPotatoes files (active and inactive)
9165
        if ($is_allowedToEdit) {
9166
            $sql = "SELECT * FROM $TBL_DOCUMENT
9167
                    WHERE
9168
                        c_id = $courseId AND
9169
                        path LIKE '".Database::escape_string($uploadPath.'/%/%')."'";
9170
            $res = Database::query($sql);
9171
            $hp_count = Database :: num_rows($res);
9172
        } else {
9173
            $sql = "SELECT * FROM $TBL_DOCUMENT d
9174
                    INNER JOIN $TBL_ITEM_PROPERTY ip
9175
                    ON (d.id = ip.ref AND d.c_id = ip.c_id)
9176
                    WHERE
9177
                        ip.tool = '".TOOL_DOCUMENT."' AND
9178
                        d.path LIKE '".Database::escape_string($uploadPath.'/%/%')."' AND
9179
                        ip.visibility = 1 AND
9180
                        d.c_id = $courseId AND
9181
                        ip.c_id  = $courseId";
9182
            $res = Database::query($sql);
9183
            $hp_count = Database::num_rows($res);
9184
        }
9185
9186
        $total = $total_exercises + $hp_count;
9187
        $exerciseList = [];
9188
        while ($row = Database::fetch_array($result, 'ASSOC')) {
9189
            $exerciseList[] = $row;
9190
        }
9191
9192
        if (!empty($exerciseList) &&
9193
            api_get_setting('exercise_invisible_in_session') === 'true'
9194
        ) {
9195
            if (!empty($sessionId)) {
9196
                $changeDefaultVisibility = true;
9197
                if (api_get_setting('configure_exercise_visibility_in_course') === 'true') {
9198
                    $changeDefaultVisibility = false;
9199
                    if (api_get_course_setting('exercise_invisible_in_session') == 1) {
9200
                        $changeDefaultVisibility = true;
9201
                    }
9202
                }
9203
9204
                if ($changeDefaultVisibility) {
9205
                    // Check exercise
9206
                    foreach ($exerciseList as $exercise) {
9207
                        if ($exercise['session_id'] == 0) {
9208
                            $visibilityInfo = api_get_item_property_info(
9209
                                $courseId,
9210
                                TOOL_QUIZ,
9211
                                $exercise['iid'],
9212
                                $sessionId
9213
                            );
9214
9215
                            if (empty($visibilityInfo)) {
9216
                                // Create a record for this
9217
                                api_item_property_update(
9218
                                    $courseInfo,
9219
                                    TOOL_QUIZ,
9220
                                    $exercise['iid'],
9221
                                    'invisible',
9222
                                    api_get_user_id(),
9223
                                    0,
9224
                                    null,
9225
                                    '',
9226
                                    '',
9227
                                    $sessionId
9228
                                );
9229
                            }
9230
                        }
9231
                    }
9232
                }
9233
            }
9234
        }
9235
9236
        $webPath = api_get_path(WEB_CODE_PATH);
9237
9238
        if (!empty($exerciseList)) {
9239
            if ($origin !== 'learnpath') {
9240
                $visibilitySetting = api_get_configuration_value('show_hidden_exercise_added_to_lp');
9241
                //avoid sending empty parameters
9242
                $mylpid = empty($learnpath_id) ? '' : '&learnpath_id='.$learnpath_id;
9243
                $mylpitemid = empty($learnpath_item_id) ? '' : '&learnpath_item_id='.$learnpath_item_id;
9244
                foreach ($exerciseList as $row) {
9245
                    $currentRow = [];
9246
                    $my_exercise_id = $row['id'];
9247
                    $attempt_text = '';
9248
                    $actions = '';
9249
                    $exercise = new Exercise($returnData ? $courseId : 0);
9250
                    $exercise->read($my_exercise_id, false);
9251
9252
                    if (empty($exercise->id)) {
9253
                        continue;
9254
                    }
9255
9256
                    $locked = $exercise->is_gradebook_locked;
9257
                    // Validation when belongs to a session
9258
                    $session_img = api_get_session_image($row['session_id'], $userInfo['status']);
9259
9260
                    $time_limits = false;
9261
                    if (!empty($row['start_time']) || !empty($row['end_time'])) {
9262
                        $time_limits = true;
9263
                    }
9264
9265
                    $is_actived_time = false;
9266
                    if ($time_limits) {
9267
                        // check if start time
9268
                        $start_time = false;
9269
                        if (!empty($row['start_time'])) {
9270
                            $start_time = api_strtotime($row['start_time'], 'UTC');
9271
                        }
9272
                        $end_time = false;
9273
                        if (!empty($row['end_time'])) {
9274
                            $end_time = api_strtotime($row['end_time'], 'UTC');
9275
                        }
9276
                        $now = time();
9277
9278
                        //If both "clocks" are enable
9279
                        if ($start_time && $end_time) {
9280
                            if ($now > $start_time && $end_time > $now) {
9281
                                $is_actived_time = true;
9282
                            }
9283
                        } else {
9284
                            //we check the start and end
9285
                            if ($start_time) {
9286
                                if ($now > $start_time) {
9287
                                    $is_actived_time = true;
9288
                                }
9289
                            }
9290
                            if ($end_time) {
9291
                                if ($end_time > $now) {
9292
                                    $is_actived_time = true;
9293
                                }
9294
                            }
9295
                        }
9296
                    }
9297
9298
                    // Blocking empty start times see BT#2800
9299
                    global $_custom;
9300
                    if (isset($_custom['exercises_hidden_when_no_start_date']) &&
9301
                        $_custom['exercises_hidden_when_no_start_date']
9302
                    ) {
9303
                        if (empty($row['start_time'])) {
9304
                            $time_limits = true;
9305
                            $is_actived_time = false;
9306
                        }
9307
                    }
9308
9309
                    $cut_title = $exercise->getCutTitle();
9310
                    $alt_title = '';
9311
                    if ($cut_title != $row['title']) {
9312
                        $alt_title = ' title = "'.$exercise->getUnformattedTitle().'" ';
9313
                    }
9314
9315
                    // Teacher only
9316
                    if ($is_allowedToEdit) {
9317
                        $lp_blocked = null;
9318
                        if ($exercise->exercise_was_added_in_lp == true) {
9319
                            $lp_blocked = Display::div(
9320
                                get_lang('AddedToLPCannotBeAccessed'),
9321
                                ['class' => 'lp_content_type_label']
9322
                            );
9323
                        }
9324
9325
                        // Get visibility in base course
9326
                        $visibility = api_get_item_visibility(
9327
                            $courseInfo,
9328
                            TOOL_QUIZ,
9329
                            $my_exercise_id,
9330
                            0
9331
                        );
9332
9333
                        if (!empty($sessionId)) {
9334
                            // If we are in a session, the test is invisible
9335
                            // in the base course, it is included in a LP
9336
                            // *and* the setting to show it is *not*
9337
                            // specifically set to true, then hide it.
9338
                            if ($visibility == 0) {
9339
                                if (!$visibilitySetting) {
9340
                                    if ($exercise->exercise_was_added_in_lp == true) {
9341
                                        continue;
9342
                                    }
9343
                                }
9344
                            }
9345
9346
                            $visibility = api_get_item_visibility(
9347
                                $courseInfo,
9348
                                TOOL_QUIZ,
9349
                                $my_exercise_id,
9350
                                $sessionId
9351
                            );
9352
                        }
9353
9354
                        if ($row['active'] == 0 || $visibility == 0) {
9355
                            $title = Display::tag('font', $cut_title, ['style' => 'color:grey']);
9356
                        } else {
9357
                            $title = $cut_title;
9358
                        }
9359
9360
                        /*$count_exercise_not_validated = (int) Event::count_exercise_result_not_validated(
9361
                            $my_exercise_id,
9362
                            $courseId,
9363
                            $sessionId
9364
                        );*/
9365
                        $move = null;
9366
                        $class_tip = '';
9367
                        /*if (!empty($count_exercise_not_validated)) {
9368
                            $results_text = $count_exercise_not_validated == 1 ? get_lang('ResultNotRevised') : get_lang('ResultsNotRevised');
9369
                            $title .= '<span class="exercise_tooltip" style="display: none;">'.$count_exercise_not_validated.' '.$results_text.' </span>';
9370
                        }*/
9371
                        $overviewUrl = api_get_path(WEB_CODE_PATH).'exercise/overview.php';
9372
                        $url = $move.
9373
                            '<a
9374
                                '.$alt_title.'
9375
                                class="'.$class_tip.'"
9376
                                id="tooltip_'.$row['id'].'"
9377
                                href="'.$overviewUrl.'?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$row['id'].'"
9378
                            >
9379
                             '.Display::return_icon('quiz.png', $row['title']).'
9380
                             '.$title.'
9381
                             </a>'.PHP_EOL;
9382
9383
                        if (ExerciseLib::isQuizEmbeddable($row)) {
9384
                            $embeddableIcon = Display::return_icon('om_integration.png', get_lang('ThisQuizCanBeEmbeddable'));
9385
                            $url .= Display::div($embeddableIcon, ['class' => 'pull-right']);
9386
                        }
9387
9388
                        $currentRow['title'] = $url.' '.$session_img.$lp_blocked;
9389
9390
                        // Count number exercise - teacher
9391
                        $sql = "SELECT count(*) count FROM $TBL_EXERCISE_QUESTION
9392
                                WHERE c_id = $courseId AND exercice_id = $my_exercise_id";
9393
                        $sqlresult = Database::query($sql);
9394
                        $rowi = (int) Database::result($sqlresult, 0, 0);
9395
9396
                        if ($sessionId == $row['session_id']) {
9397
                            // Questions list
9398
                            $actions = Display::url(
9399
                                Display::return_icon('edit.png', get_lang('Edit'), '', ICON_SIZE_SMALL),
9400
                                'admin.php?'.api_get_cidreq().'&exerciseId='.$row['id']
9401
                            );
9402
9403
                            // Test settings
9404
                            $settings = Display::url(
9405
                                Display::return_icon('settings.png', get_lang('Configure'), '', ICON_SIZE_SMALL),
9406
                                'exercise_admin.php?'.api_get_cidreq().'&exerciseId='.$row['id']
9407
                            );
9408
9409
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
9410
                                $settings = '';
9411
                            }
9412
                            $actions .= $settings;
9413
9414
                            // Exercise results
9415
                            $resultsLink = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$row['id'].'">'.
9416
                                Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
9417
9418
                            if ($limitTeacherAccess) {
9419
                                if (api_is_platform_admin()) {
9420
                                    $actions .= $resultsLink;
9421
                                }
9422
                            } else {
9423
                                // Exercise results
9424
                                $actions .= $resultsLink;
9425
                            }
9426
9427
                            // Auto launch
9428
                            if ($autoLaunchAvailable) {
9429
                                $autoLaunch = $exercise->getAutoLaunch();
9430
                                if (empty($autoLaunch)) {
9431
                                    $actions .= Display::url(
9432
                                        Display::return_icon(
9433
                                            'launch_na.png',
9434
                                            get_lang('Enable'),
9435
                                            '',
9436
                                            ICON_SIZE_SMALL
9437
                                        ),
9438
                                        'exercise.php?'.api_get_cidreq().'&choice=enable_launch&sec_token='.$token.'&exerciseId='.$row['id']
9439
                                    );
9440
                                } else {
9441
                                    $actions .= Display::url(
9442
                                        Display::return_icon(
9443
                                            'launch.png',
9444
                                            get_lang('Disable'),
9445
                                            '',
9446
                                            ICON_SIZE_SMALL
9447
                                        ),
9448
                                        'exercise.php?'.api_get_cidreq().'&choice=disable_launch&sec_token='.$token.'&exerciseId='.$row['id']
9449
                                    );
9450
                                }
9451
                            }
9452
9453
                            // Export
9454
                            $actions .= Display::url(
9455
                                Display::return_icon('cd.png', get_lang('CopyExercise')),
9456
                                '',
9457
                                [
9458
                                    'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('AreYouSureToCopy'), ENT_QUOTES, $charset))." ".addslashes($row['title'])."?"."')) return false;",
9459
                                    'href' => 'exercise.php?'.api_get_cidreq().'&choice=copy_exercise&sec_token='.$token.'&exerciseId='.$row['id'],
9460
                                ]
9461
                            );
9462
9463
                            // Clean exercise
9464
                            $clean = '';
9465
                            if (true === $allowClean) {
9466
                                if (false == $locked) {
9467
                                    $clean = Display::url(
9468
                                        Display::return_icon(
9469
                                            'clean.png',
9470
                                            get_lang('CleanStudentResults'),
9471
                                            '',
9472
                                            ICON_SIZE_SMALL
9473
                                        ),
9474
                                        '',
9475
                                        [
9476
                                            'onclick' => "javascript:if(!confirm('".addslashes(
9477
                                                    api_htmlentities(
9478
                                                        get_lang('AreYouSureToDeleteResults'),
9479
                                                        ENT_QUOTES,
9480
                                                        $charset
9481
                                                    )
9482
                                                )." ".addslashes($row['title'])."?"."')) return false;",
9483
                                            'href' => 'exercise.php?'.api_get_cidreq(
9484
                                                ).'&choice=clean_results&sec_token='.$token.'&exerciseId='.$row['id'],
9485
                                        ]
9486
                                    );
9487
                                } else {
9488
                                    $clean = Display::return_icon(
9489
                                        'clean_na.png',
9490
                                        get_lang('ResourceLockedByGradebook'),
9491
                                        '',
9492
                                        ICON_SIZE_SMALL
9493
                                    );
9494
                                }
9495
                            }
9496
9497
                            $actions .= $clean;
9498
9499
                            // Visible / invisible
9500
                            // Check if this exercise was added in a LP
9501
                            if ($exercise->exercise_was_added_in_lp == true) {
9502
                                $visibility = Display::return_icon(
9503
                                    'invisible.png',
9504
                                    get_lang('AddedToLPCannotBeAccessed'),
9505
                                    '',
9506
                                    ICON_SIZE_SMALL
9507
                                );
9508
                            } else {
9509
                                if ($row['active'] == 0 || $visibility == 0) {
9510
                                    $visibility = Display::url(
9511
                                        Display::return_icon(
9512
                                            'invisible.png',
9513
                                            get_lang('Activate'),
9514
                                            '',
9515
                                            ICON_SIZE_SMALL
9516
                                        ),
9517
                                        'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&exerciseId='.$row['id']
9518
                                    );
9519
                                } else {
9520
                                    // else if not active
9521
                                    $visibility = Display::url(
9522
                                        Display::return_icon(
9523
                                            'visible.png',
9524
                                            get_lang('Deactivate'),
9525
                                            '',
9526
                                            ICON_SIZE_SMALL
9527
                                        ),
9528
                                        'exercise.php?'.api_get_cidreq().'&choice=disable&sec_token='.$token.'&exerciseId='.$row['id']
9529
                                    );
9530
                                }
9531
                            }
9532
9533
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
9534
                                $visibility = '';
9535
                            }
9536
9537
                            $actions .= $visibility;
9538
9539
                            // Export qti ...
9540
                            $export = Display::url(
9541
                                Display::return_icon(
9542
                                    'export_qti2.png',
9543
                                    'IMS/QTI',
9544
                                    '',
9545
                                    ICON_SIZE_SMALL
9546
                                ),
9547
                                'exercise.php?action=exportqti2&exerciseId='.$row['id'].'&'.api_get_cidreq()
9548
                            );
9549
9550
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
9551
                                $export = '';
9552
                            }
9553
9554
                            $actions .= $export;
9555
                        } else {
9556
                            // not session
9557
                            $actions = Display::return_icon(
9558
                                'edit_na.png',
9559
                                get_lang('ExerciseEditionNotAvailableInSession')
9560
                            );
9561
9562
                            // Check if this exercise was added in a LP
9563
                            if ($exercise->exercise_was_added_in_lp == true) {
9564
                                $visibility = Display::return_icon(
9565
                                    'invisible.png',
9566
                                    get_lang('AddedToLPCannotBeAccessed'),
9567
                                    '',
9568
                                    ICON_SIZE_SMALL
9569
                                );
9570
                            } else {
9571
                                if ($row['active'] == 0 || $visibility == 0) {
9572
                                    $visibility = Display::url(
9573
                                        Display::return_icon(
9574
                                            'invisible.png',
9575
                                            get_lang('Activate'),
9576
                                            '',
9577
                                            ICON_SIZE_SMALL
9578
                                        ),
9579
                                        'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&exerciseId='.$row['id']
9580
                                    );
9581
                                } else {
9582
                                    // else if not active
9583
                                    $visibility = Display::url(
9584
                                        Display::return_icon(
9585
                                            'visible.png',
9586
                                            get_lang('Deactivate'),
9587
                                            '',
9588
                                            ICON_SIZE_SMALL
9589
                                        ),
9590
                                        'exercise.php?'.api_get_cidreq().'&choice=disable&sec_token='.$token.'&exerciseId='.$row['id']
9591
                                    );
9592
                                }
9593
                            }
9594
9595
                            if ($limitTeacherAccess && !api_is_platform_admin()) {
9596
                                $visibility = '';
9597
                            }
9598
9599
                            $actions .= $visibility;
9600
                            $actions .= '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$row['id'].'">'.
9601
                                Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
9602
                            $actions .= Display::url(
9603
                                Display::return_icon('cd.gif', get_lang('CopyExercise')),
9604
                                '',
9605
                                [
9606
                                    'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('AreYouSureToCopy'), ENT_QUOTES, $charset))." ".addslashes($row['title'])."?"."')) return false;",
9607
                                    'href' => 'exercise.php?'.api_get_cidreq().'&choice=copy_exercise&sec_token='.$token.'&exerciseId='.$row['id'],
9608
                                ]
9609
                            );
9610
                        }
9611
9612
                        // Delete
9613
                        $delete = '';
9614
                        if ($sessionId == $row['session_id']) {
9615
                            if ($locked == false) {
9616
                                $delete = Display::url(
9617
                                    Display::return_icon(
9618
                                        'delete.png',
9619
                                        get_lang('Delete'),
9620
                                        '',
9621
                                        ICON_SIZE_SMALL
9622
                                    ),
9623
                                    '',
9624
                                    [
9625
                                        'onclick' => "javascript:if(!confirm('".addslashes(api_htmlentities(get_lang('AreYouSureToDeleteJS'), ENT_QUOTES, $charset))." ".addslashes($exercise->getUnformattedTitle())."?"."')) return false;",
9626
                                        'href' => 'exercise.php?'.api_get_cidreq().'&choice=delete&sec_token='.$token.'&exerciseId='.$row['id'],
9627
                                    ]
9628
                                );
9629
                            } else {
9630
                                $delete = Display::return_icon(
9631
                                    'delete_na.png',
9632
                                    get_lang('ResourceLockedByGradebook'),
9633
                                    '',
9634
                                    ICON_SIZE_SMALL
9635
                                );
9636
                            }
9637
                        }
9638
9639
                        if ($limitTeacherAccess && !api_is_platform_admin()) {
9640
                            $delete = '';
9641
                        }
9642
9643
                        if (!empty($minCategoriesInExercise)) {
9644
                            $cats = TestCategory::getListOfCategoriesForTest($exercise);
9645
                            if (!(count($cats) >= $minCategoriesInExercise)) {
9646
                                continue;
9647
                            }
9648
                        }
9649
9650
                        $actions .= $delete;
9651
                        $usersToRemind = self::getUsersInExercise(
9652
                            $row['iid'],
9653
                            $row['c_id'],
9654
                            $row['session_id'],
9655
                            true
9656
                        );
9657
                        if ($usersToRemind > 0) {
9658
                            $actions .= Display::url(
9659
                                Display::return_icon('announce.png', get_lang('EmailNotifySubscription')),
9660
                                '',
9661
                                [
9662
                                    'href' => '#!',
9663
                                    'onclick' => 'showUserToSendNotificacion(this)',
9664
                                    'data-link' => 'exercise.php?'.api_get_cidreq()
9665
                                        .'&choice=send_reminder&sec_token='.$token.'&exerciseId='.$row['id'],
9666
                                ]
9667
                            );
9668
                        }
9669
9670
                        // Number of questions
9671
                        $random_label = null;
9672
                        if ($row['random'] > 0 || $row['random'] == -1) {
9673
                            // if random == -1 means use random questions with all questions
9674
                            $random_number_of_question = $row['random'];
9675
                            if ($random_number_of_question == -1) {
9676
                                $random_number_of_question = $rowi;
9677
                            }
9678
                            if ($row['random_by_category'] > 0) {
9679
                                $nbQuestionsTotal = TestCategory::getNumberOfQuestionRandomByCategory(
9680
                                    $my_exercise_id,
9681
                                    $random_number_of_question
9682
                                );
9683
                                $number_of_questions = $nbQuestionsTotal.' ';
9684
                                $number_of_questions .= ($nbQuestionsTotal > 1) ? get_lang('QuestionsLowerCase') : get_lang('QuestionLowerCase');
9685
                                $number_of_questions .= ' - ';
9686
                                $number_of_questions .= min(
9687
                                        TestCategory::getNumberMaxQuestionByCat($my_exercise_id), $random_number_of_question
9688
                                    ).' '.get_lang('QuestionByCategory');
9689
                            } else {
9690
                                $random_label = ' ('.get_lang('Random').') ';
9691
                                $number_of_questions = $random_number_of_question.' '.$random_label.' / '.$rowi;
9692
                                // Bug if we set a random value bigger than the real number of questions
9693
                                if ($random_number_of_question > $rowi) {
9694
                                    $number_of_questions = $rowi.' '.$random_label;
9695
                                }
9696
                            }
9697
                        } else {
9698
                            $number_of_questions = $rowi;
9699
                        }
9700
9701
                        $currentRow['count_questions'] = $number_of_questions;
9702
                    } else {
9703
                        // Student only.
9704
                        $visibility = api_get_item_visibility(
9705
                            $courseInfo,
9706
                            TOOL_QUIZ,
9707
                            $my_exercise_id,
9708
                            $sessionId
9709
                        );
9710
9711
                        if ($visibility == 0) {
9712
                            continue;
9713
                        }
9714
9715
                        $url = '<a '.$alt_title.'  href="overview.php?'.api_get_cidreq().$mylpid.$mylpitemid.'&exerciseId='.$row['id'].'">'.
9716
                            $cut_title.'</a>';
9717
9718
                        // Link of the exercise.
9719
                        $currentRow['title'] = $url.' '.$session_img;
9720
9721
                        if ($returnData) {
9722
                            $currentRow['title'] = $exercise->getUnformattedTitle();
9723
                        }
9724
9725
                        // This query might be improved later on by ordering by the new "tms" field rather than by exe_id
9726
                        // Don't remove this marker: note-query-exe-results
9727
                        $sql = "SELECT * FROM $TBL_TRACK_EXERCISES
9728
                                WHERE
9729
                                    exe_exo_id = ".$row['id']." AND
9730
                                    exe_user_id = $userId AND
9731
                                    c_id = ".api_get_course_int_id()." AND
9732
                                    status <> 'incomplete' AND
9733
                                    orig_lp_id = 0 AND
9734
                                    orig_lp_item_id = 0 AND
9735
                                    session_id =  '".api_get_session_id()."'
9736
                                ORDER BY exe_id DESC";
9737
9738
                        $qryres = Database::query($sql);
9739
                        $num = Database :: num_rows($qryres);
9740
9741
                        // Hide the results.
9742
                        $my_result_disabled = $row['results_disabled'];
9743
9744
                        $attempt_text = '-';
9745
                        // Time limits are on
9746
                        if ($time_limits) {
9747
                            // Exam is ready to be taken
9748
                            if ($is_actived_time) {
9749
                                // Show results
9750
                                if (
9751
                                in_array(
9752
                                    $my_result_disabled,
9753
                                    [
9754
                                        RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9755
                                        RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9756
                                        RESULT_DISABLE_SHOW_SCORE_ONLY,
9757
                                        RESULT_DISABLE_RANKING,
9758
                                    ]
9759
                                )
9760
                                ) {
9761
                                    // More than one attempt
9762
                                    if ($num > 0) {
9763
                                        $row_track = Database :: fetch_array($qryres);
9764
                                        $attempt_text = get_lang('LatestAttempt').' : ';
9765
                                        $attempt_text .= ExerciseLib::show_score(
9766
                                            $row_track['exe_result'],
9767
                                            $row_track['exe_weighting']
9768
                                        );
9769
                                    } else {
9770
                                        //No attempts
9771
                                        $attempt_text = get_lang('NotAttempted');
9772
                                    }
9773
                                } else {
9774
                                    $attempt_text = '-';
9775
                                }
9776
                            } else {
9777
                                // Quiz not ready due to time limits
9778
                                //@todo use the is_visible function
9779
                                if (!empty($row['start_time']) && !empty($row['end_time'])) {
9780
                                    $today = time();
9781
                                    $start_time = api_strtotime($row['start_time'], 'UTC');
9782
                                    $end_time = api_strtotime($row['end_time'], 'UTC');
9783
                                    if ($today < $start_time) {
9784
                                        $attempt_text = sprintf(
9785
                                            get_lang('ExerciseWillBeActivatedFromXToY'),
9786
                                            api_convert_and_format_date($row['start_time']),
9787
                                            api_convert_and_format_date($row['end_time'])
9788
                                        );
9789
                                    } else {
9790
                                        if ($today > $end_time) {
9791
                                            $attempt_text = sprintf(
9792
                                                get_lang('ExerciseWasActivatedFromXToY'),
9793
                                                api_convert_and_format_date($row['start_time']),
9794
                                                api_convert_and_format_date($row['end_time'])
9795
                                            );
9796
                                        }
9797
                                    }
9798
                                } else {
9799
                                    //$attempt_text = get_lang('ExamNotAvailableAtThisTime');
9800
                                    if (!empty($row['start_time'])) {
9801
                                        $attempt_text = sprintf(
9802
                                            get_lang('ExerciseAvailableFromX'),
9803
                                            api_convert_and_format_date($row['start_time'])
9804
                                        );
9805
                                    }
9806
                                    if (!empty($row['end_time'])) {
9807
                                        $attempt_text = sprintf(
9808
                                            get_lang('ExerciseAvailableUntilX'),
9809
                                            api_convert_and_format_date($row['end_time'])
9810
                                        );
9811
                                    }
9812
                                }
9813
                            }
9814
                        } else {
9815
                            // Normal behaviour.
9816
                            // Show results.
9817
                            if (
9818
                            in_array(
9819
                                $my_result_disabled,
9820
                                [
9821
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
9822
                                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
9823
                                    RESULT_DISABLE_SHOW_SCORE_ONLY,
9824
                                    RESULT_DISABLE_RANKING,
9825
                                    RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
9826
                                ]
9827
                            )
9828
                            ) {
9829
                                if ($num > 0) {
9830
                                    $row_track = Database :: fetch_array($qryres);
9831
                                    $attempt_text = get_lang('LatestAttempt').' : ';
9832
                                    $attempt_text .= ExerciseLib::show_score(
9833
                                        $row_track['exe_result'],
9834
                                        $row_track['exe_weighting']
9835
                                    );
9836
                                } else {
9837
                                    $attempt_text = get_lang('NotAttempted');
9838
                                }
9839
                            }
9840
                        }
9841
9842
                        if ($returnData) {
9843
                            $attempt_text = $num;
9844
                        }
9845
                    }
9846
9847
                    $currentRow['attempt'] = $attempt_text;
9848
9849
                    if ($is_allowedToEdit) {
9850
                        $additionalActions = ExerciseLib::getAdditionalTeacherActions($row['id']);
9851
9852
                        if (!empty($additionalActions)) {
9853
                            $actions .= $additionalActions.PHP_EOL;
9854
                        }
9855
9856
                        // Replace with custom actions.
9857
                        if (!empty($myActions) && is_callable($myActions)) {
9858
                            $actions = $myActions($row);
9859
                        }
9860
9861
                        $currentRow = [
9862
                            $row['id'],
9863
                            $currentRow['title'],
9864
                            $currentRow['count_questions'],
9865
                            $actions,
9866
                        ];
9867
                    } else {
9868
                        $currentRow = [
9869
                            $currentRow['title'],
9870
                            $currentRow['attempt'],
9871
                        ];
9872
9873
                        if ($isDrhOfCourse) {
9874
                            $currentRow[] = '<a href="exercise_report.php?'.api_get_cidreq().'&exerciseId='.$row['id'].'">'.
9875
                                Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
9876
                        }
9877
9878
                        if ($returnData) {
9879
                            $currentRow['id'] = $exercise->id;
9880
                            $currentRow['url'] = $webPath.'exercise/overview.php?'
9881
                                .api_get_cidreq_params($courseInfo['code'], $sessionId).'&'
9882
                                ."$mylpid$mylpitemid&exerciseId={$row['id']}";
9883
                            $currentRow['name'] = $currentRow[0];
9884
                        }
9885
                    }
9886
9887
                    $tableRows[] = $currentRow;
9888
                }
9889
            }
9890
        }
9891
9892
        // end exercise list
9893
        // Hotpotatoes results
9894
        if ($is_allowedToEdit) {
9895
            $sql = "SELECT d.iid, d.path as path, d.comment as comment
9896
                    FROM $TBL_DOCUMENT d
9897
                    WHERE
9898
                        d.c_id = $courseId AND
9899
                        (d.path LIKE '%htm%') AND
9900
                        d.path  LIKE '".Database :: escape_string($uploadPath.'/%/%')."'
9901
                    LIMIT $from , $limit"; // only .htm or .html files listed
9902
        } else {
9903
            $sql = "SELECT d.iid, d.path as path, d.comment as comment
9904
                    FROM $TBL_DOCUMENT d
9905
                    WHERE
9906
                        d.c_id = $courseId AND
9907
                        (d.path LIKE '%htm%') AND
9908
                        d.path  LIKE '".Database :: escape_string($uploadPath.'/%/%')."'
9909
                    LIMIT $from , $limit";
9910
        }
9911
9912
        $result = Database::query($sql);
9913
        $attributes = [];
9914
        while ($row = Database::fetch_array($result, 'ASSOC')) {
9915
            $attributes[$row['iid']] = $row;
9916
        }
9917
9918
        $nbrActiveTests = 0;
9919
        if (!empty($attributes)) {
9920
            foreach ($attributes as $item) {
9921
                $id = $item['iid'];
9922
                $path = $item['path'];
9923
9924
                $title = GetQuizName($path, $documentPath);
9925
                if ($title == '') {
9926
                    $title = basename($path);
9927
                }
9928
9929
                // prof only
9930
                if ($is_allowedToEdit) {
9931
                    $visibility = api_get_item_visibility(
9932
                        ['real_id' => $courseId],
9933
                        TOOL_DOCUMENT,
9934
                        $id,
9935
                        0
9936
                    );
9937
9938
                    if (!empty($sessionId)) {
9939
                        if (0 == $visibility) {
9940
                            continue;
9941
                        }
9942
9943
                        $visibility = api_get_item_visibility(
9944
                            ['real_id' => $courseId],
9945
                            TOOL_DOCUMENT,
9946
                            $id,
9947
                            $sessionId
9948
                        );
9949
                    }
9950
9951
                    $title =
9952
                        implode(PHP_EOL, [
9953
                            Display::return_icon('hotpotatoes_s.png', 'HotPotatoes'),
9954
                            Display::url(
9955
                                $title,
9956
                                'showinframes.php?'.api_get_cidreq().'&'.http_build_query([
9957
                                    'file' => $path,
9958
                                    'uid' => $userId,
9959
                                ]),
9960
                                ['class' => $visibility == 0 ? 'text-muted' : null]
9961
                            ),
9962
                        ]);
9963
9964
                    $actions = Display::url(
9965
                        Display::return_icon(
9966
                            'edit.png',
9967
                            get_lang('Edit'),
9968
                            '',
9969
                            ICON_SIZE_SMALL
9970
                        ),
9971
                        'adminhp.php?'.api_get_cidreq().'&hotpotatoesName='.$path
9972
                    );
9973
9974
                    $actions .= '<a href="hotpotatoes_exercise_report.php?'.api_get_cidreq().'&path='.$path.'">'.
9975
                        Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).
9976
                        '</a>';
9977
9978
                    // if active
9979
                    if ($visibility != 0) {
9980
                        $nbrActiveTests = $nbrActiveTests + 1;
9981
                        $actions .= '      <a href="'.$exercisePath.'?'.api_get_cidreq().'&hpchoice=disable&file='.$path.'">'.
9982
                            Display::return_icon('visible.png', get_lang('Deactivate'), '', ICON_SIZE_SMALL).'</a>';
9983
                    } else { // else if not active
9984
                        $actions .= '    <a href="'.$exercisePath.'?'.api_get_cidreq().'&hpchoice=enable&file='.$path.'">'.
9985
                            Display::return_icon('invisible.png', get_lang('Activate'), '', ICON_SIZE_SMALL).'</a>';
9986
                    }
9987
                    $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;">'.
9988
                        Display::return_icon('delete.png', get_lang('Delete'), '', ICON_SIZE_SMALL).'</a>';
9989
9990
                    $currentRow = [
9991
                        '',
9992
                        $title,
9993
                        '',
9994
                        $actions,
9995
                    ];
9996
                } else {
9997
                    $visibility = api_get_item_visibility(
9998
                        ['real_id' => $courseId],
9999
                        TOOL_DOCUMENT,
10000
                        $id,
10001
                        $sessionId
10002
                    );
10003
10004
                    if (0 == $visibility) {
10005
                        continue;
10006
                    }
10007
10008
                    // Student only
10009
                    $attempt = ExerciseLib::getLatestHotPotatoResult(
10010
                        $path,
10011
                        $userId,
10012
                        api_get_course_int_id(),
10013
                        api_get_session_id()
10014
                    );
10015
10016
                    $nbrActiveTests = $nbrActiveTests + 1;
10017
                    $title = Display::url(
10018
                        $title,
10019
                        'showinframes.php?'.api_get_cidreq().'&'.http_build_query(
10020
                            [
10021
                                'file' => $path,
10022
                                'cid' => api_get_course_id(),
10023
                                'uid' => $userId,
10024
                            ]
10025
                        )
10026
                    );
10027
10028
                    if (!empty($attempt)) {
10029
                        $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>';
10030
                        $attemptText = get_lang('LatestAttempt').' : ';
10031
                        $attemptText .= ExerciseLib::show_score(
10032
                                $attempt['exe_result'],
10033
                                $attempt['exe_weighting']
10034
                            ).' ';
10035
                        $attemptText .= $actions;
10036
                    } else {
10037
                        // No attempts.
10038
                        $attemptText = get_lang('NotAttempted').' ';
10039
                    }
10040
10041
                    $currentRow = [
10042
                        $title,
10043
                        $attemptText,
10044
                    ];
10045
10046
                    if ($isDrhOfCourse) {
10047
                        $currentRow[] = '<a href="hotpotatoes_exercise_report.php?'.api_get_cidreq().'&path='.$path.'">'.
10048
                            Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_SMALL).'</a>';
10049
                    }
10050
                }
10051
10052
                $tableRows[] = $currentRow;
10053
            }
10054
        }
10055
10056
        if ($returnData) {
10057
            return $tableRows;
10058
        }
10059
10060
        if (empty($tableRows) && empty($categoryId)) {
10061
            if ($is_allowedToEdit && $origin !== 'learnpath') {
10062
                $content .= '<div id="no-data-view">';
10063
                $content .= '<h3>'.get_lang('Quiz').'</h3>';
10064
                $content .= Display::return_icon('quiz.png', '', [], 64);
10065
                $content .= '<div class="controls">';
10066
                $content .= Display::url(
10067
                    '<em class="fa fa-plus"></em> '.get_lang('NewEx'),
10068
                    'exercise_admin.php?'.api_get_cidreq(),
10069
                    ['class' => 'btn btn-primary']
10070
                );
10071
                $content .= '</div>';
10072
                $content .= '</div>';
10073
            }
10074
        } else {
10075
            if (empty($tableRows)) {
10076
                return '';
10077
            }
10078
            $table->setTableData($tableRows);
10079
            $table->setTotalNumberOfItems($total);
10080
            $table->set_additional_parameters([
10081
                'cidReq' => api_get_course_id(),
10082
                'id_session' => api_get_session_id(),
10083
                'category_id' => $categoryId,
10084
            ]);
10085
10086
            if ($is_allowedToEdit) {
10087
                $formActions = [];
10088
                $formActions['visible'] = get_lang('Activate');
10089
                $formActions['invisible'] = get_lang('Deactivate');
10090
                $formActions['delete'] = get_lang('Delete');
10091
                $table->set_form_actions($formActions);
10092
            }
10093
10094
            $i = 0;
10095
            if ($is_allowedToEdit) {
10096
                $table->set_header($i++, '', false, 'width="18px"');
10097
            }
10098
            $table->set_header($i++, get_lang('ExerciseName'), false);
10099
10100
            if ($is_allowedToEdit) {
10101
                $table->set_header($i++, get_lang('QuantityQuestions'), false);
10102
                $table->set_header($i++, get_lang('Actions'), false);
10103
            } else {
10104
                $table->set_header($i++, get_lang('Status'), false);
10105
                if ($isDrhOfCourse) {
10106
                    $table->set_header($i++, get_lang('Actions'), false);
10107
                }
10108
            }
10109
10110
            if ($returnTable) {
10111
                return $table;
10112
            }
10113
10114
            $content .= $table->return_table();
10115
        }
10116
10117
        return $content;
10118
    }
10119
10120
    /**
10121
     * @return int value in minutes
10122
     */
10123
    public function getResultAccess()
10124
    {
10125
        $extraFieldValue = new ExtraFieldValue('exercise');
10126
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
10127
            $this->iId,
10128
            'results_available_for_x_minutes'
10129
        );
10130
10131
        if (!empty($value) && isset($value['value'])) {
10132
            return (int) $value['value'];
10133
        }
10134
10135
        return 0;
10136
    }
10137
10138
    /**
10139
     * @param array $exerciseResultInfo
10140
     *
10141
     * @return bool
10142
     */
10143
    public function getResultAccessTimeDiff($exerciseResultInfo)
10144
    {
10145
        $value = $this->getResultAccess();
10146
        if (!empty($value)) {
10147
            $endDate = new DateTime($exerciseResultInfo['exe_date'], new DateTimeZone('UTC'));
10148
            $endDate->add(new DateInterval('PT'.$value.'M'));
10149
            $now = time();
10150
            if ($endDate->getTimestamp() > $now) {
10151
                return (int) $endDate->getTimestamp() - $now;
10152
            }
10153
        }
10154
10155
        return 0;
10156
    }
10157
10158
    /**
10159
     * @param array $exerciseResultInfo
10160
     *
10161
     * @return bool
10162
     */
10163
    public function hasResultsAccess($exerciseResultInfo)
10164
    {
10165
        $diff = $this->getResultAccessTimeDiff($exerciseResultInfo);
10166
        if (0 === $diff) {
10167
            return false;
10168
        }
10169
10170
        return true;
10171
    }
10172
10173
    /**
10174
     * @return int
10175
     */
10176
    public function getResultsAccess()
10177
    {
10178
        $extraFieldValue = new ExtraFieldValue('exercise');
10179
        $value = $extraFieldValue->get_values_by_handler_and_field_variable(
10180
            $this->iId,
10181
            'results_available_for_x_minutes'
10182
        );
10183
        if (!empty($value)) {
10184
            return (int) $value;
10185
        }
10186
10187
        return 0;
10188
    }
10189
10190
    /**
10191
     * @param int   $questionId
10192
     * @param bool  $show_results
10193
     * @param array $question_result
10194
     */
10195
    public function getDelineationResult(Question $objQuestionTmp, $questionId, $show_results, $question_result)
10196
    {
10197
        $id = (int) $objQuestionTmp->id;
10198
        $questionId = (int) $questionId;
10199
10200
        $final_overlap = $question_result['extra']['final_overlap'];
10201
        $final_missing = $question_result['extra']['final_missing'];
10202
        $final_excess = $question_result['extra']['final_excess'];
10203
10204
        $overlap_color = $question_result['extra']['overlap_color'];
10205
        $missing_color = $question_result['extra']['missing_color'];
10206
        $excess_color = $question_result['extra']['excess_color'];
10207
10208
        $threadhold1 = $question_result['extra']['threadhold1'];
10209
        $threadhold2 = $question_result['extra']['threadhold2'];
10210
        $threadhold3 = $question_result['extra']['threadhold3'];
10211
10212
        if ($show_results) {
10213
            if ($overlap_color) {
10214
                $overlap_color = 'green';
10215
            } else {
10216
                $overlap_color = 'red';
10217
            }
10218
10219
            if ($missing_color) {
10220
                $missing_color = 'green';
10221
            } else {
10222
                $missing_color = 'red';
10223
            }
10224
            if ($excess_color) {
10225
                $excess_color = 'green';
10226
            } else {
10227
                $excess_color = 'red';
10228
            }
10229
10230
            if (!is_numeric($final_overlap)) {
10231
                $final_overlap = 0;
10232
            }
10233
10234
            if (!is_numeric($final_missing)) {
10235
                $final_missing = 0;
10236
            }
10237
            if (!is_numeric($final_excess)) {
10238
                $final_excess = 0;
10239
            }
10240
10241
            if ($final_excess > 100) {
10242
                $final_excess = 100;
10243
            }
10244
10245
            $table_resume = '
10246
                    <table class="table table-hover table-striped data_table">
10247
                        <tr class="row_odd" >
10248
                            <td>&nbsp;</td>
10249
                            <td><b>'.get_lang('Requirements').'</b></td>
10250
                            <td><b>'.get_lang('YourAnswer').'</b></td>
10251
                        </tr>
10252
                        <tr class="row_even">
10253
                            <td><b>'.get_lang('Overlap').'</b></td>
10254
                            <td>'.get_lang('Min').' '.$threadhold1.'</td>
10255
                            <td>
10256
                                <div style="color:'.$overlap_color.'">
10257
                                    '.(($final_overlap < 0) ? 0 : intval($final_overlap)).'
10258
                                </div>
10259
                            </td>
10260
                        </tr>
10261
                        <tr>
10262
                            <td><b>'.get_lang('Excess').'</b></td>
10263
                            <td>'.get_lang('Max').' '.$threadhold2.'</td>
10264
                            <td>
10265
                                <div style="color:'.$excess_color.'">
10266
                                    '.(($final_excess < 0) ? 0 : intval($final_excess)).'
10267
                                </div>
10268
                            </td>
10269
                        </tr>
10270
                        <tr class="row_even">
10271
                            <td><b>'.get_lang('Missing').'</b></td>
10272
                            <td>'.get_lang('Max').' '.$threadhold3.'</td>
10273
                            <td>
10274
                                <div style="color:'.$missing_color.'">
10275
                                    '.(($final_missing < 0) ? 0 : intval($final_missing)).'
10276
                                </div>
10277
                            </td>
10278
                        </tr>
10279
                    </table>
10280
                ';
10281
10282
            $answerType = $objQuestionTmp->selectType();
10283
            if ($answerType != HOT_SPOT_DELINEATION) {
10284
                $item_list = explode('@@', $destination);
10285
                $try = $item_list[0];
10286
                $lp = $item_list[1];
10287
                $destinationid = $item_list[2];
10288
                $url = $item_list[3];
10289
                $table_resume = '';
10290
            } else {
10291
                if ($next == 0) {
10292
                    $try = $try_hotspot;
10293
                    $lp = $lp_hotspot;
10294
                    $destinationid = $select_question_hotspot;
10295
                    $url = $url_hotspot;
10296
                } else {
10297
                    //show if no error
10298
                    $comment = $answerComment = $objAnswerTmp->selectComment($nbrAnswers);
10299
                    $answerDestination = $objAnswerTmp->selectDestination($nbrAnswers);
10300
                }
10301
            }
10302
10303
            echo '<h1><div style="color:#333;">'.get_lang('Feedback').'</div></h1>';
10304
            if ($answerType == HOT_SPOT_DELINEATION) {
10305
                if ($organs_at_risk_hit > 0) {
10306
                    $message = '<br />'.get_lang('ResultIs').' <b>'.$result_comment.'</b><br />';
10307
                    $message .= '<p style="color:#DC0A0A;"><b>'.get_lang('OARHit').'</b></p>';
10308
                } else {
10309
                    $message = '<p>'.get_lang('YourDelineation').'</p>';
10310
                    $message .= $table_resume;
10311
                    $message .= '<br />'.get_lang('ResultIs').' <b>'.$result_comment.'</b><br />';
10312
                }
10313
                $message .= '<p>'.$comment.'</p>';
10314
                echo $message;
10315
            } else {
10316
                echo '<p>'.$comment.'</p>';
10317
            }
10318
10319
            // Showing the score
10320
            /*$queryfree = "SELECT marks FROM $TBL_TRACK_ATTEMPT
10321
                          WHERE exe_id = $id AND question_id =  $questionId";
10322
            $resfree = Database::query($queryfree);
10323
            $questionScore = Database::result($resfree, 0, 'marks');
10324
            $totalScore += $questionScore;*/
10325
            $relPath = api_get_path(REL_CODE_PATH);
10326
            echo '</table></td></tr>';
10327
            echo "
10328
                        <tr>
10329
                            <td colspan=\"2\">
10330
                                <div id=\"hotspot-solution\"></div>
10331
                                <script>
10332
                                    $(function() {
10333
                                        new HotspotQuestion({
10334
                                            questionId: $questionId,
10335
                                            exerciseId: {$this->id},
10336
                                            exeId: $id,
10337
                                            selector: '#hotspot-solution',
10338
                                            for: 'solution',
10339
                                            relPath: '$relPath'
10340
                                        });
10341
                                    });
10342
                                </script>
10343
                            </td>
10344
                        </tr>
10345
                    </table>
10346
                ";
10347
        }
10348
    }
10349
10350
    /**
10351
     * Clean exercise session variables.
10352
     */
10353
    public static function cleanSessionVariables()
10354
    {
10355
        Session::erase('objExercise');
10356
        Session::erase('exe_id');
10357
        Session::erase('calculatedAnswerId');
10358
        Session::erase('duration_time_previous');
10359
        Session::erase('duration_time');
10360
        Session::erase('objQuestion');
10361
        Session::erase('objAnswer');
10362
        Session::erase('questionList');
10363
        Session::erase('categoryList');
10364
        Session::erase('exerciseResult');
10365
        Session::erase('firstTime');
10366
        Session::erase('time_per_question');
10367
        Session::erase('question_start');
10368
        Session::erase('exerciseResultCoordinates');
10369
        Session::erase('hotspot_coord');
10370
        Session::erase('hotspot_dest');
10371
        Session::erase('hotspot_delineation_result');
10372
    }
10373
10374
    /**
10375
     * Get the first LP found matching the session ID.
10376
     *
10377
     * @param int $sessionId
10378
     *
10379
     * @return array
10380
     */
10381
    public function getLpBySession($sessionId)
10382
    {
10383
        if (!empty($this->lpList)) {
10384
            $sessionId = (int) $sessionId;
10385
10386
            foreach ($this->lpList as $lp) {
10387
                if ((int) $lp['session_id'] == $sessionId) {
10388
                    return $lp;
10389
                }
10390
            }
10391
10392
            return current($this->lpList);
10393
        }
10394
10395
        return [
10396
            'lp_id' => 0,
10397
            'max_score' => 0,
10398
            'session_id' => 0,
10399
        ];
10400
    }
10401
10402
    public static function saveExerciseInLp($safe_item_id, $safe_exe_id)
10403
    {
10404
        $lp = Session::read('oLP');
10405
10406
        $safe_exe_id = (int) $safe_exe_id;
10407
        $safe_item_id = (int) $safe_item_id;
10408
10409
        if (empty($lp) || empty($safe_exe_id) || empty($safe_item_id)) {
10410
            return false;
10411
        }
10412
10413
        $viewId = $lp->get_view_id();
10414
        $course_id = api_get_course_int_id();
10415
        $userId = (int) api_get_user_id();
10416
        $viewId = (int) $viewId;
10417
10418
        $TBL_TRACK_EXERCICES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
10419
        $TBL_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
10420
        $TBL_LP_ITEM = Database::get_course_table(TABLE_LP_ITEM);
10421
10422
        $sql = "SELECT start_date, exe_date, exe_result, exe_weighting, exe_exo_id, exe_duration
10423
                FROM $TBL_TRACK_EXERCICES
10424
                WHERE exe_id = $safe_exe_id AND exe_user_id = $userId";
10425
        $res = Database::query($sql);
10426
        $row_dates = Database::fetch_array($res);
10427
10428
        if (empty($row_dates)) {
10429
            return false;
10430
        }
10431
10432
        $duration = (int) $row_dates['exe_duration'];
10433
        $score = (float) $row_dates['exe_result'];
10434
        $max_score = (float) $row_dates['exe_weighting'];
10435
10436
        $sql = "UPDATE $TBL_LP_ITEM SET
10437
                    max_score = '$max_score'
10438
                WHERE iid = $safe_item_id";
10439
        Database::query($sql);
10440
10441
        $sql = "SELECT id FROM $TBL_LP_ITEM_VIEW
10442
                WHERE
10443
                    c_id = $course_id AND
10444
                    lp_item_id = $safe_item_id AND
10445
                    lp_view_id = $viewId
10446
                ORDER BY id DESC
10447
                LIMIT 1";
10448
        $res_last_attempt = Database::query($sql);
10449
10450
        if (Database::num_rows($res_last_attempt) && !api_is_invitee()) {
10451
            $row_last_attempt = Database::fetch_row($res_last_attempt);
10452
            $lp_item_view_id = $row_last_attempt[0];
10453
10454
            $exercise = new Exercise($course_id);
10455
            $exercise->read($row_dates['exe_exo_id']);
10456
            $status = 'completed';
10457
10458
            if (!empty($exercise->pass_percentage)) {
10459
                $status = 'failed';
10460
                $success = ExerciseLib::isSuccessExerciseResult(
10461
                    $score,
10462
                    $max_score,
10463
                    $exercise->pass_percentage
10464
                );
10465
                if ($success) {
10466
                    $status = 'passed';
10467
                }
10468
            }
10469
10470
            $sql = "UPDATE $TBL_LP_ITEM_VIEW SET
10471
                        status = '$status',
10472
                        score = $score,
10473
                        total_time = $duration
10474
                    WHERE iid = $lp_item_view_id";
10475
            Database::query($sql);
10476
10477
            $sql = "UPDATE $TBL_TRACK_EXERCICES SET
10478
                        orig_lp_item_view_id = $lp_item_view_id
10479
                    WHERE exe_id = ".$safe_exe_id;
10480
            Database::query($sql);
10481
        }
10482
    }
10483
10484
    /**
10485
     * Get the user answers saved in exercise.
10486
     *
10487
     * @param int $attemptId
10488
     *
10489
     * @return array
10490
     */
10491
    public function getUserAnswersSavedInExercise($attemptId)
10492
    {
10493
        $exerciseResult = [];
10494
        $attemptList = Event::getAllExerciseEventByExeId($attemptId);
10495
10496
        foreach ($attemptList as $questionId => $options) {
10497
            foreach ($options as $option) {
10498
                $question = Question::read($option['question_id']);
10499
                if ($question) {
10500
                    switch ($question->type) {
10501
                        case FILL_IN_BLANKS:
10502
                            $option['answer'] = $this->fill_in_blank_answer_to_string($option['answer']);
10503
                            break;
10504
                    }
10505
                }
10506
10507
                if (!empty($option['answer'])) {
10508
                    $exerciseResult[] = $questionId;
10509
10510
                    break;
10511
                }
10512
            }
10513
        }
10514
10515
        return $exerciseResult;
10516
    }
10517
10518
    /**
10519
     * Get the number of user answers saved in exercise.
10520
     *
10521
     * @param int $attemptId
10522
     *
10523
     * @return int
10524
     */
10525
    public function countUserAnswersSavedInExercise($attemptId)
10526
    {
10527
        $answers = $this->getUserAnswersSavedInExercise($attemptId);
10528
10529
        return count($answers);
10530
    }
10531
10532
    public static function allowAction($action)
10533
    {
10534
        if (api_is_platform_admin()) {
10535
            return true;
10536
        }
10537
10538
        $limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
10539
        $disableClean = api_get_configuration_value('disable_clean_exercise_results_for_teachers');
10540
10541
        switch ($action) {
10542
            case 'delete':
10543
                if (api_is_allowed_to_edit(null, true)) {
10544
                    if ($limitTeacherAccess) {
10545
                        return false;
10546
                    }
10547
10548
                    return true;
10549
                }
10550
                break;
10551
            case 'clean_results':
10552
                if (api_is_allowed_to_edit(null, true)) {
10553
                    if ($limitTeacherAccess) {
10554
                        return false;
10555
                    }
10556
10557
                    if ($disableClean) {
10558
                        return false;
10559
                    }
10560
10561
                    return true;
10562
                }
10563
10564
                break;
10565
        }
10566
10567
        return false;
10568
    }
10569
10570
    public static function getLpListFromExercise($exerciseId, $courseId)
10571
    {
10572
        $tableLpItem = Database::get_course_table(TABLE_LP_ITEM);
10573
        $tblLp = Database::get_course_table(TABLE_LP_MAIN);
10574
10575
        $exerciseId = (int) $exerciseId;
10576
        $courseId = (int) $courseId;
10577
10578
        $sql = "SELECT
10579
                    lp.name,
10580
                    lpi.lp_id,
10581
                    lpi.max_score,
10582
                    lp.session_id
10583
                FROM $tableLpItem lpi
10584
                INNER JOIN $tblLp lp
10585
                ON (lpi.lp_id = lp.iid AND lpi.c_id = lp.c_id)
10586
                WHERE
10587
                    lpi.c_id = $courseId AND
10588
                    lpi.item_type = '".TOOL_QUIZ."' AND
10589
                    lpi.path = '$exerciseId'";
10590
        $result = Database::query($sql);
10591
        $lpList = [];
10592
        if (Database::num_rows($result) > 0) {
10593
            $lpList = Database::store_result($result, 'ASSOC');
10594
        }
10595
10596
        return $lpList;
10597
    }
10598
10599
    public function getReminderTable($questionList, $exercise_stat_info, $disableCheckBoxes = false)
10600
    {
10601
        $learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
10602
        $learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
10603
        $learnpath_item_view_id = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
10604
        $categoryId = isset($_REQUEST['category_id']) ? (int) $_REQUEST['category_id'] : 0;
10605
10606
        if (empty($exercise_stat_info)) {
10607
            return '';
10608
        }
10609
10610
        $remindList = $exercise_stat_info['questions_to_check'];
10611
        $remindList = explode(',', $remindList);
10612
        $exeId = $exercise_stat_info['exe_id'];
10613
        $exerciseId = $exercise_stat_info['exe_exo_id'];
10614
        $exercise_result = $this->getUserAnswersSavedInExercise($exeId);
10615
10616
        $content = Display::label(get_lang('QuestionWithNoAnswer'), 'danger');
10617
        $content .= '<div class="clear"></div><br />';
10618
        $table = '';
10619
        $counter = 0;
10620
        // Loop over all question to show results for each of them, one by one
10621
        foreach ($questionList as $questionId) {
10622
            $objQuestionTmp = Question::read($questionId);
10623
            $check_id = 'remind_list['.$questionId.']';
10624
            $attributes = [
10625
                'id' => $check_id,
10626
                'onclick' => "save_remind_item(this, '$questionId');",
10627
                'data-question-id' => $questionId,
10628
            ];
10629
            if (in_array($questionId, $remindList)) {
10630
                $attributes['checked'] = 1;
10631
            }
10632
10633
            $checkbox = Display::input('checkbox', 'remind_list['.$questionId.']', '', $attributes);
10634
            $checkbox = '<div class="pretty p-svg p-curve">
10635
                        '.$checkbox.'
10636
                        <div class="state p-primary ">
10637
                         <svg class="svg svg-icon" viewBox="0 0 20 20">
10638
                            <path d="M7.629,14.566c0.125,0.125,0.291,0.188,0.456,0.188c0.164,0,0.329-0.062,0.456-0.188l8.219-8.221c0.252-0.252,0.252-0.659,0-0.911c-0.252-0.252-0.659-0.252-0.911,0l-7.764,7.763L4.152,9.267c-0.252-0.251-0.66-0.251-0.911,0c-0.252,0.252-0.252,0.66,0,0.911L7.629,14.566z" style="stroke: white;fill:white;"></path>
10639
                         </svg>
10640
                         <label>&nbsp;</label>
10641
                        </div>
10642
                    </div>';
10643
            $counter++;
10644
            $questionTitle = $counter.'. '.strip_tags($objQuestionTmp->selectTitle());
10645
            // Check if the question doesn't have an answer.
10646
            if (!in_array($questionId, $exercise_result)) {
10647
                $questionTitle = Display::label($questionTitle, 'danger');
10648
            }
10649
            $label_attributes = [];
10650
            $label_attributes['for'] = $check_id;
10651
            if (false === $disableCheckBoxes) {
10652
                $questionTitle = Display::tag('label', $checkbox.$questionTitle, $label_attributes);
10653
            }
10654
            $table .= Display::div($questionTitle, ['class' => 'exercise_reminder_item ']);
10655
        }
10656
10657
        $content .= Display::div('', ['id' => 'message']).
10658
                    Display::div($table, ['class' => 'question-check-test']);
10659
10660
        $content .= '<script>
10661
        var lp_data = $.param({
10662
            "learnpath_id": '.$learnpath_id.',
10663
            "learnpath_item_id" : '.$learnpath_item_id.',
10664
            "learnpath_item_view_id": '.$learnpath_item_view_id.'
10665
        });
10666
10667
        function final_submit() {
10668
            // Normal inputs.
10669
            window.location = "'.api_get_path(WEB_CODE_PATH).'exercise/exercise_result.php?'.api_get_cidreq().'&exe_id='.$exeId.'&" + lp_data;
10670
        }
10671
10672
        function selectAll() {
10673
            $("input[type=checkbox]").each(function () {
10674
                $(this).prop("checked", 1);
10675
                var question_id = $(this).data("question-id");
10676
                var action = "add";
10677
                $.ajax({
10678
                    url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10679
                    data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
10680
                    success: function(returnValue) {
10681
                    }
10682
                });
10683
            });
10684
        }
10685
10686
        function changeOptionStatus(status)
10687
        {
10688
            $("input[type=checkbox]").each(function () {
10689
                $(this).prop("checked", status);
10690
            });
10691
            var action = "";
10692
            var option = "remove_all";
10693
            if (status == 1) {
10694
                option = "add_all";
10695
            }
10696
            $.ajax({
10697
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10698
                data: "option="+option+"&exe_id='.$exeId.'&action="+action,
10699
                success: function(returnValue) {
10700
                }
10701
            });
10702
        }
10703
10704
        function reviewQuestions() {
10705
            var isChecked = 1;
10706
            $("input[type=checkbox]").each(function () {
10707
                if ($(this).prop("checked")) {
10708
                    isChecked = 2;
10709
                    return false;
10710
                }
10711
            });
10712
10713
            if (isChecked == 1) {
10714
                $("#message").addClass("warning-message");
10715
                $("#message").html("'.addslashes(get_lang('SelectAQuestionToReview')).'");
10716
            } else {
10717
                window.location = "exercise_submit.php?'.api_get_cidreq().'&category_id='.$categoryId.'&exerciseId='.$exerciseId.'&reminder=2&" + lp_data;
10718
            }
10719
        }
10720
10721
        function save_remind_item(obj, question_id) {
10722
            var action = "";
10723
            if ($(obj).prop("checked")) {
10724
                action = "add";
10725
            } else {
10726
                action = "delete";
10727
            }
10728
            $.ajax({
10729
                url: "'.api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=add_question_to_reminder",
10730
                data: "question_id="+question_id+"&exe_id='.$exeId.'&action="+action,
10731
                success: function(returnValue) {
10732
                }
10733
            });
10734
        }
10735
        </script>';
10736
10737
        return $content;
10738
    }
10739
10740
    public function getRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10741
    {
10742
        $dataSet = [];
10743
        $labels = [];
10744
        $labelsWithId = [];
10745
        /** @var Exercise $exercise */
10746
        foreach ($exercises as $exercise) {
10747
            if (empty($labels)) {
10748
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10749
                if (!empty($categoryNameList)) {
10750
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10751
                    asort($labelsWithId);
10752
                    $labels = array_values($labelsWithId);
10753
                }
10754
            }
10755
10756
            foreach ($userList as $userId) {
10757
                $results = Event::getExerciseResultsByUser(
10758
                    $userId,
10759
                    $exercise->iId,
10760
                    $courseId,
10761
                    $sessionId
10762
                );
10763
10764
                if ($results) {
10765
                    $firstAttempt = end($results);
10766
                    $exeId = $firstAttempt['exe_id'];
10767
10768
                    ob_start();
10769
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10770
                        $exercise,
10771
                        $exeId,
10772
                        false
10773
                    );
10774
                    ob_end_clean();
10775
10776
                    $categoryList = $stats['category_list'];
10777
                    $tempResult = [];
10778
                    foreach ($labelsWithId as $category_id => $title) {
10779
                        if (isset($categoryList[$category_id])) {
10780
                            $category_item = $categoryList[$category_id];
10781
                            $tempResult[] = round($category_item['score'] / $category_item['total'] * 10);
10782
                        } else {
10783
                            $tempResult[] = 0;
10784
                        }
10785
                    }
10786
                    $dataSet[] = $tempResult;
10787
                }
10788
            }
10789
        }
10790
10791
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10792
    }
10793
10794
    public function getAverageRadarsFromUsers($userList, $exercises, $dataSetLabels, $courseId, $sessionId)
10795
    {
10796
        $dataSet = [];
10797
        $labels = [];
10798
        $labelsWithId = [];
10799
10800
        $tempResult = [];
10801
        /** @var Exercise $exercise */
10802
        foreach ($exercises as $exercise) {
10803
            $exerciseId = $exercise->iId;
10804
            if (empty($labels)) {
10805
                $categoryNameList = TestCategory::getListOfCategoriesNameForTest($exercise->iId);
10806
                if (!empty($categoryNameList)) {
10807
                    $labelsWithId = array_column($categoryNameList, 'title', 'id');
10808
                    asort($labelsWithId);
10809
                    $labels = array_values($labelsWithId);
10810
                }
10811
            }
10812
10813
            foreach ($userList as $userId) {
10814
                $results = Event::getExerciseResultsByUser(
10815
                    $userId,
10816
                    $exerciseId,
10817
                    $courseId,
10818
                    $sessionId
10819
                );
10820
10821
                if ($results) {
10822
                    $firstAttempt = end($results);
10823
                    $exeId = $firstAttempt['exe_id'];
10824
10825
                    ob_start();
10826
                    $stats = ExerciseLib::displayQuestionListByAttempt(
10827
                        $exercise,
10828
                        $exeId,
10829
                        false
10830
                    );
10831
                    ob_end_clean();
10832
10833
                    $categoryList = $stats['category_list'];
10834
                    foreach ($labelsWithId as $category_id => $title) {
10835
                        if (isset($categoryList[$category_id])) {
10836
                            $category_item = $categoryList[$category_id];
10837
                            if (!isset($tempResult[$exerciseId][$category_id])) {
10838
                                $tempResult[$exerciseId][$category_id] = 0;
10839
                            }
10840
                            $tempResult[$exerciseId][$category_id] += $category_item['score'] / $category_item['total'] * 10;
10841
                        }
10842
                    }
10843
                }
10844
            }
10845
        }
10846
10847
        $totalUsers = count($userList);
10848
10849
        foreach ($exercises as $exercise) {
10850
            $exerciseId = $exercise->iId;
10851
            $data = [];
10852
            foreach ($labelsWithId as $category_id => $title) {
10853
                if (isset($tempResult[$exerciseId]) && isset($tempResult[$exerciseId][$category_id])) {
10854
                    $data[] = round($tempResult[$exerciseId][$category_id] / $totalUsers);
10855
                } else {
10856
                    $data[] = 0;
10857
                }
10858
            }
10859
            $dataSet[] = $data;
10860
        }
10861
10862
        return $this->getRadar($labels, $dataSet, $dataSetLabels);
10863
    }
10864
10865
    public function getRadar($labels, $dataSet, $dataSetLabels = [])
10866
    {
10867
        if (empty($labels) || empty($dataSet)) {
10868
            return '';
10869
        }
10870
10871
        $displayLegend = 0;
10872
        if (!empty($dataSetLabels)) {
10873
            $displayLegend = 1;
10874
        }
10875
10876
        $labels = json_encode($labels);
10877
10878
        $colorList = ChamiloApi::getColorPalette(true, true);
10879
10880
        $dataSetToJson = [];
10881
        $counter = 0;
10882
        foreach ($dataSet as $index => $resultsArray) {
10883
            $color = isset($colorList[$counter]) ? $colorList[$counter] : 'rgb('.rand(0, 255).', '.rand(0, 255).', '.rand(0, 255).', 1.0)';
10884
10885
            $label = isset($dataSetLabels[$index]) ? $dataSetLabels[$index] : '';
10886
            $background = str_replace('1.0', '0.2', $color);
10887
            $dataSetToJson[] = [
10888
                'fill' => false,
10889
                'label' => $label,
10890
                'backgroundColor' => $background,
10891
                'borderColor' => $color,
10892
                'pointBackgroundColor' => $color,
10893
                'pointBorderColor' => '#fff',
10894
                'pointHoverBackgroundColor' => '#fff',
10895
                'pointHoverBorderColor' => $color,
10896
                'pointRadius' => 6,
10897
                'pointBorderWidth' => 3,
10898
                'pointHoverRadius' => 10,
10899
                'data' => $resultsArray,
10900
            ];
10901
            $counter++;
10902
        }
10903
        $resultsToJson = json_encode($dataSetToJson);
10904
10905
        return "
10906
                <canvas id='categoryRadar' height='200'></canvas>
10907
                <script>
10908
                    var data = {
10909
                        labels: $labels,
10910
                        datasets: $resultsToJson
10911
                    }
10912
                    var options = {
10913
                        responsive: true,
10914
                        scale: {
10915
                            angleLines: {
10916
                                display: false
10917
                            },
10918
                            ticks: {
10919
                                beginAtZero: true,
10920
                                min: 0,
10921
                                max: 10,
10922
                                stepSize: 1,
10923
                            },
10924
                            pointLabels: {
10925
                              fontSize: 14,
10926
                              //fontStyle: 'bold'
10927
                            },
10928
                        },
10929
                        elements: {
10930
                            line: {
10931
                                tension: 0,
10932
                                borderWidth: 3
10933
                            }
10934
                        },
10935
                        legend: {
10936
                            //position: 'bottom'
10937
                            display: $displayLegend
10938
                        },
10939
                        animation: {
10940
                            animateScale: true,
10941
                            animateRotate: true
10942
                        },
10943
                    };
10944
                    var ctx = document.getElementById('categoryRadar').getContext('2d');
10945
                    var myRadarChart = new Chart(ctx, {
10946
                        type: 'radar',
10947
                        data: data,
10948
                        options: options
10949
                    });
10950
                </script>
10951
                ";
10952
    }
10953
10954
    /**
10955
     * Returns a list of users based on the id of an exercise, the course and the session.
10956
     * If the count is true, it returns only the number of users.
10957
     *
10958
     * @param int   $exerciseId
10959
     * @param int   $courseId
10960
     * @param int   $sessionId
10961
     * @param false $count
10962
     */
10963
    public static function getUsersInExercise(
10964
        $exerciseId = 0,
10965
        $courseId = 0,
10966
        $sessionId = 0,
10967
        $count = false,
10968
        $toUsers = [],
10969
        $withSelectAll = true
10970
    ) {
10971
        $sessionId = empty($sessionId) ? api_get_session_id() : (int) $sessionId;
10972
        $courseId = empty($courseId) ? api_get_course_id() : (int) $courseId;
10973
        $exerciseId = (int) $exerciseId;
10974
        if ((0 == $sessionId && 0 == $courseId) || 0 == $exerciseId) {
10975
            return [];
10976
        }
10977
        $tblCourse = Database::get_main_table(TABLE_MAIN_COURSE);
10978
        $tblCourseRelUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
10979
        $tblSessionRelUser = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
10980
10981
        $data = [];
10982
10983
        $tblQuiz = Database::get_course_table(TABLE_QUIZ_TEST);
10984
        $countSelect = " COUNT(*) AS total ";
10985
        $sqlToUsers = '';
10986
        if (0 == $sessionId) {
10987
            // Courses
10988
            if (false === $count) {
10989
                $countSelect = " cq.title AS quiz_title,
10990
                    cq.iid AS quiz_id,
10991
                    cru.c_id AS course_id,
10992
                    cru.user_id AS user_id,
10993
                    c.title AS title,
10994
                    c.code AS 'code',
10995
                    cq.active AS active,
10996
                    cq.session_id AS session_id ";
10997
            }
10998
10999
            $sql = "SELECT $countSelect
11000
                FROM $tblCourseRelUser AS cru
11001
                INNER JOIN $tblCourse AS c ON ( cru.c_id = c.id )
11002
                INNER JOIN $tblQuiz AS cq ON ( cq.c_id = c.id )
11003
                WHERE cru.is_tutor IS NULL
11004
                    AND ( cq.session_id = 0 OR cq.session_id IS NULL)
11005
                    AND cq.active > 0
11006
                    AND cq.c_id = $courseId
11007
                    AND cq.iid = $exerciseId ";
11008
            if (!empty($toUsers)) {
11009
                $sqlToUsers = ' AND cru.user_id IN ('.implode(',', $toUsers).') ';
11010
            }
11011
        } else {
11012
            //Sessions
11013
            if (false === $count) {
11014
                $countSelect = " cq.title AS quiz_title,
11015
                    cq.iid AS quiz_id,
11016
                    sru.user_id AS user_id,
11017
                    cq.c_id AS course_id,
11018
                    sru.session_id AS session_id,
11019
                    c.title AS title,
11020
                    c.code AS 'code',
11021
                    cq.active AS active ";
11022
            }
11023
            if (!empty($toUsers)) {
11024
                $sqlToUsers = ' AND sru.user_id IN ('.implode(',', $toUsers).') ';
11025
            }
11026
            $sql = " SELECT $countSelect
11027
                FROM $tblSessionRelUser AS sru
11028
                    INNER JOIN $tblQuiz AS cq ON ( sru.session_id = sru.session_id )
11029
                    INNER JOIN $tblCourse AS c ON ( c.id = cq.c_id )
11030
                WHERE cq.active > 0
11031
                  AND cq.c_id = $courseId
11032
                  AND sru.session_id = $sessionId
11033
                  AND cq.iid = $exerciseId ";
11034
        }
11035
        $sql .= " $sqlToUsers ORDER BY cq.c_id ";
11036
11037
        $result = Database::query($sql);
11038
        $data = Database::store_result($result);
11039
        Database::free_result($result);
11040
        if (true === $count) {
11041
            return (isset($data[0]) && isset($data[0]['total'])) ? $data[0]['total'] : 0;
11042
        }
11043
        $usersArray = [];
11044
11045
        $return = [];
11046
        if ($withSelectAll) {
11047
            $return[] = [
11048
                'user_id' => 'X',
11049
                'value' => 'X',
11050
                'user_name' => get_lang('AllStudents'),
11051
            ];
11052
        }
11053
11054
        foreach ($data as $index => $item) {
11055
            if (isset($item['user_id'])) {
11056
                $userId = (int) $item['user_id'];
11057
                if (!isset($usersArray[$userId])) {
11058
                    $usersArray[$userId] = api_get_user_info($userId);
11059
                }
11060
                $usersArray['user_id'] = $userId;
11061
                $userData = $usersArray[$userId];
11062
                $data[$index]['user_name'] = $userData['complete_name'];
11063
                $return[] = $data[$index];
11064
            }
11065
        }
11066
11067
        return $return;
11068
    }
11069
11070
    /**
11071
     * Search the users who are in an exercise to send them an exercise reminder email and to the human resources
11072
     * managers, it is necessary the exercise id, the course and the session if it exists.
11073
     *
11074
     * @param int $exerciseId
11075
     * @param int $courseId
11076
     * @param int $sessionId
11077
     */
11078
    public static function notifyUsersOfTheExercise(
11079
        $exerciseId = 0,
11080
        $courseId = 0,
11081
        $sessionId = 0,
11082
        $toUsers = []
11083
    ) {
11084
        $users = self::getUsersInExercise(
11085
            $exerciseId,
11086
            $courseId,
11087
            $sessionId,
11088
            false,
11089
            $toUsers
11090
        );
11091
        $totalUsers = count($users);
11092
        $usersArray = [];
11093
        $courseTitle = '';
11094
        $quizTitle = '';
11095
11096
        for ($i = 0; $i < $totalUsers; $i++) {
11097
            $user = $users[$i];
11098
            $userId = (int) $user['user_id'];
11099
            if (0 != $userId) {
11100
                $quizTitle = $user['quiz_title'];
11101
                $courseTitle = $user['title'];
11102
                if (!isset($usersArray[$userId])) {
11103
                    $usersArray[$userId] = api_get_user_info($userId);
11104
                }
11105
            }
11106
        }
11107
11108
        $url = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'
11109
            .api_get_cidreq()."&exerciseId=$exerciseId";
11110
        $link = "<a href=\"$url\">$url</a>";
11111
11112
        $objExerciseTmp = new Exercise();
11113
        $objExerciseTmp->read($exerciseId);
11114
        $end = $objExerciseTmp->end_time;
11115
        $start = $objExerciseTmp->start_time;
11116
        $minutes = $objExerciseTmp->expired_time;
11117
        $formatDate = DATE_TIME_FORMAT_LONG;
11118
        $tblCourseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
11119
        $tblSession = Database::get_main_table(TABLE_MAIN_SESSION);
11120
        $tblSessionUser = Database::get_main_table(TABLE_MAIN_SESSION_USER);
11121
        $tblSessionUserRelCourse = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
11122
        $teachersName = [];
11123
        $teachersPrint = [];
11124
        if (0 == $sessionId) {
11125
            $sql = "SELECT course_user.user_id AS user_id
11126
                FROM $tblCourseUser AS course_user
11127
                WHERE course_user.status = 1 AND course_user.c_id ='".$courseId."'";
11128
            $result = Database::query($sql);
11129
            $data = Database::store_result($result);
11130
            Database::free_result($result);
11131
            foreach ($data as $teacher) {
11132
                $teacherId = (int) $teacher['user_id'];
11133
                if (!isset($teachersName[$teacherId])) {
11134
                    $teachersName[$teacherId] = api_get_user_info($teacherId);
11135
                }
11136
                $teacherData = $teachersName[$teacherId];
11137
                $teachersPrint[] = $teacherData['complete_name'];
11138
            }
11139
        } else {
11140
            // general tutor
11141
            $sql = "SELECT sesion.id_coach AS user_id
11142
                FROM $tblSession AS sesion
11143
                WHERE sesion.id = $sessionId";
11144
            $result = Database::query($sql);
11145
            $data = Database::store_result($result);
11146
            Database::free_result($result);
11147
            foreach ($data as $teacher) {
11148
                $teacherId = (int) $teacher['user_id'];
11149
                if (!isset($teachersName[$teacherId])) {
11150
                    $teachersName[$teacherId] = api_get_user_info($teacherId);
11151
                }
11152
                $teacherData = $teachersName[$teacherId];
11153
                $teachersPrint[] = $teacherData['complete_name'];
11154
            }
11155
            // Teacher into sessions course
11156
            $sql = "SELECT session_rel_course_rel_user.user_id
11157
                FROM $tblSessionUserRelCourse AS session_rel_course_rel_user
11158
                WHERE session_rel_course_rel_user.session_id = $sessionId AND
11159
                      session_rel_course_rel_user.c_id = $courseId AND
11160
                      session_rel_course_rel_user.status = 2";
11161
            $result = Database::query($sql);
11162
            $data = Database::store_result($result);
11163
            Database::free_result($result);
11164
            foreach ($data as $teacher) {
11165
                $teacherId = (int) $teacher['user_id'];
11166
                if (!isset($teachersName[$teacherId])) {
11167
                    $teachersName[$teacherId] = api_get_user_info($teacherId);
11168
                }
11169
                $teacherData = $teachersName[$teacherId];
11170
                $teachersPrint[] = $teacherData['complete_name'];
11171
            }
11172
        }
11173
11174
        $teacherName = implode('<br>', $teachersPrint);
11175
11176
        foreach ($usersArray as $userId => $userData) {
11177
            $studentName = $userData['complete_name'];
11178
            $title = sprintf(get_lang('QuizRemindSubject'), $teacherName);
11179
            $content = sprintf(
11180
                get_lang('QuizFirstRemindBody'),
11181
                $studentName,
11182
                $quizTitle,
11183
                $courseTitle,
11184
                $courseTitle,
11185
                $quizTitle
11186
            );
11187
            if (!empty($minutes)) {
11188
                $content .= sprintf(get_lang('QuizRemindDuration'), $minutes);
11189
            }
11190
            if (!empty($start)) {
11191
                // api_get_utc_datetime
11192
                $start = api_format_date(($start), $formatDate);
11193
11194
                $content .= sprintf(get_lang('QuizRemindStartDate'), $start);
11195
            }
11196
            if (!empty($end)) {
11197
                $end = api_format_date(($end), $formatDate);
11198
                $content .= sprintf(get_lang('QuizRemindEndDate'), $end);
11199
            }
11200
            $content .= sprintf(
11201
                get_lang('QuizLastRemindBody'),
11202
                $link,
11203
                $link,
11204
                $teacherName
11205
            );
11206
            $drhList = UserManager::getDrhListFromUser($userId);
11207
            if (!empty($drhList)) {
11208
                foreach ($drhList as $drhUser) {
11209
                    $drhUserData = api_get_user_info($drhUser['id']);
11210
                    $drhName = $drhUserData['complete_name'];
11211
                    $contentDHR = sprintf(
11212
                        get_lang('QuizDhrRemindBody'),
11213
                        $drhName,
11214
                        $studentName,
11215
                        $quizTitle,
11216
                        $courseTitle,
11217
                        $studentName,
11218
                        $courseTitle,
11219
                        $quizTitle
11220
                    );
11221
                    if (!empty($minutes)) {
11222
                        $contentDHR .= sprintf(get_lang('QuizRemindDuration'), $minutes);
11223
                    }
11224
                    if (!empty($start)) {
11225
                        // api_get_utc_datetime
11226
                        $start = api_format_date(($start), $formatDate);
11227
11228
                        $contentDHR .= sprintf(get_lang('QuizRemindStartDate'), $start);
11229
                    }
11230
                    if (!empty($end)) {
11231
                        $end = api_format_date(($end), $formatDate);
11232
                        $contentDHR .= sprintf(get_lang('QuizRemindEndDate'), $end);
11233
                    }
11234
                    MessageManager::send_message(
11235
                        $drhUser['id'],
11236
                        $title,
11237
                        $contentDHR
11238
                    );
11239
                }
11240
            }
11241
            MessageManager::send_message(
11242
                $userData['id'],
11243
                $title,
11244
                $content
11245
            );
11246
        }
11247
11248
        return $usersArray;
11249
    }
11250
11251
    /**
11252
     * Returns true if the exercise is locked by percentage. an exercise attempt must be passed.
11253
     */
11254
    public function isBlockedByPercentage(array $attempt = []): bool
11255
    {
11256
        if (empty($attempt)) {
11257
            return false;
11258
        }
11259
        $extraFieldValue = new ExtraFieldValue('exercise');
11260
        $blockExercise = $extraFieldValue->get_values_by_handler_and_field_variable(
11261
            $this->iId,
11262
            'blocking_percentage'
11263
        );
11264
11265
        if (empty($blockExercise['value'])) {
11266
            return false;
11267
        }
11268
11269
        $blockPercentage = (int) $blockExercise['value'];
11270
11271
        if (0 === $blockPercentage) {
11272
            return false;
11273
        }
11274
11275
        $resultPercentage = 0;
11276
11277
        if (isset($attempt['exe_result']) && isset($attempt['exe_weighting'])) {
11278
            $weight = (int) $attempt['exe_weighting'];
11279
            $weight = (0 == $weight) ? 1 : $weight;
11280
            $resultPercentage = float_format(
11281
                ($attempt['exe_result'] / $weight) * 100,
11282
                1
11283
            );
11284
        }
11285
        if ($resultPercentage <= $blockPercentage) {
11286
            return true;
11287
        }
11288
11289
        return false;
11290
    }
11291
11292
    /**
11293
     * Gets the question list ordered by the question_order setting (drag and drop).
11294
     *
11295
     * @param bool $adminView Optional.
11296
     *
11297
     * @return array
11298
     */
11299
    public function getQuestionOrderedList($adminView = false)
11300
    {
11301
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
11302
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
11303
11304
        // Getting question_order to verify that the question
11305
        // list is correct and all question_order's were set
11306
        $sql = "SELECT DISTINCT count(e.question_order) as count
11307
                FROM $TBL_EXERCICE_QUESTION e
11308
                INNER JOIN $TBL_QUESTIONS q
11309
                ON (e.question_id = q.id AND e.c_id = q.c_id)
11310
                WHERE
11311
                  e.c_id = {$this->course_id} AND
11312
                  e.exercice_id	= ".$this->id;
11313
11314
        $result = Database::query($sql);
11315
        $row = Database::fetch_array($result);
11316
        $count_question_orders = $row['count'];
11317
11318
        // Getting question list from the order (question list drag n drop interface).
11319
        $sql = "SELECT DISTINCT e.question_id, e.question_order
11320
                FROM $TBL_EXERCICE_QUESTION e
11321
                INNER JOIN $TBL_QUESTIONS q
11322
                ON (e.question_id = q.id AND e.c_id = q.c_id)
11323
                WHERE
11324
                    e.c_id = {$this->course_id} AND
11325
                    e.exercice_id = '".$this->id."'
11326
                ORDER BY question_order";
11327
        $result = Database::query($sql);
11328
11329
        // Fills the array with the question ID for this exercise
11330
        // the key of the array is the question position
11331
        $temp_question_list = [];
11332
        $counter = 1;
11333
        $questionList = [];
11334
        while ($new_object = Database::fetch_object($result)) {
11335
            if (!$adminView) {
11336
                // Correct order.
11337
                $questionList[$new_object->question_order] = $new_object->question_id;
11338
            } else {
11339
                $questionList[$counter] = $new_object->question_id;
11340
            }
11341
11342
            // Just in case we save the order in other array
11343
            $temp_question_list[$counter] = $new_object->question_id;
11344
            $counter++;
11345
        }
11346
11347
        if (!empty($temp_question_list)) {
11348
            /* If both array don't match it means that question_order was not correctly set
11349
               for all questions using the default mysql order */
11350
            if (count($temp_question_list) != $count_question_orders) {
11351
                $questionList = $temp_question_list;
11352
            }
11353
        }
11354
11355
        return $questionList;
11356
    }
11357
11358
    /**
11359
     * Get number of questions in exercise by user attempt.
11360
     *
11361
     * @return int
11362
     */
11363
    private function countQuestionsInExercise()
11364
    {
11365
        $lpId = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
11366
        $lpItemId = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
11367
        $lpItemViewId = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
11368
11369
        $trackInfo = $this->get_stat_track_exercise_info($lpId, $lpItemId, $lpItemViewId);
11370
11371
        if (!empty($trackInfo)) {
11372
            $questionIds = explode(',', $trackInfo['data_tracking']);
11373
11374
            return count($questionIds);
11375
        }
11376
11377
        return $this->getQuestionCount();
11378
    }
11379
11380
    /**
11381
     * Select N values from the questions per category array.
11382
     *
11383
     * @param array $categoriesAddedInExercise
11384
     * @param array $question_list
11385
     * @param array $questions_by_category
11386
     * @param bool  $flatResult
11387
     * @param bool  $randomizeQuestions
11388
     * @param array $questionsByCategoryMandatory
11389
     *
11390
     * @return array
11391
     */
11392
    private function pickQuestionsPerCategory(
11393
        $categoriesAddedInExercise,
11394
        $question_list,
11395
        &$questions_by_category,
11396
        $flatResult = true,
11397
        $randomizeQuestions = false,
11398
        $questionsByCategoryMandatory = []
11399
    ) {
11400
        $addAll = true;
11401
        $categoryCountArray = [];
11402
11403
        // Getting how many questions will be selected per category.
11404
        if (!empty($categoriesAddedInExercise)) {
11405
            $addAll = false;
11406
            // Parsing question according the category rel exercise settings
11407
            foreach ($categoriesAddedInExercise as $category_info) {
11408
                $category_id = $category_info['category_id'];
11409
                if (isset($questions_by_category[$category_id])) {
11410
                    // How many question will be picked from this category.
11411
                    $count = $category_info['count_questions'];
11412
                    // -1 means all questions
11413
                    $categoryCountArray[$category_id] = $count;
11414
                    if (-1 == $count) {
11415
                        $categoryCountArray[$category_id] = 999;
11416
                    }
11417
                }
11418
            }
11419
        }
11420
11421
        if (!empty($questions_by_category)) {
11422
            $temp_question_list = [];
11423
            foreach ($questions_by_category as $category_id => &$categoryQuestionList) {
11424
                if (isset($categoryCountArray) && !empty($categoryCountArray)) {
11425
                    $numberOfQuestions = 0;
11426
                    if (isset($categoryCountArray[$category_id])) {
11427
                        $numberOfQuestions = $categoryCountArray[$category_id];
11428
                    }
11429
                }
11430
11431
                if ($addAll) {
11432
                    $numberOfQuestions = 999;
11433
                }
11434
                if (!empty($numberOfQuestions)) {
11435
                    $mandatoryQuestions = [];
11436
                    if (isset($questionsByCategoryMandatory[$category_id])) {
11437
                        $mandatoryQuestions = $questionsByCategoryMandatory[$category_id];
11438
                    }
11439
11440
                    $elements = TestCategory::getNElementsFromArray(
11441
                        $categoryQuestionList,
11442
                        $numberOfQuestions,
11443
                        $randomizeQuestions,
11444
                        $mandatoryQuestions
11445
                    );
11446
                    if (!empty($elements)) {
11447
                        $temp_question_list[$category_id] = $elements;
11448
                        $categoryQuestionList = $elements;
11449
                    }
11450
                }
11451
            }
11452
11453
            if (!empty($temp_question_list)) {
11454
                if ($flatResult) {
11455
                    $temp_question_list = array_flatten($temp_question_list);
11456
                }
11457
                $question_list = $temp_question_list;
11458
            }
11459
        }
11460
11461
        return $question_list;
11462
    }
11463
11464
    /**
11465
     * Changes the exercise id.
11466
     *
11467
     * @param int $id - exercise id
11468
     */
11469
    private function updateId($id)
11470
    {
11471
        $this->id = $id;
11472
    }
11473
11474
    /**
11475
     * Sends a notification when a user ends an examn.
11476
     *
11477
     * @param array  $question_list_answers
11478
     * @param string $origin
11479
     * @param array  $user_info
11480
     * @param string $url_email
11481
     * @param array  $teachers
11482
     */
11483
    private function sendNotificationForOpenQuestions(
11484
        $question_list_answers,
11485
        $origin,
11486
        $user_info,
11487
        $url_email,
11488
        $teachers
11489
    ) {
11490
        // Email configuration settings
11491
        $courseCode = api_get_course_id();
11492
        $courseInfo = api_get_course_info($courseCode);
11493
        $sessionId = api_get_session_id();
11494
        $sessionData = '';
11495
        if (!empty($sessionId)) {
11496
            $sessionInfo = api_get_session_info($sessionId);
11497
            if (!empty($sessionInfo)) {
11498
                $sessionData = '<tr>'
11499
                    .'<td><em>'.get_lang('SessionName').'</em></td>'
11500
                    .'<td>&nbsp;<b>'.$sessionInfo['name'].'</b></td>'
11501
                    .'</tr>';
11502
            }
11503
        }
11504
11505
        $msg = get_lang('OpenQuestionsAttempted').'<br /><br />'
11506
            .get_lang('AttemptDetails').' : <br /><br />'
11507
            .'<table>'
11508
            .'<tr>'
11509
            .'<td><em>'.get_lang('CourseName').'</em></td>'
11510
            .'<td>&nbsp;<b>#course#</b></td>'
11511
            .'</tr>'
11512
            .$sessionData
11513
            .'<tr>'
11514
            .'<td>'.get_lang('TestAttempted').'</td>'
11515
            .'<td>&nbsp;#exercise#</td>'
11516
            .'</tr>'
11517
            .'<tr>'
11518
            .'<td>'.get_lang('StudentName').'</td>'
11519
            .'<td>&nbsp;#firstName# #lastName#</td>'
11520
            .'</tr>'
11521
            .'<tr>'
11522
            .'<td>'.get_lang('StudentEmail').'</td>'
11523
            .'<td>&nbsp;#mail#</td>'
11524
            .'</tr>'
11525
            .'</table>';
11526
11527
        $open_question_list = null;
11528
        foreach ($question_list_answers as $item) {
11529
            $question = $item['question'];
11530
            $answer = $item['answer'];
11531
            $answer_type = $item['answer_type'];
11532
            if (!empty($question) && !empty($answer) && $answer_type == FREE_ANSWER) {
11533
                $open_question_list .=
11534
                    '<tr>'
11535
                    .'<td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>'
11536
                    .'<td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>'
11537
                    .'</tr>'
11538
                    .'<tr>'
11539
                    .'<td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>'
11540
                    .'<td valign="top" bgcolor="#F3F3F3">'.$answer.'</td>'
11541
                    .'</tr>';
11542
            }
11543
        }
11544
11545
        if (!empty($open_question_list)) {
11546
            $msg .= '<p><br />'.get_lang('OpenQuestionsAttemptedAre').' :</p>'.
11547
                '<table width="730" height="136" border="0" cellpadding="3" cellspacing="3">';
11548
            $msg .= $open_question_list;
11549
            $msg .= '</table><br />';
11550
11551
            $msg = str_replace('#exercise#', $this->exercise, $msg);
11552
            $msg = str_replace('#firstName#', $user_info['firstname'], $msg);
11553
            $msg = str_replace('#lastName#', $user_info['lastname'], $msg);
11554
            $msg = str_replace('#mail#', $user_info['email'], $msg);
11555
            $msg = str_replace(
11556
                '#course#',
11557
                Display::url($courseInfo['title'], $courseInfo['course_public_url'].'?id_session='.$sessionId),
11558
                $msg
11559
            );
11560
11561
            if ($origin != 'learnpath') {
11562
                $msg .= '<br /><a href="#url#">'.get_lang('ClickToCommentAndGiveFeedback').'</a>';
11563
            }
11564
            $msg = str_replace('#url#', $url_email, $msg);
11565
            $subject = get_lang('OpenQuestionsAttempted');
11566
11567
            if (!empty($teachers)) {
11568
                foreach ($teachers as $user_id => $teacher_data) {
11569
                    MessageManager::send_message_simple(
11570
                        $user_id,
11571
                        $subject,
11572
                        $msg
11573
                    );
11574
                }
11575
            }
11576
        }
11577
    }
11578
11579
    /**
11580
     * Send notification for oral questions.
11581
     *
11582
     * @param array  $question_list_answers
11583
     * @param string $origin
11584
     * @param int    $exe_id
11585
     * @param array  $user_info
11586
     * @param string $url_email
11587
     * @param array  $teachers
11588
     */
11589
    private function sendNotificationForOralQuestions(
11590
        $question_list_answers,
11591
        $origin,
11592
        $exe_id,
11593
        $user_info,
11594
        $url_email,
11595
        $teachers
11596
    ) {
11597
        // Email configuration settings
11598
        $courseCode = api_get_course_id();
11599
        $courseInfo = api_get_course_info($courseCode);
11600
        $oral_question_list = null;
11601
        foreach ($question_list_answers as $item) {
11602
            $question = $item['question'];
11603
            $file = $item['generated_oral_file'];
11604
            $answer = $item['answer'];
11605
            if (0 == $answer) {
11606
                $answer = '';
11607
            }
11608
            $answer_type = $item['answer_type'];
11609
            if (!empty($question) && (!empty($answer) || !empty($file)) && $answer_type == ORAL_EXPRESSION) {
11610
                if (!empty($file)) {
11611
                    $file = Display::url($file, $file);
11612
                }
11613
                $oral_question_list .= '<br />
11614
                    <table width="730" height="136" border="0" cellpadding="3" cellspacing="3">
11615
                    <tr>
11616
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Question').'</td>
11617
                        <td width="473" valign="top" bgcolor="#F3F3F3">'.$question.'</td>
11618
                    </tr>
11619
                    <tr>
11620
                        <td width="220" valign="top" bgcolor="#E5EDF8">&nbsp;&nbsp;'.get_lang('Answer').'</td>
11621
                        <td valign="top" bgcolor="#F3F3F3">'.$answer.$file.'</td>
11622
                    </tr></table>';
11623
            }
11624
        }
11625
11626
        if (!empty($oral_question_list)) {
11627
            $msg = get_lang('OralQuestionsAttempted').'<br /><br />
11628
                    '.get_lang('AttemptDetails').' : <br /><br />
11629
                    <table>
11630
                        <tr>
11631
                            <td><em>'.get_lang('CourseName').'</em></td>
11632
                            <td>&nbsp;<b>#course#</b></td>
11633
                        </tr>
11634
                        <tr>
11635
                            <td>'.get_lang('TestAttempted').'</td>
11636
                            <td>&nbsp;#exercise#</td>
11637
                        </tr>
11638
                        <tr>
11639
                            <td>'.get_lang('StudentName').'</td>
11640
                            <td>&nbsp;#firstName# #lastName#</td>
11641
                        </tr>
11642
                        <tr>
11643
                            <td>'.get_lang('StudentEmail').'</td>
11644
                            <td>&nbsp;#mail#</td>
11645
                        </tr>
11646
                    </table>';
11647
            $msg .= '<br />'.sprintf(get_lang('OralQuestionsAttemptedAreX'), $oral_question_list).'<br />';
11648
            $msg1 = str_replace("#exercise#", $this->exercise, $msg);
11649
            $msg = str_replace("#firstName#", $user_info['firstname'], $msg1);
11650
            $msg1 = str_replace("#lastName#", $user_info['lastname'], $msg);
11651
            $msg = str_replace("#mail#", $user_info['email'], $msg1);
11652
            $msg = str_replace("#course#", $courseInfo['name'], $msg1);
11653
11654
            if (!in_array($origin, ['learnpath', 'embeddable'])) {
11655
                $msg .= '<br /><a href="#url#">'.get_lang('ClickToCommentAndGiveFeedback').'</a>';
11656
            }
11657
            $msg1 = str_replace("#url#", $url_email, $msg);
11658
            $mail_content = $msg1;
11659
            $subject = get_lang('OralQuestionsAttempted');
11660
11661
            if (!empty($teachers)) {
11662
                foreach ($teachers as $user_id => $teacher_data) {
11663
                    MessageManager::send_message_simple(
11664
                        $user_id,
11665
                        $subject,
11666
                        $mail_content
11667
                    );
11668
                }
11669
            }
11670
        }
11671
    }
11672
11673
    /**
11674
     * Returns an array with the media list.
11675
     *
11676
     * @param array question list
11677
     *
11678
     * @example there's 1 question with iid 5 that belongs to the media question with iid = 100
11679
     * <code>
11680
     * array (size=2)
11681
     *  999 =>
11682
     *    array (size=3)
11683
     *      0 => int 7
11684
     *      1 => int 6
11685
     *      2 => int 3254
11686
     *  100 =>
11687
     *   array (size=1)
11688
     *      0 => int 5
11689
     *  </code>
11690
     */
11691
    private function setMediaList($questionList)
11692
    {
11693
        $mediaList = [];
11694
        /*
11695
         * Media feature is not activated in 1.11.x
11696
        if (!empty($questionList)) {
11697
            foreach ($questionList as $questionId) {
11698
                $objQuestionTmp = Question::read($questionId, $this->course_id);
11699
                // If a media question exists
11700
                if (isset($objQuestionTmp->parent_id) && $objQuestionTmp->parent_id != 0) {
11701
                    $mediaList[$objQuestionTmp->parent_id][] = $objQuestionTmp->id;
11702
                } else {
11703
                    // Always the last item
11704
                    $mediaList[999][] = $objQuestionTmp->id;
11705
                }
11706
            }
11707
        }*/
11708
11709
        $this->mediaList = $mediaList;
11710
    }
11711
11712
    /**
11713
     * @return HTML_QuickForm_group
11714
     */
11715
    private function setResultDisabledGroup(FormValidator $form)
11716
    {
11717
        $resultDisabledGroup = [];
11718
11719
        $resultDisabledGroup[] = $form->createElement(
11720
            'radio',
11721
            'results_disabled',
11722
            null,
11723
            get_lang('ShowScoreAndRightAnswer'),
11724
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
11725
            ['id' => 'result_disabled_0']
11726
        );
11727
11728
        $warning = sprintf(
11729
            get_lang('TheSettingXWillChangeToX'),
11730
            get_lang('FeedbackType'),
11731
            get_lang('NoFeedback')
11732
        );
11733
11734
        $resultDisabledGroup[] = $form->createElement(
11735
            'radio',
11736
            'results_disabled',
11737
            null,
11738
            get_lang('DoNotShowScoreNorRightAnswer'),
11739
            RESULT_DISABLE_NO_SCORE_AND_EXPECTED_ANSWERS,
11740
            [
11741
                'id' => 'result_disabled_1',
11742
                //'onclick' => 'check_results_disabled()'
11743
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
11744
            ]
11745
        );
11746
11747
        $resultDisabledGroup[] = $form->createElement(
11748
            'radio',
11749
            'results_disabled',
11750
            null,
11751
            get_lang('OnlyShowScore'),
11752
            RESULT_DISABLE_SHOW_SCORE_ONLY,
11753
            [
11754
                'id' => 'result_disabled_2',
11755
                //'onclick' => 'check_results_disabled()'
11756
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
11757
            ]
11758
        );
11759
11760
        if (in_array($this->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
11761
            return $form->addGroup(
11762
                $resultDisabledGroup,
11763
                null,
11764
                get_lang('ShowResultsToStudents')
11765
            );
11766
        }
11767
11768
        $resultDisabledGroup[] = $form->createElement(
11769
            'radio',
11770
            'results_disabled',
11771
            null,
11772
            get_lang('ShowScoreEveryAttemptShowAnswersLastAttempt'),
11773
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
11774
            ['id' => 'result_disabled_4']
11775
        );
11776
11777
        $resultDisabledGroup[] = $form->createElement(
11778
            'radio',
11779
            'results_disabled',
11780
            null,
11781
            get_lang('DontShowScoreOnlyWhenUserFinishesAllAttemptsButShowFeedbackEachAttempt'),
11782
            RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
11783
            [
11784
                'id' => 'result_disabled_5',
11785
                //'onclick' => 'check_results_disabled()'
11786
                'onclick' => 'javascript:if(confirm('."'".addslashes($warning)."'".')) { check_results_disabled(); } else { return false;} ',
11787
            ]
11788
        );
11789
11790
        $resultDisabledGroup[] = $form->createElement(
11791
            'radio',
11792
            'results_disabled',
11793
            null,
11794
            get_lang('ExerciseRankingMode'),
11795
            RESULT_DISABLE_RANKING,
11796
            ['id' => 'result_disabled_6']
11797
        );
11798
11799
        $resultDisabledGroup[] = $form->createElement(
11800
            'radio',
11801
            'results_disabled',
11802
            null,
11803
            get_lang('ExerciseShowOnlyGlobalScoreAndCorrectAnswers'),
11804
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
11805
            ['id' => 'result_disabled_7']
11806
        );
11807
11808
        $resultDisabledGroup[] = $form->createElement(
11809
            'radio',
11810
            'results_disabled',
11811
            null,
11812
            get_lang('ExerciseAutoEvaluationAndRankingMode'),
11813
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
11814
            ['id' => 'result_disabled_8']
11815
        );
11816
11817
        $resultDisabledGroup[] = $form->createElement(
11818
            'radio',
11819
            'results_disabled',
11820
            null,
11821
            get_lang('ExerciseCategoriesRadarMode'),
11822
            RESULT_DISABLE_RADAR,
11823
            ['id' => 'result_disabled_9']
11824
        );
11825
11826
        $resultDisabledGroup[] = $form->createElement(
11827
            'radio',
11828
            'results_disabled',
11829
            null,
11830
            get_lang('ShowScoreEveryAttemptShowAnswersLastAttemptNoFeedback'),
11831
            RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
11832
            ['id' => 'result_disabled_10']
11833
        );
11834
11835
        return $form->addGroup(
11836
            $resultDisabledGroup,
11837
            null,
11838
            get_lang('ShowResultsToStudents')
11839
        );
11840
    }
11841
}
11842