Passed
Push — 1.11.x ( ed53f7...9ff550 )
by Julito
13:55
created

Question::setMandatory()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CourseBundle\Entity\CQuizAnswer;
6
7
/**
8
 * Class Question.
9
 *
10
 * This class allows to instantiate an object of type Question
11
 *
12
 * @author Olivier Brouckaert, original author
13
 * @author Patrick Cool, LaTeX support
14
 * @author Julio Montoya <[email protected]> lot of bug fixes
15
 * @author [email protected] - add question categories
16
 */
17
abstract class Question
18
{
19
    public $id;
20
    public $iid;
21
    public $question;
22
    public $description;
23
    public $weighting;
24
    public $position;
25
    public $type;
26
    public $level;
27
    public $picture;
28
    public $exerciseList; // array with the list of exercises which this question is in
29
    public $category_list;
30
    public $parent_id;
31
    public $category;
32
    public $mandatory;
33
    public $isContent;
34
    public $course;
35
    public $feedback;
36
    public $typePicture = 'new_question.png';
37
    public $explanationLangVar = '';
38
    public $question_table_class = 'table table-striped';
39
    public $questionTypeWithFeedback;
40
    public $extra;
41
    public $export = false;
42
    public $code;
43
    public static $questionTypes = [
44
        UNIQUE_ANSWER => ['unique_answer.class.php', 'UniqueAnswer'],
45
        MULTIPLE_ANSWER => ['multiple_answer.class.php', 'MultipleAnswer'],
46
        FILL_IN_BLANKS => ['fill_blanks.class.php', 'FillBlanks'],
47
        MATCHING => ['matching.class.php', 'Matching'],
48
        FREE_ANSWER => ['freeanswer.class.php', 'FreeAnswer'],
49
        ORAL_EXPRESSION => ['oral_expression.class.php', 'OralExpression'],
50
        HOT_SPOT => ['hotspot.class.php', 'HotSpot'],
51
        HOT_SPOT_DELINEATION => ['HotSpotDelineation.php', 'HotSpotDelineation'],
52
        MULTIPLE_ANSWER_COMBINATION => ['multiple_answer_combination.class.php', 'MultipleAnswerCombination'],
53
        UNIQUE_ANSWER_NO_OPTION => ['unique_answer_no_option.class.php', 'UniqueAnswerNoOption'],
54
        MULTIPLE_ANSWER_TRUE_FALSE => ['multiple_answer_true_false.class.php', 'MultipleAnswerTrueFalse'],
55
        MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY => [
56
            'MultipleAnswerTrueFalseDegreeCertainty.php',
57
            'MultipleAnswerTrueFalseDegreeCertainty',
58
        ],
59
        MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE => [
60
            'multiple_answer_combination_true_false.class.php',
61
            'MultipleAnswerCombinationTrueFalse',
62
        ],
63
        GLOBAL_MULTIPLE_ANSWER => ['global_multiple_answer.class.php', 'GlobalMultipleAnswer'],
64
        CALCULATED_ANSWER => ['calculated_answer.class.php', 'CalculatedAnswer'],
65
        UNIQUE_ANSWER_IMAGE => ['UniqueAnswerImage.php', 'UniqueAnswerImage'],
66
        DRAGGABLE => ['Draggable.php', 'Draggable'],
67
        MATCHING_DRAGGABLE => ['MatchingDraggable.php', 'MatchingDraggable'],
68
        //MEDIA_QUESTION => array('media_question.class.php' , 'MediaQuestion')
69
        ANNOTATION => ['Annotation.php', 'Annotation'],
70
        READING_COMPREHENSION => ['ReadingComprehension.php', 'ReadingComprehension'],
71
    ];
72
73
    /**
74
     * constructor of the class.
75
     *
76
     * @author Olivier Brouckaert
77
     */
78
    public function __construct()
79
    {
80
        $this->id = 0;
81
        $this->iid = 0;
82
        $this->question = '';
83
        $this->description = '';
84
        $this->weighting = 0;
85
        $this->position = 1;
86
        $this->picture = '';
87
        $this->level = 1;
88
        $this->category = 0;
89
        // This variable is used when loading an exercise like an scenario with
90
        // an special hotspot: final_overlap, final_missing, final_excess
91
        $this->extra = '';
92
        $this->exerciseList = [];
93
        $this->course = api_get_course_info();
94
        $this->category_list = [];
95
        $this->parent_id = 0;
96
        $this->mandatory = 0;
97
        // See BT#12611
98
        $this->questionTypeWithFeedback = [
99
            MATCHING,
100
            MATCHING_DRAGGABLE,
101
            DRAGGABLE,
102
            FILL_IN_BLANKS,
103
            FREE_ANSWER,
104
            ORAL_EXPRESSION,
105
            CALCULATED_ANSWER,
106
            ANNOTATION,
107
        ];
108
    }
109
110
    /**
111
     * @return int|null
112
     */
113
    public function getIsContent()
114
    {
115
        $isContent = null;
116
        if (isset($_REQUEST['isContent'])) {
117
            $isContent = (int) $_REQUEST['isContent'];
118
        }
119
120
        return $this->isContent = $isContent;
121
    }
122
123
    /**
124
     * Reads question information from the data base.
125
     *
126
     * @param int   $id              - question ID
127
     * @param array $course_info
128
     * @param bool  $getExerciseList
129
     *
130
     * @return Question
131
     *
132
     * @author Olivier Brouckaert
133
     */
134
    public static function read($id, $course_info = [], $getExerciseList = true)
135
    {
136
        $id = (int) $id;
137
        if (empty($course_info)) {
138
            $course_info = api_get_course_info();
139
        }
140
        $course_id = $course_info['real_id'];
141
142
        if (empty($course_id) || -1 == $course_id) {
143
            return false;
144
        }
145
146
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
147
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
148
149
        $sql = "SELECT *
150
                FROM $TBL_QUESTIONS
151
                WHERE c_id = $course_id AND id = $id ";
152
        $result = Database::query($sql);
153
154
        // if the question has been found
155
        if ($object = Database::fetch_object($result)) {
156
            $objQuestion = self::getInstance($object->type);
157
            if (!empty($objQuestion)) {
158
                $objQuestion->id = (int) $id;
159
                $objQuestion->iid = (int) $object->iid;
160
                $objQuestion->question = $object->question;
161
                $objQuestion->description = $object->description;
162
                $objQuestion->weighting = $object->ponderation;
163
                $objQuestion->position = $object->position;
164
                $objQuestion->type = (int) $object->type;
165
                $objQuestion->picture = $object->picture;
166
                $objQuestion->level = (int) $object->level;
167
                $objQuestion->extra = $object->extra;
168
                $objQuestion->course = $course_info;
169
                $objQuestion->feedback = isset($object->feedback) ? $object->feedback : '';
170
                $objQuestion->code = isset($object->code) ? $object->code : '';
171
                $categoryInfo = TestCategory::getCategoryInfoForQuestion($id, $course_id);
172
                if (!empty($categoryInfo)) {
173
                    if (isset($categoryInfo['category_id'])) {
174
                        $objQuestion->category = (int) $categoryInfo['category_id'];
175
                    }
176
177
                    if (api_get_configuration_value('allow_mandatory_question_in_category') &&
178
                        isset($categoryInfo['mandatory'])
179
                    ) {
180
                        $objQuestion->mandatory = (int) $categoryInfo['mandatory'];
181
                    }
182
                }
183
184
                if ($getExerciseList) {
185
                    $tblQuiz = Database::get_course_table(TABLE_QUIZ_TEST);
186
                    $sql = "SELECT DISTINCT q.exercice_id
187
                            FROM $TBL_EXERCISE_QUESTION q
188
                            INNER JOIN $tblQuiz e
189
                            ON e.c_id = q.c_id AND e.id = q.exercice_id
190
                            WHERE
191
                                q.c_id = $course_id AND
192
                                q.question_id = $id AND
193
                                e.active >= 0";
194
195
                    $result = Database::query($sql);
196
197
                    // fills the array with the exercises which this question is in
198
                    if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
199
                        while ($obj = Database::fetch_object($result)) {
200
                            $objQuestion->exerciseList[] = $obj->exercice_id;
201
                        }
202
                    }
203
                }
204
205
                return $objQuestion;
206
            }
207
        }
208
209
        // question not found
210
        return false;
211
    }
212
213
    /**
214
     * returns the question ID.
215
     *
216
     * @author Olivier Brouckaert
217
     *
218
     * @return int - question ID
219
     */
220
    public function selectId()
221
    {
222
        return $this->id;
223
    }
224
225
    /**
226
     * returns the question title.
227
     *
228
     * @author Olivier Brouckaert
229
     *
230
     * @return string - question title
231
     */
232
    public function selectTitle()
233
    {
234
        if (!api_get_configuration_value('save_titles_as_html')) {
235
            return $this->question;
236
        }
237
238
        return Display::div($this->question, ['style' => 'display: inline-block;']);
239
    }
240
241
    /**
242
     * @param int $itemNumber
243
     *
244
     * @return string
245
     */
246
    public function getTitleToDisplay($itemNumber)
247
    {
248
        $showQuestionTitleHtml = api_get_configuration_value('save_titles_as_html');
249
        $title = '';
250
        if (api_get_configuration_value('show_question_id')) {
251
            $title .= '<h4>#'.$this->course['code'].'-'.$this->iid.'</h4>';
252
        }
253
254
        $title .= $showQuestionTitleHtml ? '' : '<strong>';
255
        $title .= $itemNumber.'. '.$this->selectTitle();
256
        $title .= $showQuestionTitleHtml ? '' : '</strong>';
257
258
        return Display::div(
259
            $title,
260
            ['class' => 'question_title']
261
        );
262
    }
263
264
    /**
265
     * returns the question description.
266
     *
267
     * @author Olivier Brouckaert
268
     *
269
     * @return string - question description
270
     */
271
    public function selectDescription()
272
    {
273
        return $this->description;
274
    }
275
276
    /**
277
     * returns the question weighting.
278
     *
279
     * @author Olivier Brouckaert
280
     *
281
     * @return int - question weighting
282
     */
283
    public function selectWeighting()
284
    {
285
        return $this->weighting;
286
    }
287
288
    /**
289
     * returns the question position.
290
     *
291
     * @author Olivier Brouckaert
292
     *
293
     * @return int - question position
294
     */
295
    public function selectPosition()
296
    {
297
        return $this->position;
298
    }
299
300
    /**
301
     * returns the answer type.
302
     *
303
     * @author Olivier Brouckaert
304
     *
305
     * @return int - answer type
306
     */
307
    public function selectType()
308
    {
309
        return $this->type;
310
    }
311
312
    /**
313
     * returns the level of the question.
314
     *
315
     * @author Nicolas Raynaud
316
     *
317
     * @return int - level of the question, 0 by default
318
     */
319
    public function getLevel()
320
    {
321
        return $this->level;
322
    }
323
324
    /**
325
     * returns the picture name.
326
     *
327
     * @author Olivier Brouckaert
328
     *
329
     * @return string - picture name
330
     */
