Passed
Push — master ( 1cf373...d04f46 )
by Julito
09:24
created

Exercise::renderQuestion()   D

Complexity

Conditions 20

Size

Total Lines 181
Code Lines 94

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
eloc 94
c 0
b 0
f 0
nop 9
dl 0
loc 181
rs 4.1666

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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