Completed
Push — master ( 6c6efe...024a20 )
by Julito
09:28
created

Exercise::saveExerciseInLp()   B

Complexity

Conditions 9

Size

Total Lines 79
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 55
nop 2
dl 0
loc 79
rs 7.4262
c 0
b 0
f 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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