331
    public function selectPicture()
332
    {
333
        return $this->picture;
334
    }
335
336
    /**
337
     * @return string
338
     */
339
    public function selectPicturePath()
340
    {
341
        if (!empty($this->picture)) {
342
            return api_get_path(WEB_COURSE_PATH).$this->course['directory'].'/document/images/'.$this->getPictureFilename();
343
        }
344
345
        return '';
346
    }
347
348
    /**
349
     * @return int|string
350
     */
351
    public function getPictureId()
352
    {
353
        // for backward compatibility
354
        // when in field picture we had the filename not the document id
355
        if (preg_match("/quiz-.*/", $this->picture)) {
356
            return DocumentManager::get_document_id(
357
                $this->course,
358
                $this->selectPicturePath(),
359
                api_get_session_id()
360
            );
361
        }
362
363
        return $this->picture;
364
    }
365
366
    /**
367
     * @param int $courseId
368
     * @param int $sessionId
369
     *
370
     * @return string
371
     */
372
    public function getPictureFilename($courseId = 0, $sessionId = 0)
373
    {
374
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
375
        $sessionId = empty($sessionId) ? api_get_session_id() : (int) $sessionId;
376
377
        if (empty($courseId)) {
378
            return '';
379
        }
380
        // for backward compatibility
381
        // when in field picture we had the filename not the document id
382
        if (preg_match("/quiz-.*/", $this->picture)) {
383
            return $this->picture;
384
        }
385
386
        $pictureId = $this->getPictureId();
387
        $courseInfo = $this->course;
388
        $documentInfo = DocumentManager::get_document_data_by_id(
389
            $pictureId,
390
            $courseInfo['code'],
391
            false,
392
            $sessionId
393
        );
394
        $documentFilename = '';
395
        if ($documentInfo) {
396
            // document in document/images folder
397
            $documentFilename = pathinfo(
398
                $documentInfo['path'],
399
                PATHINFO_BASENAME
400
            );
401
        }
402
403
        return $documentFilename;
404
    }
405
406
    /**
407
     * returns the array with the exercise ID list.
408
     *
409
     * @author Olivier Brouckaert
410
     *
411
     * @return array - list of exercise ID which the question is in
412
     */
413
    public function selectExerciseList()
414
    {
415
        return $this->exerciseList;
416
    }
417
418
    /**
419
     * returns the number of exercises which this question is in.
420
     *
421
     * @author Olivier Brouckaert
422
     *
423
     * @return int - number of exercises
424
     */
425
    public function selectNbrExercises()
426
    {
427
        return count($this->exerciseList);
428
    }
429
430
    /**
431
     * changes the question title.
432
     *
433
     * @param string $title - question title
434
     *
435
     * @author Olivier Brouckaert
436
     */
437
    public function updateTitle($title)
438
    {
439
        $this->question = $title;
440
    }
441
442
    /**
443
     * @param int $id
444
     */
445
    public function updateParentId($id)
446
    {
447
        $this->parent_id = (int) $id;
448
    }
449
450
    /**
451
     * changes the question description.
452
     *
453
     * @param string $description - question description
454
     *
455
     * @author Olivier Brouckaert
456
     */
457
    public function updateDescription($description)
458
    {
459
        $this->description = $description;
460
    }
461
462
    /**
463
     * changes the question weighting.
464
     *
465
     * @param int $weighting - question weighting
466
     *
467
     * @author Olivier Brouckaert
468
     */
469
    public function updateWeighting($weighting)
470
    {
471
        $this->weighting = $weighting;
472
    }
473
474
    /**
475
     * @param array $category
476
     *
477
     * @author Hubert Borderiou 12-10-2011
478
     */
479
    public function updateCategory($category)
480
    {
481
        $this->category = $category;
482
    }
483
484
    public function setMandatory($value)
485
    {
486
        $this->mandatory = (int) $value;
487
    }
488
489
    /**
490
     * @param int $value
491
     *
492
     * @author Hubert Borderiou 12-10-2011
493
     */
494
    public function updateScoreAlwaysPositive($value)
495
    {
496
        $this->scoreAlwaysPositive = $value;
497
    }
498
499
    /**
500
     * @param int $value
501
     *
502
     * @author Hubert Borderiou 12-10-2011
503
     */
504
    public function updateUncheckedMayScore($value)
505
    {
506
        $this->uncheckedMayScore = $value;
507
    }
508
509
    /**
510
     * Save category of a question.
511
     *
512
     * A question can have n categories if category is empty,
513
     * then question has no category then delete the category entry
514
     *
515
     * @param array $category_list
516
     *
517
     * @author Julio Montoya - Adding multiple cat support
518
     */
519
    public function saveCategories($category_list)
520
    {
521
        if (!empty($category_list)) {
522
            $this->deleteCategory();
523
            $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
524
525
            // update or add category for a question
526
            foreach ($category_list as $category_id) {
527
                $category_id = (int) $category_id;
528
                $question_id = (int) $this->id;
529
                $sql = "SELECT count(*) AS nb
530
                        FROM $table
531
                        WHERE
532
                            category_id = $category_id
533
                            AND question_id = $question_id
534
                            AND c_id=".api_get_course_int_id();
535
                $res = Database::query($sql);
536
                $row = Database::fetch_array($res);
537
                if ($row['nb'] > 0) {
538
                    // DO nothing
539
                } else {
540
                    $sql = "INSERT INTO $table (c_id, question_id, category_id)
541
                            VALUES (".api_get_course_int_id().", $question_id, $category_id)";
542
                    Database::query($sql);
543
                }
544
            }
545
        }
546
    }
547
548
    /**
549
     * In this version, a question can only have 1 category.
550
     * If category is 0, then question has no category then delete the category entry.
551
     *
552
     * @param int $categoryId
553
     * @param int $courseId
554
     *
555
     * @return bool
556
     *
557
     * @author Hubert Borderiou 12-10-2011
558
     */
559
    public function saveCategory($categoryId, $courseId = 0)
560
    {
561
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
562
563
        if (empty($courseId)) {
564
            return false;
565
        }
566
567
        if ($categoryId <= 0) {
568
            $this->deleteCategory($courseId);
569
        } else {
570
            // update or add category for a question
571
            $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
572
            $categoryId = (int) $categoryId;
573
            $question_id = (int) $this->id;
574
            $sql = "SELECT count(*) AS nb FROM $table
575
                    WHERE
576
                        question_id = $question_id AND
577
                        c_id = ".$courseId;
578
            $res = Database::query($sql);
579
            $row = Database::fetch_array($res);
580
            $allowMandatory = api_get_configuration_value('allow_mandatory_question_in_category');
581
            if ($row['nb'] > 0) {
582
                $extraMandatoryCondition = '';
583
                if ($allowMandatory) {
584
                    $extraMandatoryCondition = ", mandatory = {$this->mandatory}";
585
                }
586
                $sql = "UPDATE $table
587
                        SET category_id = $categoryId
588
                        $extraMandatoryCondition
589
                        WHERE
590
                            question_id = $question_id AND
591
                            c_id = ".$courseId;
592
                Database::query($sql);
593
            } else {
594
                $sql = "INSERT INTO $table (c_id, question_id, category_id)
595
                        VALUES (".$courseId.", $question_id, $categoryId)";
596
                Database::query($sql);
597
598
                if ($allowMandatory) {
599
                    $id = Database::insert_id();
600
                    if ($id) {
601
                        $sql = "UPDATE $table SET mandatory = {$this->mandatory}
602
                                WHERE iid = $id";
603
                        Database::query($sql);
604
                    }
605
                }
606
            }
607
608
            return true;
609
        }
610
    }
611
612
    /**
613
     * @author hubert borderiou 12-10-2011
614
     *
615
     * @param int $courseId
616
     *                      delete any category entry for question id
617
     *                      delete the category for question
618
     *
619
     * @return bool
620
     */
621
    public function deleteCategory($courseId = 0)
622
    {
623
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
624
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
625
        $questionId = (int) $this->id;
626
        if (empty($courseId) || empty($questionId)) {
627
            return false;
628
        }
629
        $sql = "DELETE FROM $table
630
                WHERE
631
                    question_id = $questionId AND
632
                    c_id = ".$courseId;
633
        Database::query($sql);
634
635
        return true;
636
    }
637
638
    /**
639
     * changes the question position.
640
     *
641
     * @param int $position - question position
642
     *
643
     * @author Olivier Brouckaert
644
     */
645
    public function updatePosition($position)
646
    {
647
        $this->position = $position;
648
    }
649
650
    /**
651
     * changes the question level.
652
     *
653
     * @param int $level - question level
654
     *
655
     * @author Nicolas Raynaud
656
     */
657
    public function updateLevel($level)
658
    {
659
        $this->level = $level;
660
    }
661
662
    /**
663
     * changes the answer type. If the user changes the type from "unique answer" to "multiple answers"
664
     * (or conversely) answers are not deleted, otherwise yes.
665
     *
666
     * @param int $type - answer type
667
     *
668
     * @author Olivier Brouckaert
669
     */
670
    public function updateType($type)
671
    {
672
        $table = Database::get_course_table(TABLE_QUIZ_ANSWER);
673
        $course_id = $this->course['real_id'];
674
675
        if (empty($course_id)) {
676
            $course_id = api_get_course_int_id();
677
        }
678
        // if we really change the type
679
        if ($type != $this->type) {
680
            // if we don't change from "unique answer" to "multiple answers" (or conversely)
681
            if (!in_array($this->type, [UNIQUE_ANSWER, MULTIPLE_ANSWER]) ||
682
                !in_array($type, [UNIQUE_ANSWER, MULTIPLE_ANSWER])
683
            ) {
684
                // removes old answers
685
                $sql = "DELETE FROM $table
686
                        WHERE c_id = $course_id AND question_id = ".intval($this->id);
687
                Database::query($sql);
688
            }
689
690
            $this->type = $type;
691
        }
692
    }
693
694
    /**
695
     * Get default hot spot folder in documents.
696
     *
697
     * @param array $courseInfo
698
     *
699
     * @return string
700
     */
701
    public function getHotSpotFolderInCourse($courseInfo = [])
702
    {
703
        $courseInfo = empty($courseInfo) ? $this->course : $courseInfo;
704
705
        if (empty($courseInfo) || empty($courseInfo['directory'])) {
706
            // Stop everything if course is not set.
707
            api_not_allowed();
708
        }
709
710
        $pictureAbsolutePath = api_get_path(SYS_COURSE_PATH).$courseInfo['directory'].'/document/images/';
711
        $picturePath = basename($pictureAbsolutePath);
712
713
        if (!is_dir($picturePath)) {
714
            create_unexisting_directory(
715
                $courseInfo,
716
                api_get_user_id(),
717
                0,
718
                0,
719
                0,
720
                dirname($pictureAbsolutePath),
721
                '/'.$picturePath,
722
                $picturePath,
723
                '',
724
                false,
725
                false
726
            );
727
        }
728
729
        return $pictureAbsolutePath;
730
    }
