Completed
Push — master ( 3742bc...55196a )
by Julito
13:21
created

Exercise::pickQuestionsPerCategory()   C

Complexity

Conditions 15

Size

Total Lines 65
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 33
nop 5
dl 0
loc 65
rs 5.9166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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