731
732
    /**
733
     * adds a picture to the question.
734
     *
735
     * @param string $picture - temporary path of the picture to upload
736
     *
737
     * @return bool - true if uploaded, otherwise false
738
     *
739
     * @author Olivier Brouckaert
740
     */
741
    public function uploadPicture($picture)
742
    {
743
        $picturePath = $this->getHotSpotFolderInCourse();
744
745
        // if the question has got an ID
746
        if ($this->id) {
747
            $pictureFilename = self::generatePictureName();
748
            $img = new Image($picture);
749
            $img->send_image($picturePath.'/'.$pictureFilename, -1, 'jpg');
750
            $document_id = add_document(
751
                $this->course,
752
                '/images/'.$pictureFilename,
753
                'file',
754
                filesize($picturePath.'/'.$pictureFilename),
755
                $pictureFilename
756
            );
757
758
            if ($document_id) {
759
                $this->picture = $document_id;
760
761
                if (!file_exists($picturePath.'/'.$pictureFilename)) {
762
                    return false;
763
                }
764
765
                api_item_property_update(
766
                    $this->course,
767
                    TOOL_DOCUMENT,
768
                    $document_id,
769
                    'DocumentAdded',
770
                    api_get_user_id()
771
                );
772
773
                $this->resizePicture('width', 800);
774
775
                return true;
776
            }
777
        }
778
779
        return false;
780
    }
781
782
    /**
783
     * return the name for image use in hotspot question
784
     * to be unique, name is quiz-[utc unix timestamp].jpg.
785
     *
786
     * @param string $prefix
787
     * @param string $extension
788
     *
789
     * @return string
790
     */
791
    public function generatePictureName($prefix = 'quiz-', $extension = 'jpg')
792
    {
793
        // image name is quiz-xxx.jpg in folder images/
794
        $utcTime = time();
795
796
        return $prefix.$utcTime.'.'.$extension;
797
    }
798
799
    /**
800
     * deletes the picture.
801
     *
802
     * @author Olivier Brouckaert
803
     *
804
     * @return bool - true if removed, otherwise false
805
     */
806
    public function removePicture()
807
    {
808
        $picturePath = $this->getHotSpotFolderInCourse();
809
810
        // if the question has got an ID and if the picture exists
811
        if ($this->id) {
812
            $picture = $this->picture;
813
            $this->picture = '';
814
815
            return @unlink($picturePath.'/'.$picture) ? true : false;
816
        }
817
818
        return false;
819
    }
820
821
    /**
822
     * Exports a picture to another question.
823
     *
824
     * @author Olivier Brouckaert
825
     *
826
     * @param int   $questionId - ID of the target question
827
     * @param array $courseInfo destination course info
828
     *
829
     * @return bool - true if copied, otherwise false
830
     */
831
    public function exportPicture($questionId, $courseInfo)
832
    {
833
        if (empty($questionId) || empty($courseInfo)) {
834
            return false;
835
        }
836
837
        $course_id = $courseInfo['real_id'];
838
        $destination_path = $this->getHotSpotFolderInCourse($courseInfo);
839
840
        if (empty($destination_path)) {
841
            return false;
842
        }
843
844
        $source_path = $this->getHotSpotFolderInCourse();
845
846
        // if the question has got an ID and if the picture exists
847
        if (!$this->id || empty($this->picture)) {
848
            return false;
849
        }
850
851
        $sourcePictureName = $this->getPictureFilename($course_id);
852
        $picture = $this->generatePictureName();
853
        $result = false;
854
        if (file_exists($source_path.'/'.$sourcePictureName)) {
855
            // for backward compatibility
856
            $result = copy(
857
                $source_path.'/'.$sourcePictureName,
858
                $destination_path.'/'.$picture
859
            );
860
        } else {
861
            $imageInfo = DocumentManager::get_document_data_by_id(
862
                $this->picture,
863
                $courseInfo['code']
864
            );
865
            if (file_exists($imageInfo['absolute_path'])) {
866
                $result = @copy(
867
                    $imageInfo['absolute_path'],
868
                    $destination_path.'/'.$picture
869
                );
870
            }
871
        }
872
873
        // If copy was correct then add to the database
874
        if (!$result) {
875
            return false;
876
        }
877
878
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION);
879
        $sql = "UPDATE $table SET
880
                picture = '".Database::escape_string($picture)."'
881
                WHERE c_id = $course_id AND id='".intval($questionId)."'";
882
        Database::query($sql);
883
884
        $documentId = add_document(
885
            $courseInfo,
886
            '/images/'.$picture,
887
            'file',
888
            filesize($destination_path.'/'.$picture),
889
            $picture
890
        );
891
892
        if (!$documentId) {
893
            return false;
894
        }
895
896
        return api_item_property_update(
897
            $courseInfo,
898
            TOOL_DOCUMENT,
899
            $documentId,
900
            'DocumentAdded',
901
            api_get_user_id()
902
        );
903
    }
904
905
    /**
906
     * Saves the picture coming from POST into a temporary file
907
     * Temporary pictures are used when we don't want to save a picture right after a form submission.
908
     * For example, if we first show a confirmation box.
909
     *
910
     * @author Olivier Brouckaert
911
     *
912
     * @param string $picture     - temporary path of the picture to move
913
     * @param string $pictureName - Name of the picture
914
     */
915
    public function setTmpPicture($picture, $pictureName)
916
    {
917
        $picturePath = $this->getHotSpotFolderInCourse();
918
        $pictureName = explode('.', $pictureName);
919
        $Extension = $pictureName[sizeof($pictureName) - 1];
920
921
        // saves the picture into a temporary file
922
        @move_uploaded_file($picture, $picturePath.'/tmp.'.$Extension);
923
    }
924
925
    /**
926
     * Set title.
927
     *
928
     * @param string $title
929
     */
930
    public function setTitle($title)
931
    {
932
        $this->question = $title;
933
    }
934
935
    /**
936
     * Sets extra info.
937
     *
938
     * @param string $extra
939
     */
940
    public function setExtra($extra)
941
    {
942
        $this->extra = $extra;
943
    }
944
945
    /**
946
     * Updates the question in the database.
947
     * if an exercise ID is provided, we add that exercise ID into the exercise list.
948
     *
949
     * @author Olivier Brouckaert
950
     *
951
     * @param Exercise $exercise
952
     */
953
    public function save($exercise)
954
    {
955
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
956
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
957
        $em = Database::getManager();
958
        $exerciseId = $exercise->id;
959
960
        $id = $this->id;
961
        $question = $this->question;
962
        $description = $this->description;
963
        $weighting = $this->weighting;
964
        $position = $this->position;
965
        $type = $this->type;
966
        $picture = $this->picture;
967
        $level = $this->level;
968
        $extra = $this->extra;
969
        $c_id = $this->course['real_id'];
970
        $categoryId = $this->category;
971
972
        // question already exists
973
        if (!empty($id)) {
974
            $params = [
975
                'question' => $question,
976
                'description' => $description,
977
                'ponderation' => $weighting,
978
                'position' => $position,
979
                'type' => $type,
980
                'picture' => $picture,
981
                'extra' => $extra,
982
                'level' => $level,
983
            ];
984
            if ($exercise->questionFeedbackEnabled) {
985
                $params['feedback'] = $this->feedback;
986
            }
987
988
            Database::update(
989
                $TBL_QUESTIONS,
990
                $params,
991
                ['c_id = ? AND id = ?' => [$c_id, $id]]
992
            );
993
994
            Event::addEvent(
995
                LOG_QUESTION_UPDATED,
996
                LOG_QUESTION_ID,
997
                $this->iid
998
            );
999
            $this->saveCategory($categoryId);
1000
1001
            if (!empty($exerciseId)) {
1002
                api_item_property_update(
1003
                    $this->course,
1004
                    TOOL_QUIZ,
1005
                    $id,
1006
                    'QuizQuestionUpdated',
1007
                    api_get_user_id()
1008
                );
1009
            }
1010
            if (api_get_setting('search_enabled') === 'true') {
1011
                $this->search_engine_edit($exerciseId);
1012
            }
1013
        } else {
1014
            // Creates a new question.
1015
            $sql = "SELECT max(position)
1016
                    FROM $TBL_QUESTIONS as question,
1017
                    $TBL_EXERCISE_QUESTION as test_question
1018
                    WHERE
1019
                        question.id = test_question.question_id AND
1020
                        test_question.exercice_id = ".$exerciseId." AND
1021
                        question.c_id = $c_id AND
1022
                        test_question.c_id = $c_id ";
1023
            $result = Database::query($sql);
1024
            $current_position = Database::result($result, 0, 0);
1025
            $this->updatePosition($current_position + 1);
1026
            $position = $this->position;
1027
1028
            $params = [
1029
                'c_id' => $c_id,
1030
                'question' => $question,
1031
                'description' => $description,
1032
                'ponderation' => $weighting,
1033
                'position' => $position,
1034
                'type' => $type,
1035
                'picture' => $picture,
1036
                'extra' => $extra,
1037
                'level' => $level,
1038
            ];
1039
1040
            if ($exercise->questionFeedbackEnabled) {
1041
                $params['feedback'] = $this->feedback;
1042
            }
1043
            $this->id = Database::insert($TBL_QUESTIONS, $params);
1044
1045
            if ($this->id) {
1046
                $sql = "UPDATE $TBL_QUESTIONS SET id = iid WHERE iid = {$this->id}";
1047
                Database::query($sql);
1048
1049
                Event::addEvent(
1050
                    LOG_QUESTION_CREATED,
1051
                    LOG_QUESTION_ID,
1052
                    $this->id
1053
                );
1054
1055
                api_item_property_update(
1056
                    $this->course,
1057
                    TOOL_QUIZ,
1058
                    $this->id,
1059
                    'QuizQuestionAdded',
1060
                    api_get_user_id()
1061
                );
1062
1063
                // If hotspot, create first answer
1064
                if ($type == HOT_SPOT || $type == HOT_SPOT_ORDER) {
1065
                    $quizAnswer = new CQuizAnswer();
1066
                    $quizAnswer
1067
                        ->setCId($c_id)
1068
                        ->setQuestionId($this->id)
1069
                        ->setAnswer('')
1070
                        ->setPonderation(10)
1071
                        ->setPosition(1)
1072
                        ->setHotspotCoordinates('0;0|0|0')
1073
                        ->setHotspotType('square');
1074
1075
                    $em->persist($quizAnswer);
1076
                    $em->flush();
1077
1078
                    $id = $quizAnswer->getIid();
1079
1080
                    if ($id) {
1081
                        $quizAnswer
1082
                            ->setId($id)
1083
                            ->setIdAuto($id);
1084
1085
                        $em->merge($quizAnswer);
1086
                        $em->flush();
1087
                    }
1088
                }
1089
1090
                if ($type == HOT_SPOT_DELINEATION) {
1091
                    $quizAnswer = new CQuizAnswer();
1092
                    $quizAnswer
1093
                        ->setCId($c_id)
1094
                        ->setQuestionId($this->id)
1095
                        ->setAnswer('')
1096
                        ->setPonderation(10)
1097
                        ->setPosition(1)
1098
                        ->setHotspotCoordinates('0;0|0|0')
1099
                        ->setHotspotType('delineation');
1100
1101
                    $em->persist($quizAnswer);
1102
                    $em->flush();
1103
1104
                    $id = $quizAnswer->getIid();
1105
1106
                    if ($id) {
1107
                        $quizAnswer
1108
                            ->setId($id)
1109
                            ->setIdAuto($id);
1110
1111
                        $em->merge($quizAnswer);
1112
                        $em->flush();
1113
                    }
1114
                }
1115
1116
                if (api_get_setting('search_enabled') === 'true') {
1117
                    $this->search_engine_edit($exerciseId, true);
1118
                }
1119
            }
1120
        }
1121
1122
        // if the question is created in an exercise
1123
        if (!empty($exerciseId)) {
1124
            // adds the exercise into the exercise list of this question
1125
            $this->addToList($exerciseId, true);
1126
        }
1127
    }
1128
1129
    /**
1130
     * @param int  $exerciseId
1131
     * @param bool $addQs
1132
     * @param bool $rmQs
1133
     */
1134
    public function search_engine_edit(
1135
        $exerciseId,
1136
        $addQs = false,
1137
        $rmQs = false
1138
    ) {
1139
        // update search engine and its values table if enabled
1140
        if (!empty($exerciseId) && api_get_setting('search_enabled') == 'true' &&
1141
            extension_loaded('xapian')
1142
        ) {
1143
            $course_id = api_get_course_id();
1144
            // get search_did
1145
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
1146
            if ($addQs || $rmQs) {
1147
                //there's only one row per question on normal db and one document per question on search engine db
1148
                $sql = 'SELECT * FROM %s
1149
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_second_level=%s LIMIT 1';
1150
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
1151
            } else {
1152
                $sql = 'SELECT * FROM %s
1153
                    WHERE course_code=\'%s\' AND tool_id=\'%s\'
1154
                    AND ref_id_high_level=%s AND ref_id_second_level=%s LIMIT 1';
1155
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
1156
            }
1157
            $res = Database::query($sql);
1158
1159
            if (Database::num_rows($res) > 0 || $addQs) {
1160
                $di = new ChamiloIndexer();
1161
                if ($addQs) {
1162
                    $question_exercises = [(int) $exerciseId];
1163
                } else {
1164
                    $question_exercises = [];
1165
                }
1166
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
1167
                $di->connectDb(null, null, $lang);
1168
1169
                // retrieve others exercise ids
1170
                $se_ref = Database::fetch_array($res);
1171
                $se_doc = $di->get_document((int) $se_ref['search_did']);
1172
                if ($se_doc !== false) {
1173
                    if (($se_doc_data = $di->get_document_data($se_doc)) !== false) {
1174
                        $se_doc_data = UnserializeApi::unserialize(
1175
                            'not_allowed_classes',
1176
                            $se_doc_data
1177
                        );
1178
                        if (isset($se_doc_data[SE_DATA]['type']) &&
1179
                            $se_doc_data[SE_DATA]['type'] == SE_DOCTYPE_EXERCISE_QUESTION
1180
                        ) {
1181
                            if (isset($se_doc_data[SE_DATA]['exercise_ids']) &&
1182
                                is_array($se_doc_data[SE_DATA]['exercise_ids'])
1183
                            ) {
1184
                                foreach ($se_doc_data[SE_DATA]['exercise_ids'] as $old_value) {
1185
                                    if (!in_array($old_value, $question_exercises)) {
1186
                                        $question_exercises[] = $old_value;
1187
                                    }
1188
                                }
1189
                            }
1190
                        }
1191
                    }
1192
                }
1193
                if ($rmQs) {
1194
                    while (($key = array_search($exerciseId, $question_exercises)) !== false) {
1195
                        unset($question_exercises[$key]);
1196
                    }
1197
                }
1198
1199
                // build the chunk to index
1200
                $ic_slide = new IndexableChunk();
1201
                $ic_slide->addValue('title', $this->question);
1202
                $ic_slide->addCourseId($course_id);
1203
                $ic_slide->addToolId(TOOL_QUIZ);
1204
                $xapian_data = [
1205
                    SE_COURSE_ID => $course_id,
1206
                    SE_TOOL_ID => TOOL_QUIZ,
1207
                    SE_DATA => [
1208
                        'type' => SE_DOCTYPE_EXERCISE_QUESTION,
1209
                        'exercise_ids' => $question_exercises,
1210
                        'question_id' => (int) $this->id,
1211
                    ],
1212
                    SE_USER => (int) api_get_user_id(),
1213
                ];
1214
                $ic_slide->xapian_data = serialize($xapian_data);
1215
                $ic_slide->addValue('content', $this->description);
1216
1217
                //TODO: index answers, see also form validation on question_admin.inc.php
1218
1219
                $di->remove_document($se_ref['search_did']);
1220
                $di->addChunk($ic_slide);
1221
1222
                //index and return search engine document id
1223
                if (!empty($question_exercises)) { // if empty there is nothing to index
1224
                    $did = $di->index();
1225
                    unset($di);
1226
                }
1227
                if ($did || $rmQs) {
1228
                    // save it to db
1229
                    if ($addQs || $rmQs) {
1230
                        $sql = "DELETE FROM %s
1231
                            WHERE course_code = '%s' AND tool_id = '%s' AND ref_id_second_level = '%s'";
1232
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
1233
                    } else {
1234
                        $sql = "DELETE FROM %S
1235
                            WHERE
1236
                                course_code = '%s'
1237
                                AND tool_id = '%s'
1238
                                AND tool_id = '%s'
1239
                                AND ref_id_high_level = '%s'
1240
                                AND ref_id_second_level = '%s'";
1241
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
1242
                    }
1243
                    Database::query($sql);
1244
                    if ($rmQs) {
1245
                        if (!empty($question_exercises)) {
1246
                            $sql = "INSERT INTO %s (
1247
                                    id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
1248
                                )
1249
                                VALUES (
1250
                                    NULL, '%s', '%s', %s, %s, %s
1251
                                )";
1252
                            $sql = sprintf(
1253
                                $sql,
1254
                                $tbl_se_ref,
1255
                                $course_id,
1256
                                TOOL_QUIZ,
1257
                                array_shift($question_exercises),
1258
                                $this->id,
1259
                                $did
1260
                            );
1261
                            Database::query($sql);
1262
                        }
1263
                    } else {
1264
                        $sql = "INSERT INTO %s (
1265
                                id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
1266
                            )
1267
                            VALUES (
1268
                                NULL , '%s', '%s', %s, %s, %s
1269
                            )";
1270
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id, $did);
1271
                        Database::query($sql);
1272
                    }
1273
                }
1274
            }
1275
        }
1276
    }
1277
1278
    /**
1279
     * adds an exercise into the exercise list.
1280
     *
1281
     * @author Olivier Brouckaert
1282
     *
1283
     * @param int  $exerciseId - exercise ID
1284
     * @param bool $fromSave   - from $this->save() or not
1285
     */
1286
    public function addToList($exerciseId, $fromSave = false)
1287
    {
1288
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1289
        $id = (int) $this->id;
1290
        $exerciseId = (int) $exerciseId;
1291
1292
        // checks if the exercise ID is not in the list
1293
        if (!empty($exerciseId) && !in_array($exerciseId, $this->exerciseList)) {
1294
            $this->exerciseList[] = $exerciseId;
1295
            $courseId = isset($this->course['real_id']) ? $this->course['real_id'] : 0;
1296
            $newExercise = new Exercise($courseId);
1297
            $newExercise->read($exerciseId, false);
1298
            $count = $newExercise->getQuestionCount();
1299
            $count++;
1300
            $sql = "INSERT INTO $exerciseRelQuestionTable (c_id, question_id, exercice_id, question_order)
1301
                    VALUES ({$this->course['real_id']}, ".$id.", ".$exerciseId.", '$count')";
1302
            Database::query($sql);
1303
1304
            // we do not want to reindex if we had just saved adnd indexed the question
1305
            if (!$fromSave) {
1306
                $this->search_engine_edit($exerciseId, true);
1307
            }
1308
        }
1309
    }
1310
1311
    /**
1312
     * removes an exercise from the exercise list.
1313
     *
1314
     * @author Olivier Brouckaert
1315
     *
1316
     * @param int $exerciseId - exercise ID
1317
     * @param int $courseId
1318
     *
1319
     * @return bool - true if removed, otherwise false
1320
     */
1321
    public function removeFromList($exerciseId, $courseId = 0)
1322
    {
1323
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1324
        $id = (int) $this->id;
1325
        $exerciseId = (int) $exerciseId;
1326
1327
        // searches the position of the exercise ID in the list
1328
        $pos = array_search($exerciseId, $this->exerciseList);
1329
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
1330
1331
        // exercise not found
1332
        if (false === $pos) {
1333
            return false;
1334
        } else {
1335
            // deletes the position in the array containing the wanted exercise ID
1336
            unset($this->exerciseList[$pos]);
1337
            //update order of other elements
1338
            $sql = "SELECT question_order
1339
                    FROM $table
1340
                    WHERE
1341
                        c_id = $courseId AND
1342
                        question_id = $id AND
1343
                        exercice_id = $exerciseId";
1344
            $res = Database::query($sql);
1345
            if (Database::num_rows($res) > 0) {
1346
                $row = Database::fetch_array($res);
1347
                if (!empty($row['question_order'])) {
1348
                    $sql = "UPDATE $table
1349
                            SET question_order = question_order-1
1350
                            WHERE
1351
                                c_id = $courseId AND
1352
                                exercice_id = $exerciseId AND
1353
                                question_order > ".$row['question_order'];
1354
                    Database::query($sql);
1355
                }
1356
            }
1357
1358
            $sql = "DELETE FROM $table
1359
                    WHERE
1360
                        c_id = $courseId AND
1361
                        question_id = $id AND
1362
                        exercice_id = $exerciseId";
1363
            Database::query($sql);
1364
1365
            return true;
1366
        }
1367
    }
1368
1369
    /**
1370
     * Deletes a question from the database
1371
     * the parameter tells if the question is removed from all exercises (value = 0),
1372
     * or just from one exercise (value = exercise ID).
1373
     *
1374
     * @author Olivier Brouckaert
1375
     *
1376
     * @param int $deleteFromEx - exercise ID if the question is only removed from one exercise
1377
     *
1378
     * @return bool
1379
     */
1380
    public function delete($deleteFromEx = 0)
1381
    {
1382
        if (empty($this->course)) {
1383
            return false;
1384
        }
1385
1386
        $courseId = $this->course['real_id'];
1387
1388
        if (empty($courseId)) {
1389
            return false;
1390
        }
1391
1392
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1393
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1394
        $TBL_REPONSES = Database::get_course_table(TABLE_QUIZ_ANSWER);
1395
        $TBL_QUIZ_QUESTION_REL_CATEGORY = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
1396
1397
        $id = (int) $this->id;
1398
1399
        // if the question must be removed from all exercises
1400
        if (!$deleteFromEx) {
1401
            //update the question_order of each question to avoid inconsistencies
1402
            $sql = "SELECT exercice_id, question_order
1403
                    FROM $TBL_EXERCISE_QUESTION
1404
                    WHERE c_id = $courseId AND question_id = ".$id;
1405
1406
            $res = Database::query($sql);
1407
            if (Database::num_rows($res) > 0) {
1408
                while ($row = Database::fetch_array($res)) {
1409
                    if (!empty($row['question_order'])) {
1410
                        $sql = "UPDATE $TBL_EXERCISE_QUESTION
1411
                                SET question_order = question_order-1
1412
                                WHERE
1413
                                    c_id = $courseId AND
1414
                                    exercice_id = ".intval($row['exercice_id'])." AND
1415
                                    question_order > ".$row['question_order'];
1416
                        Database::query($sql);
1417
                    }
1418
                }
1419
            }
1420
1421
            $sql = "DELETE FROM $TBL_EXERCISE_QUESTION
1422
                    WHERE c_id = $courseId AND question_id = ".$id;
1423
            Database::query($sql);
1424
1425
            $sql = "DELETE FROM $TBL_QUESTIONS
1426
                    WHERE c_id = $courseId AND id = ".$id;
1427
            Database::query($sql);
1428
1429
            $sql = "DELETE FROM $TBL_REPONSES
1430
                    WHERE c_id = $courseId AND question_id = ".$id;
1431
            Database::query($sql);
1432
1433
            // remove the category of this question in the question_rel_category table
1434
            $sql = "DELETE FROM $TBL_QUIZ_QUESTION_REL_CATEGORY
1435
                    WHERE
1436
                        c_id = $courseId AND
1437
                        question_id = ".$id;
1438
            Database::query($sql);
1439
1440
            api_item_property_update(
1441
                $this->course,
1442
                TOOL_QUIZ,
1443
                $id,
1444
                'QuizQuestionDeleted',
1445
                api_get_user_id()
1446
            );
1447
            Event::addEvent(
1448
                LOG_QUESTION_DELETED,
1449
                LOG_QUESTION_ID,
1450
                $this->iid
1451
            );
1452
            $this->removePicture();
1453
        } else {
1454
            // just removes the exercise from the list
1455
            $this->removeFromList($deleteFromEx, $courseId);
1456
            if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
1457
                // disassociate question with this exercise
1458
                $this->search_engine_edit($deleteFromEx, false, true);
1459
            }
1460
1461
            api_item_property_update(
1462
                $this->course,
1463
                TOOL_QUIZ,
1464
                $id,
1465
                'QuizQuestionDeleted',
1466
                api_get_user_id()
1467
            );
1468
            Event::addEvent(
1469
                LOG_QUESTION_REMOVED_FROM_QUIZ,
1470
                LOG_QUESTION_ID,
1471
                $this->iid
1472
            );
1473
        }
1474
1475
        return true;
1476
    }
1477
1478
    /**
1479
     * Duplicates the question.
1480
     *
1481
     * @author Olivier Brouckaert
1482
     *
1483
     * @param array $courseInfo Course info of the destination course
1484
     *
1485
     * @return false|string ID of the new question
1486
     */
1487
    public function duplicate($courseInfo = [])
1488
    {
1489
        $courseInfo = empty($courseInfo) ? $this->course : $courseInfo;
1490
1491
        if (empty($courseInfo)) {
1492
            return false;
1493
        }
1494
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
1495
        $TBL_QUESTION_OPTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1496
1497
        $question = $this->question;
1498
        $description = $this->description;
1499
        $weighting = $this->weighting;
1500
        $position = $this->position;
1501
        $type = $this->type;
1502
        $level = (int) $this->level;
1503
        $extra = $this->extra;
1504
1505
        // Using the same method used in the course copy to transform URLs
1506
        if ($this->course['id'] != $courseInfo['id']) {
1507
            $description = DocumentManager::replaceUrlWithNewCourseCode(
1508
                $description,
1509
                $this->course['code'],
1510
                $courseInfo['id']
1511
            );
1512
            $question = DocumentManager::replaceUrlWithNewCourseCode(
1513
                $question,
1514
                $this->course['code'],
1515
                $courseInfo['id']
1516
            );
1517
        }
1518
1519
        $course_id = $courseInfo['real_id'];
1520
1521
        // Read the source options
1522
        $options = self::readQuestionOption($this->id, $this->course['real_id']);
1523
1524
        // Inserting in the new course db / or the same course db
1525
        $params = [
1526
            'c_id' => $course_id,
1527
            'question' => $question,
1528
            'description' => $description,
1529
            'ponderation' => $weighting,
1530
            'position' => $position,
1531
            'type' => $type,
1532
            'level' => $level,
1533
            'extra' => $extra,
1534
        ];
1535
        $newQuestionId = Database::insert($questionTable, $params);
1536
1537
        if ($newQuestionId) {
1538
            $sql = "UPDATE $questionTable
1539
                    SET id = iid
1540
                    WHERE iid = $newQuestionId";
1541
            Database::query($sql);
1542
1543
            if (!empty($options)) {
1544
                // Saving the quiz_options
1545
                foreach ($options as $item) {
1546
                    $item['question_id'] = $newQuestionId;
1547
                    $item['c_id'] = $course_id;
1548
                    unset($item['id']);
1549
                    unset($item['iid']);
1550
                    $id = Database::insert($TBL_QUESTION_OPTIONS, $item);
1551
                    if ($id) {
1552
                        $sql = "UPDATE $TBL_QUESTION_OPTIONS
1553
                                SET id = iid
1554
                                WHERE iid = $id";
1555
                        Database::query($sql);
1556
                    }
1557
                }
1558
            }
1559
1560
            // Duplicates the picture of the hotspot
1561
            $this->exportPicture($newQuestionId, $courseInfo);
1562
        }
1563
1564
        return $newQuestionId;
1565
    }
1566
1567
    /**
1568
     * @return string
1569
     */
1570
    public function get_question_type_name()
1571
    {
1572
        $key = self::$questionTypes[$this->type];
1573
1574
        return get_lang($key[1]);
1575
    }
1576
1577
    /**
1578
     * @param string $type
1579
     */
1580
    public static function get_question_type($type)
1581
    {
1582
        if ($type == ORAL_EXPRESSION && api_get_setting('enable_record_audio') !== 'true') {
1583
            return null;
1584
        }
1585
1586
        return self::$questionTypes[$type];
1587
    }
1588
1589
    /**
1590
     * @return array
1591
     */
1592
    public static function getQuestionTypeList()
1593
    {
1594
        if ('true' !== api_get_setting('enable_record_audio')) {
1595
            self::$questionTypes[ORAL_EXPRESSION] = null;
1596
            unset(self::$questionTypes[ORAL_EXPRESSION]);
1597
        }
1598
        if ('true' !== api_get_setting('enable_quiz_scenario')) {
1599
            self::$questionTypes[HOT_SPOT_DELINEATION] = null;
1600
            unset(self::$questionTypes[HOT_SPOT_DELINEATION]);
1601
        }
1602
1603
        return self::$questionTypes;
1604
    }
1605
1606
    /**
1607
     * Returns an instance of the class corresponding to the type.
1608
     *
1609
     * @param int $type the type of the question
1610
     *
1611
     * @return $this instance of a Question subclass (or of Questionc class by default)
1612
     */
1613
    public static function getInstance($type)
1614
    {
1615
        if (!is_null($type)) {
1616
            list($fileName, $className) = self::get_question_type($type);
1617
            if (!empty($fileName)) {
1618
                include_once $fileName;
1619
                if (class_exists($className)) {
1620
                    return new $className();
1621
                } else {
1622
                    echo 'Can\'t instanciate class '.$className.' of type '.$type;
1623
                }
1624
            }
1625
        }
1626
1627
        return null;
1628
    }
1629
1630
    /**
1631
     * Creates the form to create / edit a question
1632
     * A subclass can redefine this function to add fields...
1633
     *
1634
     * @param FormValidator $form
1635
     * @param Exercise      $exercise
1636
     */
1637
    public function createForm(&$form, $exercise)
1638
    {
1639
        echo '<style>
1640
                .media { display:none;}
1641
            </style>';
1642
1643
        $zoomOptions = api_get_configuration_value('quiz_image_zoom');
1644
        if (isset($zoomOptions['options'])) {
1645
            $finderFolder = api_get_path(WEB_PATH).'vendor/studio-42/elfinder/';
1646
            echo '<!-- elFinder CSS (REQUIRED) -->';
1647
            echo '<link rel="stylesheet" type="text/css" media="screen" href="'.$finderFolder.'css/elfinder.full.css">';
1648
            echo '<link rel="stylesheet" type="text/css" media="screen" href="'.$finderFolder.'css/theme.css">';
1649
1650
            echo '<!-- elFinder JS (REQUIRED) -->';
1651
            echo '<script type="text/javascript" src="'.$finderFolder.'js/elfinder.full.js"></script>';
1652
1653
            echo '<!-- elFinder translation (OPTIONAL) -->';
1654
            $language = 'en';
1655
            $platformLanguage = api_get_interface_language();
1656
            $iso = api_get_language_isocode($platformLanguage);
1657
            $filePart = "vendor/studio-42/elfinder/js/i18n/elfinder.$iso.js";
1658
            $file = api_get_path(SYS_PATH).$filePart;
1659
            $includeFile = '';
1660
            if (file_exists($file)) {
1661
                $includeFile = '<script type="text/javascript" src="'.api_get_path(WEB_PATH).$filePart.'"></script>';
1662
                $language = $iso;
1663
            }
1664
            echo $includeFile;
1665
1666
            echo '<script type="text/javascript" charset="utf-8">
1667
            $(document).ready(function () {
1668
                $(".create_img_link").click(function(e){
1669
                    e.preventDefault();
1670
                    e.stopPropagation();
1671
                    var imageZoom = $("input[name=\'imageZoom\']").val();
1672
                    var imageWidth = $("input[name=\'imageWidth\']").val();
1673
                    CKEDITOR.instances.questionDescription.insertHtml(\'<img id="zoom_picture" class="zoom_picture" src="\'+imageZoom+\'" data-zoom-image="\'+imageZoom+\'" width="\'+imageWidth+\'px" />\');
1674
                });
1675
1676
                $("input[name=\'imageZoom\']").on("click", function(){
1677
                    console.log("click en campo");
1678
                    var elf = $("#elfinder").elfinder({
1679
                        url : "'.api_get_path(WEB_LIBRARY_PATH).'elfinder/connectorAction.php?'.api_get_cidreq().'",
1680
                        getFileCallback: function(file) {
1681
                            var filePath = file; //file contains the relative url.
1682
                            console.log(filePath);
1683
                            var imgPath = "<img src = \'"+filePath+"\'/>";
1684
                            $("input[name=\'imageZoom\']").val(filePath.url);
1685
                            $("#elfinder").remove(); //close the window after image is selected
1686
                        },
1687
                        startPathHash: "l2_Lw", // Sets the course driver as default
1688
                        resizable: false,
1689
                        lang: "'.$language.'"
1690
                    }).elfinder("instance");
1691
                });
1692
            });
1693
            </script>';
1694
            echo '<div id="elfinder"></div>';
1695
        }
1696
1697
        // question name
1698
        if (api_get_configuration_value('save_titles_as_html')) {
1699
            $editorConfig = ['ToolbarSet' => 'TitleAsHtml'];
1700
            $form->addHtmlEditor(
1701
                'questionName',
1702
                get_lang('Question'),
1703
                false,
1704
                false,
1705
                $editorConfig,
1706
                true
1707
            );
1708
        } else {
1709
            $form->addElement('text', 'questionName', get_lang('Question'));
1710
        }
1711
1712
        $form->addRule('questionName', get_lang('GiveQuestion'), 'required');
1713
1714
        // default content
1715
        $isContent = isset($_REQUEST['isContent']) ? (int) $_REQUEST['isContent'] : null;
1716
1717
        // Question type
1718
        $answerType = isset($_REQUEST['answerType']) ? (int) $_REQUEST['answerType'] : null;
1719
        $form->addElement('hidden', 'answerType', $answerType);
1720
1721
        // html editor
1722
        $editorConfig = [
1723
            'ToolbarSet' => 'TestQuestionDescription',
1724
            'Height' => '150',
1725
        ];
1726
1727
        if (!api_is_allowed_to_edit(null, true)) {
1728
            $editorConfig['UserStatus'] = 'student';
1729
        }
1730
1731
        $form->addButtonAdvancedSettings('advanced_params');
1732
        $form->addHtml('<div id="advanced_params_options" style="display:none">');
1733
1734
        if (isset($zoomOptions['options'])) {
1735
            $form->addElement('text', 'imageZoom', get_lang('ImageURL'));
1736
            $form->addElement('text', 'imageWidth', get_lang('PixelWidth'));
1737
            $form->addButton('btn_create_img', get_lang('AddToEditor'), 'plus', 'info', 'small', 'create_img_link');
1738
        }
1739
1740
        $form->addHtmlEditor(
1741
            'questionDescription',
1742
            get_lang('QuestionDescription'),
1743
            false,
1744
            false,
1745
            $editorConfig
1746
        );
1747
1748
        if ($this->type != MEDIA_QUESTION) {
1749
            // Advanced parameters.
1750
            $form->addElement(
1751
                'select',
1752
                'questionLevel',
1753
                get_lang('Difficulty'),
1754
                self::get_default_levels()
1755
            );
1756
1757
            // Categories.
1758
            $form->addElement(
1759
                'select',
1760
                'questionCategory',
1761
                get_lang('Category'),
1762
                TestCategory::getCategoriesIdAndName()
1763
            );
1764
1765
            if (EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM == $exercise->getQuestionSelectionType() &&
1766
                api_get_configuration_value('allow_mandatory_question_in_category')
1767
            ) {
1768
                $form->addCheckBox(
1769
                    'mandatory',
1770
                    get_lang('IsMandatory')
1771
                );
1772
            }
1773
1774
            global $text;
1775
            switch ($this->type) {
1776
                case UNIQUE_ANSWER:
1777
                    $buttonGroup = [];
1778
                    $buttonGroup[] = $form->addButtonSave(
1779
                        $text,
1780
                        'submitQuestion',
1781
                        true
1782
                    );
1783
                    $buttonGroup[] = $form->addButton(
1784
                        'convertAnswer',
1785
                        get_lang('ConvertToMultipleAnswer'),
1786
                        'dot-circle-o',
1787
                        'default',
1788
                        null,
1789
                        null,
1790
                        null,
1791
                        true
1792
                    );
1793
                    $form->addGroup($buttonGroup);
1794
                    break;
1795
                case MULTIPLE_ANSWER:
1796
                    $buttonGroup = [];
1797
                    $buttonGroup[] = $form->addButtonSave(
1798
                        $text,
1799
                        'submitQuestion',
1800
                        true
1801
                    );
1802
                    $buttonGroup[] = $form->addButton(
1803
                        'convertAnswer',
1804
                        get_lang('ConvertToUniqueAnswer'),
1805
                        'check-square-o',
1806
                        'default',
1807
                        null,
1808
                        null,
1809
                        null,
1810
                        true
1811
                    );
1812
                    $form->addGroup($buttonGroup);
1813
                    break;
1814
            }
1815
            //Medias
1816
            //$course_medias = self::prepare_course_media_select(api_get_course_int_id());
1817
            //$form->addElement('select', 'parent_id', get_lang('AttachToMedia'), $course_medias);
1818
        }
1819
1820
        $form->addElement('html', '</div>');
1821
1822
        if (!isset($_GET['fromExercise'])) {
1823
            switch ($answerType) {
1824
                case 1:
1825
                    $this->question = get_lang('DefaultUniqueQuestion');
1826
                    break;
1827
                case 2:
1828
                    $this->question = get_lang('DefaultMultipleQuestion');
1829
                    break;
1830
                case 3:
1831
                    $this->question = get_lang('DefaultFillBlankQuestion');
1832
                    break;
1833
                case 4:
1834
                    $this->question = get_lang('DefaultMathingQuestion');
1835
                    break;
1836
                case 5:
1837
                    $this->question = get_lang('DefaultOpenQuestion');
1838
                    break;
1839
                case 9:
1840
                    $this->question = get_lang('DefaultMultipleQuestion');
1841
                    break;
1842
            }
1843
        }
1844
1845
        if (!is_null($exercise)) {
1846
            if ($exercise->questionFeedbackEnabled && $this->showFeedback($exercise)) {
1847
                $form->addTextarea('feedback', get_lang('FeedbackIfNotCorrect'));
1848
            }
1849
        }
1850
1851
        $extraField = new ExtraField('question');
1852
        $extraField->addElements($form, $this->iid);
1853
1854
        // default values
1855
        $defaults = [];
1856
        $defaults['questionName'] = $this->question;
1857
        $defaults['questionDescription'] = $this->description;
1858
        $defaults['questionLevel'] = $this->level;
1859
        $defaults['questionCategory'] = $this->category;
1860
        $defaults['feedback'] = $this->feedback;
1861
        $defaults['mandatory'] = $this->mandatory;
1862
1863
        // Came from he question pool
1864
        if (isset($_GET['fromExercise'])) {
1865
            $form->setDefaults($defaults);
1866
        }
1867
1868
        if (!isset($_GET['newQuestion']) || $isContent) {
1869
            $form->setDefaults($defaults);
1870
        }
1871
1872
        /*if (!empty($_REQUEST['myid'])) {
1873
            $form->setDefaults($defaults);
1874
        } else {
1875
            if ($isContent == 1) {
1876
                $form->setDefaults($defaults);
1877
            }
1878
        }*/
1879
    }
1880
1881
    /**
1882
     * function which process the creation of questions.
1883
     *
1884
     * @param FormValidator $form
1885
     * @param Exercise      $exercise
1886
     */
1887
    public function processCreation($form, $exercise)
1888
    {
1889
        $this->updateTitle($form->getSubmitValue('questionName'));
1890
        $this->updateDescription($form->getSubmitValue('questionDescription'));
1891
        $this->updateLevel($form->getSubmitValue('questionLevel'));
1892
        $this->updateCategory($form->getSubmitValue('questionCategory'));
1893
        $this->setMandatory($form->getSubmitValue('mandatory'));
1894
        $this->setFeedback($form->getSubmitValue('feedback'));
1895
1896
        //Save normal question if NOT media
1897
        if (MEDIA_QUESTION != $this->type) {
1898
            $this->save($exercise);
1899
            // modify the exercise
1900
            $exercise->addToList($this->id);
1901
            $exercise->update_question_positions();
1902
1903
            $params = $form->exportValues();
1904
            $params['item_id'] = $this->id;
1905
1906
            $extraFieldValues = new ExtraFieldValue('question');
1907
            $extraFieldValues->saveFieldValues($params);
1908
        }
1909
    }
1910
1911
    /**
1912
     * abstract function which creates the form to create / edit the answers of the question.
1913
     *
1914
     * @param FormValidator $form
1915
     */
1916
    abstract public function createAnswersForm($form);
1917
1918
    /**
1919
     * abstract function which process the creation of answers.
1920
     *
1921
     * @param FormValidator $form
1922
     * @param Exercise      $exercise
1923
     */
1924
    abstract public function processAnswersCreation($form, $exercise);
1925
1926
    /**
1927
     * Displays the menu of question types.
1928
     *
1929
     * @param Exercise $objExercise
1930
     */
1931
    public static function displayTypeMenu($objExercise)
1932
    {
1933
        if (empty($objExercise)) {
1934
            return '';
1935
        }
1936
1937
        $feedbackType = $objExercise->getFeedbackType();
1938
        $exerciseId = $objExercise->id;
1939
1940
        // 1. by default we show all the question types
1941
        $questionTypeList = self::getQuestionTypeList();
1942
1943
        if (!isset($feedbackType)) {
1944
            $feedbackType = 0;
1945
        }
1946
1947
        switch ($feedbackType) {
1948
            case EXERCISE_FEEDBACK_TYPE_DIRECT:
1949
                $questionTypeList = [
1950
                    UNIQUE_ANSWER => self::$questionTypes[UNIQUE_ANSWER],
1951
                    HOT_SPOT_DELINEATION => self::$questionTypes[HOT_SPOT_DELINEATION],
1952
                ];
1953
                break;
1954
            case EXERCISE_FEEDBACK_TYPE_POPUP:
1955
                $questionTypeList = [
1956
                    UNIQUE_ANSWER => self::$questionTypes[UNIQUE_ANSWER],
1957
                    MULTIPLE_ANSWER => self::$questionTypes[MULTIPLE_ANSWER],
1958
                    DRAGGABLE => self::$questionTypes[DRAGGABLE],
1959
                    HOT_SPOT_DELINEATION => self::$questionTypes[HOT_SPOT_DELINEATION],
1960
                    CALCULATED_ANSWER => self::$questionTypes[CALCULATED_ANSWER],
1961
                ];
1962
                break;
1963
            default:
1964
                unset($questionTypeList[HOT_SPOT_DELINEATION]);
1965
                break;
1966
        }
1967
1968
        echo '<div class="panel panel-default">';
1969
        echo '<div class="panel-body">';
1970
        echo '<ul class="question_menu">';
1971
        foreach ($questionTypeList as $i => $type) {
1972
            /** @var Question $type */
1973
            $type = new $type[1]();
1974
            $img = $type->getTypePicture();
1975
            $explanation = get_lang($type->getExplanation());
1976
            echo '<li>';
1977
            echo '<div class="icon-image">';
1978
            $icon = '<a href="admin.php?'.api_get_cidreq().'&newQuestion=yes&answerType='.$i.'">'.
1979
                Display::return_icon($img, $explanation, null, ICON_SIZE_BIG).'</a>';
1980
1981
            if ($objExercise->force_edit_exercise_in_lp === false) {
1982
                if ($objExercise->exercise_was_added_in_lp == true) {
1983
                    $img = pathinfo($img);
1984
                    $img = $img['filename'].'_na.'.$img['extension'];
1985
                    $icon = Display::return_icon($img, $explanation, null, ICON_SIZE_BIG);
1986
                }
1987
            }
1988
            echo $icon;
1989
            echo '</div>';
1990
            echo '</li>';
1991
        }
1992
1993
        echo '<li>';
1994
        echo '<div class="icon_image_content">';
1995
        if ($objExercise->exercise_was_added_in_lp == true) {
1996
            echo Display::return_icon(
1997
                'database_na.png',
1998
                get_lang('GetExistingQuestion'),
1999
                null,
2000
                ICON_SIZE_BIG
2001
            );
2002
        } else {
2003
            if (in_array($feedbackType, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
2004
                echo $url = "<a href=\"question_pool.php?".api_get_cidreq()."&type=1&fromExercise=$exerciseId\">";
2005
            } else {
2006
                echo $url = '<a href="question_pool.php?'.api_get_cidreq().'&fromExercise='.$exerciseId.'">';
2007
            }
2008
            echo Display::return_icon(
2009
                'database.png',
2010
                get_lang('GetExistingQuestion'),
2011
                null,
2012
                ICON_SIZE_BIG
2013
            );
2014
        }
2015
        echo '</a>';
2016
        echo '</div></li>';
2017
        echo '</ul>';
2018
        echo '</div>';
2019
        echo '</div>';
2020
    }
2021
2022
    /**
2023
     * @param int    $question_id
2024
     * @param string $name
2025
     * @param int    $course_id
2026
     * @param int    $position
2027
     *
2028
     * @return false|string
2029
     */
2030
    public static function saveQuestionOption($question_id, $name, $course_id, $position = 0)
2031
    {
2032
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
2033
        $params['question_id'] = (int) $question_id;
2034
        $params['name'] = $name;
2035
        $params['position'] = $position;
2036
        $params['c_id'] = $course_id;
2037
        $result = self::readQuestionOption($question_id, $course_id);
2038
        $last_id = Database::insert($table, $params);
2039
        if ($last_id) {
2040
            $sql = "UPDATE $table SET id = iid WHERE iid = $last_id";
2041
            Database::query($sql);
2042
        }
2043
2044
        return $last_id;
2045
    }
2046
2047
    /**
2048
     * @param int $question_id
2049
     * @param int $course_id
2050
     */
2051
    public static function deleteAllQuestionOptions($question_id, $course_id)
2052
    {
2053
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
2054
        Database::delete(
2055
            $table,
2056
            [
2057
                'c_id = ? AND question_id = ?' => [
2058
                    $course_id,
2059
                    $question_id,
2060
                ],
2061
            ]
2062
        );
2063
    }
2064
2065
    /**
2066
     * @param int   $id
2067
     * @param array $params
2068
     * @param int   $course_id
2069
     *
2070
     * @return bool|int
2071
     */
2072
    public static function updateQuestionOption($id, $params, $course_id)
2073
    {
2074
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
2075
2076
        return Database::update(
2077
            $table,
2078
            $params,
2079
            ['c_id = ? AND id = ?' => [$course_id, $id]]
2080
        );
2081
    }
2082
2083
    /**
2084
     * @param int $question_id
2085
     * @param int $course_id
2086
     *
2087
     * @return array
2088
     */
2089
    public static function readQuestionOption($question_id, $course_id)
2090
    {
2091
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
2092
2093
        return Database::select(
2094
            '*',
2095
            $table,
2096
            [
2097
                'where' => [
2098
                    'c_id = ? AND question_id = ?' => [
2099
                        $course_id,
2100
                        $question_id,
2101
                    ],
2102
                ],
2103
                'order' => 'id ASC',
2104
            ]
2105
        );
2106
    }
2107
2108
    /**
2109
     * Shows question title an description.
2110
     *
2111
     * @param int   $counter
2112
     * @param array $score
2113
     *
2114
     * @return string HTML string with the header of the question (before the answers table)
2115
     */
2116
    public function return_header(Exercise $exercise, $counter = null, $score = [])
2117
    {
2118
        $counterLabel = '';
2119
        if (!empty($counter)) {
2120
            $counterLabel = (int) $counter;
2121
        }
2122
2123
        $scoreLabel = get_lang('Wrong');
2124
        if (in_array($exercise->results_disabled, [
2125
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
2126
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
2127
        ])
2128
        ) {
2129
            $scoreLabel = get_lang('QuizWrongAnswerHereIsTheCorrectOne');
2130
        }
2131
2132
        $class = 'error';
2133
        if (isset($score['pass']) && $score['pass'] == true) {
2134
            $scoreLabel = get_lang('Correct');
2135
2136
            if (in_array($exercise->results_disabled, [
2137
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
2138
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
2139
            ])
2140
            ) {
2141
                $scoreLabel = get_lang('CorrectAnswer');
2142
            }
2143
            $class = 'success';
2144
        }
2145
2146
        switch ($this->type) {
2147
            case FREE_ANSWER:
2148
            case ORAL_EXPRESSION:
2149
            case ANNOTATION:
2150
                $score['revised'] = isset($score['revised']) ? $score['revised'] : false;
2151
                if ($score['revised'] == true) {
2152
                    $scoreLabel = get_lang('Revised');
2153
                    $class = '';
2154
                } else {
2155
                    $scoreLabel = get_lang('NotRevised');
2156
                    $class = 'warning';
2157
                    if (isset($score['weight'])) {
2158
                        $weight = float_format($score['weight'], 1);
2159
                        $score['result'] = ' ? / '.$weight;
2160
                    }
2161
                    $model = ExerciseLib::getCourseScoreModel();
2162
                    if (!empty($model)) {
2163
                        $score['result'] = ' ? ';
2164
                    }
2165
2166
                    $hide = api_get_configuration_value('hide_free_question_score');
2167
                    if (true === $hide) {
2168
                        $score['result'] = '-';
2169
                    }
2170
                }
2171
                break;
2172
            case UNIQUE_ANSWER:
2173
                if (in_array($exercise->results_disabled, [
2174
                    RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
2175
                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
2176
                ])
2177
                ) {
2178
                    if (isset($score['user_answered'])) {
2179
                        if ($score['user_answered'] === false) {
2180
                            $scoreLabel = get_lang('Unanswered');
2181
                            $class = 'info';
2182
                        }
2183
                    }
2184
                }
2185
                break;
2186
        }
2187
2188
        // display question category, if any
2189
        $header = '';
2190
        if ($exercise->display_category_name) {
2191
            $header = TestCategory::returnCategoryAndTitle($this->id);
2192
        }
2193
        $show_media = '';
2194
        if ($show_media) {
2195
            $header .= $this->show_media_content();
2196
        }
2197
2198
        $scoreCurrent = [
2199
            'used' => isset($score['score']) ? $score['score'] : '',
2200
            'missing' => isset($score['weight']) ? $score['weight'] : '',
2201
        ];
2202
        $header .= Display::page_subheader2($counterLabel.'. '.$this->question);
2203
2204
        $showRibbon = true;
2205
        // dont display score for certainty degree questions
2206
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $this->type) {
2207
            $showRibbon = false;
2208
            $ribbonResult = api_get_configuration_value('show_exercise_question_certainty_ribbon_result');
2209
            if (true === $ribbonResult) {
2210
                $showRibbon = true;
2211
            }
2212
        }
2213
2214
        if ($showRibbon && isset($score['result'])) {
2215
            if (in_array($exercise->results_disabled, [
2216
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
2217
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
2218
            ])
2219
            ) {
2220
                $score['result'] = null;
2221
            }
2222
            $header .= $exercise->getQuestionRibbon($class, $scoreLabel, $score['result'], $scoreCurrent);
2223
        }
2224
2225
        if ($this->type != READING_COMPREHENSION) {
2226
            // Do not show the description (the text to read) if the question is of type READING_COMPREHENSION
2227
            $header .= Display::div(
2228
                $this->description,
2229
                ['class' => 'question_description']
2230
            );
2231
        } else {
2232
            if ($score['pass'] == true) {
2233
                $message = Display::div(
2234
                    sprintf(
2235
                        get_lang('ReadingQuestionCongratsSpeedXReachedForYWords'),
2236
                        ReadingComprehension::$speeds[$this->level],
2237
                        $this->getWordsCount()
2238
                    )
2239
                );
2240
            } else {
2241
                $message = Display::div(
2242
                    sprintf(
2243
                        get_lang('ReadingQuestionCongratsSpeedXNotReachedForYWords'),
2244
                        ReadingComprehension::$speeds[$this->level],
2245
                        $this->getWordsCount()
2246
                    )
2247
                );
2248
            }
2249
            $header .= $message.'<br />';
2250
        }
2251
2252
        if ($exercise->hideComment && $this->type == HOT_SPOT) {
2253
            $header .= Display::return_message(get_lang('ResultsOnlyAvailableOnline'));
2254
2255
            return $header;
2256
        }
2257
2258
        if (isset($score['pass']) && $score['pass'] === false) {
2259
            if ($this->showFeedback($exercise)) {
2260
                $header .= $this->returnFormatFeedback();
2261
            }
2262
        }
2263
2264
        return $header;
2265
    }
2266
2267
    /**
2268
     * @deprecated
2269
     * Create a question from a set of parameters.
2270
     *
2271
     * @param   int     Quiz ID
2272
     * @param   string  Question name
2273
     * @param   int     Maximum result for the question
2274
     * @param   int     Type of question (see constants at beginning of question.class.php)
2275
     * @param   int     Question level/category
2276
     * @param string $quiz_id
2277
     */
2278
    public function create_question(
2279
        $quiz_id,
2280
        $question_name,
2281
        $question_description = '',
2282
        $max_score = 0,
2283
        $type = 1,
2284
        $level = 1
2285
    ) {
2286
        $course_id = api_get_course_int_id();
2287
        $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2288
        $tbl_quiz_rel_question = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
2289
2290
        $quiz_id = intval($quiz_id);
2291
        $max_score = (float) $max_score;
2292
        $type = intval($type);
2293
        $level = intval($level);
2294
2295
        // Get the max position
2296
        $sql = "SELECT max(position) as max_position
2297
                FROM $tbl_quiz_question q
2298
                INNER JOIN $tbl_quiz_rel_question r
2299
                ON
2300
                    q.id = r.question_id AND
2301
                    exercice_id = $quiz_id AND
2302
                    q.c_id = $course_id AND
2303
                    r.c_id = $course_id";
2304
        $rs_max = Database::query($sql);
2305
        $row_max = Database::fetch_object($rs_max);
2306
        $max_position = $row_max->max_position + 1;
2307
2308
        $params = [
2309
            'c_id' => $course_id,
2310
            'question' => $question_name,
2311
            'description' => $question_description,
2312
            'ponderation' => $max_score,
2313
            'position' => $max_position,
2314
            'type' => $type,
2315
            'level' => $level,
2316
        ];
2317
        $question_id = Database::insert($tbl_quiz_question, $params);
2318
2319
        if ($question_id) {
2320
            $sql = "UPDATE $tbl_quiz_question
2321
                    SET id = iid WHERE iid = $question_id";
2322
            Database::query($sql);
2323
2324
            // Get the max question_order
2325
            $sql = "SELECT max(question_order) as max_order
2326
                    FROM $tbl_quiz_rel_question
2327
                    WHERE c_id = $course_id AND exercice_id = $quiz_id ";
2328
            $rs_max_order = Database::query($sql);
2329
            $row_max_order = Database::fetch_object($rs_max_order);
2330
            $max_order = $row_max_order->max_order + 1;
2331
            // Attach questions to quiz
2332
            $sql = "INSERT INTO $tbl_quiz_rel_question (c_id, question_id, exercice_id, question_order)
2333
                    VALUES($course_id, $question_id, $quiz_id, $max_order)";
2334
            Database::query($sql);
2335
        }
2336
2337
        return $question_id;
2338
    }
2339
2340
    /**
2341
     * @return string
2342
     */
2343
    public function getTypePicture()
2344
    {
2345
        return $this->typePicture;
2346
    }
2347
2348
    /**
2349
     * @return string
2350
     */
2351
    public function getExplanation()
2352
    {
2353
        return $this->explanationLangVar;
2354
    }
2355
2356
    /**
2357
     * Get course medias.
2358
     *
2359
     * @param int $course_id
2360
     *
2361
     * @return array
2362
     */
2363
    public static function get_course_medias(
2364
        $course_id,
2365
        $start = 0,
2366
        $limit = 100,
2367
        $sidx = 'question',
2368
        $sord = 'ASC',
2369
        $where_condition = []
2370
    ) {
2371
        $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2372
        $default_where = [
2373
            'c_id = ? AND parent_id = 0 AND type = ?' => [
2374
                $course_id,
2375
                MEDIA_QUESTION,
2376
            ],
2377
        ];
2378
2379
        return Database::select(
2380
            '*',
2381
            $table_question,
2382
            [
2383
                'limit' => " $start, $limit",
2384
                'where' => $default_where,
2385
                'order' => "$sidx $sord",
2386
            ]
2387
        );
2388
    }
2389
2390
    /**
2391
     * Get count course medias.
2392
     *
2393
     * @param int $course_id course id
2394
     *
2395
     * @return int
2396
     */
2397
    public static function get_count_course_medias($course_id)
2398
    {
2399
        $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2400
        $result = Database::select(
2401
            'count(*) as count',
2402
            $table_question,
2403
            [
2404
                'where' => [
2405
                    'c_id = ? AND parent_id = 0 AND type = ?' => [
2406
                        $course_id,
2407
                        MEDIA_QUESTION,
2408
                    ],
2409
                ],
2410
            ],
2411
            'first'
2412
        );
2413
2414
        if ($result && isset($result['count'])) {
2415
            return $result['count'];
2416
        }
2417
2418
        return 0;
2419
    }
2420
2421
    /**
2422
     * @param int $course_id
2423
     *
2424
     * @return array
2425
     */
2426
    public static function prepare_course_media_select($course_id)
2427
    {
2428
        $medias = self::get_course_medias($course_id);
2429
        $media_list = [];
2430
        $media_list[0] = get_lang('NoMedia');
2431
2432
        if (!empty($medias)) {
2433
            foreach ($medias as $media) {
2434
                $media_list[$media['id']] = empty($media['question']) ? get_lang('Untitled') : $media['question'];
2435
            }
2436
        }
2437
2438
        return $media_list;
2439
    }
2440
2441
    /**
2442
     * @return array
2443
     */
2444
    public static function get_default_levels()
2445
    {
2446
        return [
2447
            1 => 1,
2448
            2 => 2,
2449
            3 => 3,
2450
            4 => 4,
2451
            5 => 5,
2452
        ];
2453
    }
2454
2455
    /**
2456
     * @return string
2457
     */
2458
    public function show_media_content()
2459
    {
2460
        $html = '';
2461
        if (0 != $this->parent_id) {
2462
            $parent_question = self::read($this->parent_id);
2463
            $html = $parent_question->show_media_content();
2464
        } else {
2465
            $html .= Display::page_subheader($this->selectTitle());
2466
            $html .= $this->selectDescription();
2467
        }
2468
2469
        return $html;
2470
    }
2471
2472
    /**
2473
     * Swap between unique and multiple type answers.
2474
     *
2475
     * @return UniqueAnswer|MultipleAnswer
2476
     */
2477
    public function swapSimpleAnswerTypes()
2478
    {
2479
        $oppositeAnswers = [
2480
            UNIQUE_ANSWER => MULTIPLE_ANSWER,
2481
            MULTIPLE_ANSWER => UNIQUE_ANSWER,
2482
        ];
2483
        $this->type = $oppositeAnswers[$this->type];
2484
        Database::update(
2485
            Database::get_course_table(TABLE_QUIZ_QUESTION),
2486
            ['type' => $this->type],
2487
            ['c_id = ? AND id = ?' => [$this->course['real_id'], $this->id]]
2488
        );
2489
        $answerClasses = [
2490
            UNIQUE_ANSWER => 'UniqueAnswer',
2491
            MULTIPLE_ANSWER => 'MultipleAnswer',
2492
        ];
2493
        $swappedAnswer = new $answerClasses[$this->type]();
2494
        foreach ($this as $key => $value) {
2495
            $swappedAnswer->$key = $value;
2496
        }
2497
2498
        return $swappedAnswer;
2499
    }
2500
2501
    /**
2502
     * @param array $score
2503
     *
2504
     * @return bool
2505
     */
2506
    public function isQuestionWaitingReview($score)
2507
    {
2508
        $isReview = false;
2509
        if (!empty($score)) {
2510
            if (!empty($score['comments']) || $score['score'] > 0) {
2511
                $isReview = true;
2512
            }
2513
        }
2514
2515
        return $isReview;
2516
    }
2517
2518
    /**
2519
     * @param string $value
2520
     */
2521
    public function setFeedback($value)
2522
    {
2523
        $this->feedback = $value;
2524
    }
2525
2526
    /**
2527
     * @param Exercise $exercise
2528
     *
2529
     * @return bool
2530
     */
2531
    public function showFeedback($exercise)
2532
    {
2533
        if (false === $exercise->hideComment) {
2534
            return false;
2535
        }
2536
2537
        return
2538
            in_array($this->type, $this->questionTypeWithFeedback) &&
2539
            $exercise->getFeedbackType() != EXERCISE_FEEDBACK_TYPE_EXAM;
2540
    }
2541
2542
    /**
2543
     * @return string
2544
     */
2545
    public function returnFormatFeedback()
2546
    {
2547
        return '<br />'.Display::return_message($this->feedback, 'normal', false);
2548
    }
2549
2550
    /**
2551
     * Check if this question exists in another exercise.
2552
     *
2553
     * @throws \Doctrine\ORM\Query\QueryException
2554
     *
2555
     * @return bool
2556
     */
2557
    public function existsInAnotherExercise()
2558
    {
2559
        $count = $this->getCountExercise();
2560
2561
        return $count > 1;
2562
    }
2563
2564
    /**
2565
     * @throws \Doctrine\ORM\Query\QueryException
2566
     *
2567
     * @return int
2568
     */
2569
    public function getCountExercise()
2570
    {
2571
        $em = Database::getManager();
2572
2573
        $count = $em
2574
            ->createQuery('
2575
                SELECT COUNT(qq.iid) FROM ChamiloCourseBundle:CQuizRelQuestion qq
2576
                WHERE qq.questionId = :id
2577
            ')
2578
            ->setParameters(['id' => (int) $this->id])
2579
            ->getSingleScalarResult();
2580
2581
        return (int) $count;
2582
    }
2583
2584
    /**
2585
     * Check if this question exists in another exercise.
2586
     *
2587
     * @throws \Doctrine\ORM\Query\QueryException
2588
     *
2589
     * @return mixed
2590
     */
2591
    public function getExerciseListWhereQuestionExists()
2592
    {
2593
        $em = Database::getManager();
2594
2595
        return $em
2596
            ->createQuery('
2597
                SELECT e
2598
                FROM ChamiloCourseBundle:CQuizRelQuestion qq
2599
                JOIN ChamiloCourseBundle:CQuiz e
2600
                WHERE e.iid = qq.exerciceId AND qq.questionId = :id
2601
            ')
2602
            ->setParameters(['id' => (int) $this->id])
2603
            ->getResult();
2604
    }
2605
2606
    /**
2607
     * @return int
2608
     */
2609
    public function countAnswers()
2610
    {
2611
        $result = Database::select(
2612
            'COUNT(1) AS c',
2613
            Database::get_course_table(TABLE_QUIZ_ANSWER),
2614
            ['where' => ['question_id = ?' => [$this->id]]],
2615
            'first'
2616
        );
2617
2618
        return (int) $result['c'];
2619
    }
2620
2621
    /**
2622
     * Resizes a picture || Warning!: can only be called after uploadPicture,
2623
     * or if picture is already available in object.
2624
     *
2625
     * @param string $Dimension - Resizing happens proportional according to given dimension: height|width|any
2626
     * @param int    $Max       - Maximum size
2627
     *
2628
     * @return bool|null - true if success, false if failed
2629
     *
2630
     * @author Toon Keppens
2631
     */
2632
    private function resizePicture($Dimension, $Max)
2633
    {
2634
        // if the question has an ID
2635
        if (!$this->id) {
2636
            return false;
2637
        }
2638
2639
        $picturePath = $this->getHotSpotFolderInCourse().'/'.$this->getPictureFilename();
2640
2641
        // Get dimensions from current image.
2642
        $my_image = new Image($picturePath);
2643
2644
        $current_image_size = $my_image->get_image_size();
2645
        $current_width = $current_image_size['width'];
2646
        $current_height = $current_image_size['height'];
2647
2648
        if ($current_width < $Max && $current_height < $Max) {
2649
            return true;
2650
        } elseif ($current_height == '') {
2651
            return false;
2652
        }
2653
2654
        // Resize according to height.
2655
        if ($Dimension == "height") {
2656
            $resize_scale = $current_height / $Max;
2657
            $new_width = ceil($current_width / $resize_scale);
2658
        }
2659
2660
        // Resize according to width
2661
        if ($Dimension == "width") {
2662
            $new_width = $Max;
2663
        }
2664
2665
        // Resize according to height or width, both should not be larger than $Max after resizing.
2666
        if ($Dimension == "any") {
2667
            if ($current_height > $current_width || $current_height == $current_width) {
2668
                $resize_scale = $current_height / $Max;
2669
                $new_width = ceil($current_width / $resize_scale);
2670
            }
2671
            if ($current_height < $current_width) {
2672
                $new_width = $Max;
2673
            }
2674
        }
2675
2676
        $my_image->resize($new_width);
2677
        $result = $my_image->send_image($picturePath);
2678
2679
        if ($result) {
2680
            return true;
2681
        }
2682
2683
        return false;
2684
    }
2685
}
2686