Completed
Push — master ( e0a519...eabd41 )
by Julito
119:58 queued 99:23
created

main/exercise/question.class.php (3 issues)

1
<?php
2
/* For licensing terms, see /license.txt */
3
4
use Chamilo\CourseBundle\Entity\CQuizAnswer;
5
6
/**
7
 * Class Question.
8
 *
9
 * This class allows to instantiate an object of type Question
10
 *
11
 * @author Olivier Brouckaert, original author
12
 * @author Patrick Cool, LaTeX support
13
 * @author Julio Montoya <[email protected]> lot of bug fixes
14
 * @author [email protected] - add question categories
15
 *
16
 * @package chamilo.exercise
17
 */
18
abstract class Question
19
{
20
    public $id;
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 $isContent;
33
    public $course;
34
    public $feedback;
35
    public static $typePicture = 'new_question.png';
36
    public static $explanationLangVar = '';
37
    public $question_table_class = 'table table-striped';
38
    public $questionTypeWithFeedback;
39
    public $extra;
40
    public $export = false;
41
    public static $questionTypes = [
42
        UNIQUE_ANSWER => ['unique_answer.class.php', 'UniqueAnswer'],
43
        MULTIPLE_ANSWER => ['multiple_answer.class.php', 'MultipleAnswer'],
44
        FILL_IN_BLANKS => ['fill_blanks.class.php', 'FillBlanks'],
45
        MATCHING => ['matching.class.php', 'Matching'],
46
        FREE_ANSWER => ['freeanswer.class.php', 'FreeAnswer'],
47
        ORAL_EXPRESSION => ['oral_expression.class.php', 'OralExpression'],
48
        HOT_SPOT => ['hotspot.class.php', 'HotSpot'],
49
        HOT_SPOT_DELINEATION => ['hotspot.class.php', 'HotspotDelineation'],
50
        MULTIPLE_ANSWER_COMBINATION => ['multiple_answer_combination.class.php', 'MultipleAnswerCombination'],
51
        UNIQUE_ANSWER_NO_OPTION => ['unique_answer_no_option.class.php', 'UniqueAnswerNoOption'],
52
        MULTIPLE_ANSWER_TRUE_FALSE => ['multiple_answer_true_false.class.php', 'MultipleAnswerTrueFalse'],
53
        MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE => [
54
            'multiple_answer_combination_true_false.class.php',
55
            'MultipleAnswerCombinationTrueFalse',
56
        ],
57
        GLOBAL_MULTIPLE_ANSWER => ['global_multiple_answer.class.php', 'GlobalMultipleAnswer'],
58
        CALCULATED_ANSWER => ['calculated_answer.class.php', 'CalculatedAnswer'],
59
        UNIQUE_ANSWER_IMAGE => ['UniqueAnswerImage.php', 'UniqueAnswerImage'],
60
        DRAGGABLE => ['Draggable.php', 'Draggable'],
61
        MATCHING_DRAGGABLE => ['MatchingDraggable.php', 'MatchingDraggable'],
62
        //MEDIA_QUESTION => array('media_question.class.php' , 'MediaQuestion')
63
        ANNOTATION => ['Annotation.php', 'Annotation'],
64
        READING_COMPREHENSION => ['ReadingComprehension.php', 'ReadingComprehension'],
65
    ];
66
67
    /**
68
     * constructor of the class.
69
     *
70
     * @author Olivier Brouckaert
71
     */
72
    public function __construct()
73
    {
74
        $this->id = 0;
75
        $this->question = '';
76
        $this->description = '';
77
        $this->weighting = 0;
78
        $this->position = 1;
79
        $this->picture = '';
80
        $this->level = 1;
81
        $this->category = 0;
82
        // This variable is used when loading an exercise like an scenario with
83
        // an special hotspot: final_overlap, final_missing, final_excess
84
        $this->extra = '';
85
        $this->exerciseList = [];
86
        $this->course = api_get_course_info();
87
        $this->category_list = [];
88
        $this->parent_id = 0;
89
        // See BT#12611
90
        $this->questionTypeWithFeedback = [
91
            MATCHING,
92
            MATCHING_DRAGGABLE,
93
            DRAGGABLE,
94
            FILL_IN_BLANKS,
95
            FREE_ANSWER,
96
            ORAL_EXPRESSION,
97
            CALCULATED_ANSWER,
98
            ANNOTATION,
99
        ];
100
    }
101
102
    /**
103
     * @return int|null
104
     */
105
    public function getIsContent()
106
    {
107
        $isContent = null;
108
        if (isset($_REQUEST['isContent'])) {
109
            $isContent = intval($_REQUEST['isContent']);
110
        }
111
112
        return $this->isContent = $isContent;
113
    }
114
115
    /**
116
     * Reads question information from the data base.
117
     *
118
     * @param int $id        - question ID
119
     * @param int $course_id
120
     *
121
     * @return Question
122
     *
123
     * @author Olivier Brouckaert
124
     */
125
    public static function read($id, $course_id = null)
126
    {
127
        $id = intval($id);
128
        if (!empty($course_id)) {
129
            $course_info = api_get_course_info_by_id($course_id);
130
        } else {
131
            $course_info = api_get_course_info();
132
        }
133
134
        $course_id = $course_info['real_id'];
135
136
        if (empty($course_id) || $course_id == -1) {
137
            return false;
138
        }
139
140
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
141
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
142
143
        $sql = "SELECT *
144
                FROM $TBL_QUESTIONS
145
                WHERE c_id = $course_id AND id = $id ";
146
        $result = Database::query($sql);
147
148
        // if the question has been found
149
        if ($object = Database::fetch_object($result)) {
150
            $objQuestion = self::getInstance($object->type);
151
            if (!empty($objQuestion)) {
152
                $objQuestion->id = (int) $id;
153
                $objQuestion->question = $object->question;
154
                $objQuestion->description = $object->description;
155
                $objQuestion->weighting = $object->ponderation;
156
                $objQuestion->position = $object->position;
157
                $objQuestion->type = (int) $object->type;
158
                $objQuestion->picture = $object->picture;
159
                $objQuestion->level = (int) $object->level;
160
                $objQuestion->extra = $object->extra;
161
                $objQuestion->course = $course_info;
162
                $objQuestion->feedback = isset($object->feedback) ? $object->feedback : '';
163
                $objQuestion->category = TestCategory::getCategoryForQuestion($id, $course_id);
164
165
                $tblQuiz = Database::get_course_table(TABLE_QUIZ_TEST);
166
                $sql = "SELECT DISTINCT q.exercice_id
167
                        FROM $TBL_EXERCISE_QUESTION q
168
                        INNER JOIN $tblQuiz e
169
                        ON e.c_id = q.c_id AND e.id = q.exercice_id
170
                        WHERE
171
                            q.c_id = $course_id AND
172
                            q.question_id = $id AND
173
                            e.active >= 0";
174
175
                $result = Database::query($sql);
176
177
                // fills the array with the exercises which this question is in
178
                if ($result) {
0 ignored issues
show
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
179
                    while ($obj = Database::fetch_object($result)) {
180
                        $objQuestion->exerciseList[] = $obj->exercice_id;
181
                    }
182
                }
183
184
                return $objQuestion;
185
            }
186
        }
187
188
        // question not found
189
        return false;
190
    }
191
192
    /**
193
     * returns the question ID.
194
     *
195
     * @author Olivier Brouckaert
196
     *
197
     * @return int - question ID
198
     */
199
    public function selectId()
200
    {
201
        return $this->id;
202
    }
203
204
    /**
205
     * returns the question title.
206
     *
207
     * @author Olivier Brouckaert
208
     *
209
     * @return string - question title
210
     */
211
    public function selectTitle()
212
    {
213
        if (!api_get_configuration_value('save_titles_as_html')) {
214
            return $this->question;
215
        }
216
217
        return Display::div($this->question, ['style' => 'display: inline-block;']);
218
    }
219
220
    /**
221
     * @param int $itemNumber
222
     *
223
     * @return string
224
     */
225
    public function getTitleToDisplay($itemNumber)
226
    {
227
        $showQuestionTitleHtml = api_get_configuration_value('save_titles_as_html');
228
229
        $title = $showQuestionTitleHtml ? '' : '<strong>';
230
        $title .= $itemNumber.'. '.$this->selectTitle();
231
        $title .= $showQuestionTitleHtml ? '' : '</strong>';
232
233
        return Display::div(
234
            $title,
235
            ['class' => 'question_title']
236
        );
237
    }
238
239
    /**
240
     * returns the question description.
241
     *
242
     * @author Olivier Brouckaert
243
     *
244
     * @return string - question description
245
     */
246
    public function selectDescription()
247
    {
248
        return $this->description;
249
    }
250
251
    /**
252
     * returns the question weighting.
253
     *
254
     * @author Olivier Brouckaert
255
     *
256
     * @return int - question weighting
257
     */
258
    public function selectWeighting()
259
    {
260
        return $this->weighting;
261
    }
262
263
    /**
264
     * returns the question position.
265
     *
266
     * @author Olivier Brouckaert
267
     *
268
     * @return int - question position
269
     */
270
    public function selectPosition()
271
    {
272
        return $this->position;
273
    }
274
275
    /**
276
     * returns the answer type.
277
     *
278
     * @author Olivier Brouckaert
279
     *
280
     * @return int - answer type
281
     */
282
    public function selectType()
283
    {
284
        return $this->type;
285
    }
286
287
    /**
288
     * returns the level of the question.
289
     *
290
     * @author Nicolas Raynaud
291
     *
292
     * @return int - level of the question, 0 by default
293
     */
294
    public function getLevel()
295
    {
296
        return $this->level;
297
    }
298
299
    /**
300
     * returns the picture name.
301
     *
302
     * @author Olivier Brouckaert
303
     *
304
     * @return string - picture name
305
     */
306
    public function selectPicture()
307
    {
308
        return $this->picture;
309
    }
310
311
    /**
312
     * @return string
313
     */
314
    public function selectPicturePath()
315
    {
316
        if (!empty($this->picture)) {
317
            return api_get_path(WEB_COURSE_PATH).$this->course['directory'].'/document/images/'.$this->getPictureFilename();
318
        }
319
320
        return '';
321
    }
322
323
    /**
324
     * @return int|string
325
     */
326
    public function getPictureId()
327
    {
328
        // for backward compatibility
329
        // when in field picture we had the filename not the document id
330
        if (preg_match("/quiz-.*/", $this->picture)) {
331
            return DocumentManager::get_document_id(
332
                $this->course,
333
                $this->selectPicturePath(),
334
                api_get_session_id()
335
            );
336
        }
337
338
        return $this->picture;
339
    }
340
341
    /**
342
     * @param int $courseId
343
     * @param int $sessionId
344
     *
345
     * @return string
346
     */
347
    public function getPictureFilename($courseId = 0, $sessionId = 0)
348
    {
349
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
350
        $sessionId = empty($sessionId) ? api_get_session_id() : (int) $sessionId;
351
352
        if (empty($courseId)) {
353
            return '';
354
        }
355
        // for backward compatibility
356
        // when in field picture we had the filename not the document id
357
        if (preg_match("/quiz-.*/", $this->picture)) {
358
            return $this->picture;
359
        }
360
361
        $pictureId = $this->getPictureId();
362
        $courseInfo = $this->course;
363
        $documentInfo = DocumentManager::get_document_data_by_id(
364
            $pictureId,
365
            $courseInfo['code'],
366
            false,
367
            $sessionId
368
        );
369
        $documentFilename = '';
370
        if ($documentInfo) {
371
            // document in document/images folder
372
            $documentFilename = pathinfo(
373
                $documentInfo['path'],
374
                PATHINFO_BASENAME
375
            );
376
        }
377
378
        return $documentFilename;
379
    }
380
381
    /**
382
     * returns the array with the exercise ID list.
383
     *
384
     * @author Olivier Brouckaert
385
     *
386
     * @return array - list of exercise ID which the question is in
387
     */
388
    public function selectExerciseList()
389
    {
390
        return $this->exerciseList;
391
    }
392
393
    /**
394
     * returns the number of exercises which this question is in.
395
     *
396
     * @author Olivier Brouckaert
397
     *
398
     * @return int - number of exercises
399
     */
400
    public function selectNbrExercises()
401
    {
402
        return sizeof($this->exerciseList);
403
    }
404
405
    /**
406
     * changes the question title.
407
     *
408
     * @param string $title - question title
409
     *
410
     * @author Olivier Brouckaert
411
     */
412
    public function updateTitle($title)
413
    {
414
        $this->question = $title;
415
    }
416
417
    /**
418
     * @param int $id
419
     */
420
    public function updateParentId($id)
421
    {
422
        $this->parent_id = intval($id);
423
    }
424
425
    /**
426
     * changes the question description.
427
     *
428
     * @param string $description - question description
429
     *
430
     * @author Olivier Brouckaert
431
     */
432
    public function updateDescription($description)
433
    {
434
        $this->description = $description;
435
    }
436
437
    /**
438
     * changes the question weighting.
439
     *
440
     * @param int $weighting - question weighting
441
     *
442
     * @author Olivier Brouckaert
443
     */
444
    public function updateWeighting($weighting)
445
    {
446
        $this->weighting = $weighting;
447
    }
448
449
    /**
450
     * @param array $category
451
     *
452
     * @author Hubert Borderiou 12-10-2011
453
     */
454
    public function updateCategory($category)
455
    {
456
        $this->category = $category;
457
    }
458
459
    /**
460
     * @param int $value
461
     *
462
     * @author Hubert Borderiou 12-10-2011
463
     */
464
    public function updateScoreAlwaysPositive($value)
465
    {
466
        $this->scoreAlwaysPositive = $value;
467
    }
468
469
    /**
470
     * @param int $value
471
     *
472
     * @author Hubert Borderiou 12-10-2011
473
     */
474
    public function updateUncheckedMayScore($value)
475
    {
476
        $this->uncheckedMayScore = $value;
477
    }
478
479
    /**
480
     * Save category of a question.
481
     *
482
     * A question can have n categories if category is empty,
483
     * then question has no category then delete the category entry
484
     *
485
     * @param array $category_list
486
     *
487
     * @author Julio Montoya - Adding multiple cat support
488
     */
489
    public function saveCategories($category_list)
490
    {
491
        if (!empty($category_list)) {
492
            $this->deleteCategory();
493
            $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
494
495
            // update or add category for a question
496
            foreach ($category_list as $category_id) {
497
                $category_id = intval($category_id);
498
                $question_id = intval($this->id);
499
                $sql = "SELECT count(*) AS nb
500
                        FROM $table
501
                        WHERE
502
                            category_id = $category_id
503
                            AND question_id = $question_id
504
                            AND c_id=".api_get_course_int_id();
505
                $res = Database::query($sql);
506
                $row = Database::fetch_array($res);
507
                if ($row['nb'] > 0) {
508
                    // DO nothing
509
                } else {
510
                    $sql = "INSERT INTO $table (c_id, question_id, category_id)
511
                            VALUES (".api_get_course_int_id().", $question_id, $category_id)";
512
                    Database::query($sql);
513
                }
514
            }
515
        }
516
    }
517
518
    /**
519
     * in this version, a question can only have 1 category
520
     * if category is 0, then question has no category then delete the category entry.
521
     *
522
     * @param int $categoryId
523
     * @param int $courseId
524
     *
525
     * @return bool
526
     *
527
     * @author Hubert Borderiou 12-10-2011
528
     */
529
    public function saveCategory($categoryId, $courseId = 0)
530
    {
531
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
532
533
        if (empty($courseId)) {
534
            return false;
535
        }
536
537
        if ($categoryId <= 0) {
538
            $this->deleteCategory($courseId);
539
        } else {
540
            // update or add category for a question
541
            $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
542
            $categoryId = (int) $categoryId;
543
            $question_id = (int) $this->id;
544
            $sql = "SELECT count(*) AS nb FROM $table
545
                    WHERE
546
                        question_id = $question_id AND
547
                        c_id = ".$courseId;
548
            $res = Database::query($sql);
549
            $row = Database::fetch_array($res);
550
            if ($row['nb'] > 0) {
551
                $sql = "UPDATE $table
552
                        SET category_id = $categoryId
553
                        WHERE
554
                            question_id = $question_id AND
555
                            c_id = ".$courseId;
556
                Database::query($sql);
557
            } else {
558
                $sql = "INSERT INTO $table (c_id, question_id, category_id)
559
                        VALUES (".$courseId.", $question_id, $categoryId)";
560
                Database::query($sql);
561
            }
562
563
            return true;
564
        }
565
    }
566
567
    /**
568
     * @author hubert borderiou 12-10-2011
569
     *
570
     * @param int $courseId
571
     *                      delete any category entry for question id
572
     *                      delete the category for question
573
     *
574
     * @return bool
575
     */
576
    public function deleteCategory($courseId = 0)
577
    {
578
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
579
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
580
        $questionId = (int) $this->id;
581
        if (empty($courseId) || empty($questionId)) {
582
            return false;
583
        }
584
        $sql = "DELETE FROM $table
585
                WHERE
586
                    question_id = $questionId AND
587
                    c_id = ".$courseId;
588
        Database::query($sql);
589
590
        return true;
591
    }
592
593
    /**
594
     * changes the question position.
595
     *
596
     * @param int $position - question position
597
     *
598
     * @author Olivier Brouckaert
599
     */
600
    public function updatePosition($position)
601
    {
602
        $this->position = $position;
603
    }
604
605
    /**
606
     * changes the question level.
607
     *
608
     * @param int $level - question level
609
     *
610
     * @author Nicolas Raynaud
611
     */
612
    public function updateLevel($level)
613
    {
614
        $this->level = $level;
615
    }
616
617
    /**
618
     * changes the answer type. If the user changes the type from "unique answer" to "multiple answers"
619
     * (or conversely) answers are not deleted, otherwise yes.
620
     *
621
     * @param int $type - answer type
622
     *
623
     * @author Olivier Brouckaert
624
     */
625
    public function updateType($type)
626
    {
627
        $table = Database::get_course_table(TABLE_QUIZ_ANSWER);
628
        $course_id = $this->course['real_id'];
629
630
        if (empty($course_id)) {
631
            $course_id = api_get_course_int_id();
632
        }
633
        // if we really change the type
634
        if ($type != $this->type) {
635
            // if we don't change from "unique answer" to "multiple answers" (or conversely)
636
            if (!in_array($this->type, [UNIQUE_ANSWER, MULTIPLE_ANSWER]) ||
637
                !in_array($type, [UNIQUE_ANSWER, MULTIPLE_ANSWER])
638
            ) {
639
                // removes old answers
640
                $sql = "DELETE FROM $table
641
                        WHERE c_id = $course_id AND question_id = ".intval($this->id);
642
                Database::query($sql);
643
            }
644
645
            $this->type = $type;
646
        }
647
    }
648
649
    /**
650
     * Get default hot spot folder in documents.
651
     *
652
     * @return string
653
     */
654
    public function getHotSpotFolderInCourse()
655
    {
656
        if (empty($this->course) || empty($this->course['directory'])) {
657
            // Stop everything if course is not set.
658
            api_not_allowed();
659
        }
660
661
        $pictureAbsolutePath = api_get_path(SYS_COURSE_PATH).$this->course['directory'].'/document/images/';
662
        $picturePath = basename($pictureAbsolutePath);
663
664
        if (!is_dir($picturePath)) {
665
            create_unexisting_directory(
666
                $this->course,
667
                api_get_user_id(),
668
                0,
669
                0,
670
                0,
671
                dirname($pictureAbsolutePath),
672
                '/'.$picturePath,
673
                $picturePath
674
            );
675
        }
676
677
        return $pictureAbsolutePath;
678
    }
679
680
    /**
681
     * adds a picture to the question.
682
     *
683
     * @param string $picture - temporary path of the picture to upload
684
     *
685
     * @return bool - true if uploaded, otherwise false
686
     *
687
     * @author Olivier Brouckaert
688
     */
689
    public function uploadPicture($picture)
690
    {
691
        $picturePath = $this->getHotSpotFolderInCourse();
692
693
        // if the question has got an ID
694
        if ($this->id) {
695
            $pictureFilename = self::generatePictureName();
0 ignored issues
show
Bug Best Practice introduced by
The method Question::generatePictureName() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

695
            /** @scrutinizer ignore-call */ 
696
            $pictureFilename = self::generatePictureName();
Loading history...
696
            $img = new Image($picture);
697
            $img->send_image($picturePath.'/'.$pictureFilename, -1, 'jpg');
698
            $document_id = add_document(
699
                $this->course,
700
                '/images/'.$pictureFilename,
701
                'file',
702
                filesize($picturePath.'/'.$pictureFilename),
703
                $pictureFilename
704
            );
705
706
            if ($document_id) {
707
                $this->picture = $document_id;
708
709
                if (!file_exists($picturePath.'/'.$pictureFilename)) {
710
                    return false;
711
                }
712
713
                api_item_property_update(
714
                    $this->course,
715
                    TOOL_DOCUMENT,
716
                    $document_id,
717
                    'DocumentAdded',
718
                    api_get_user_id()
719
                );
720
721
                $this->resizePicture('width', 800);
722
723
                return true;
724
            }
725
        }
726
727
        return false;
728
    }
729
730
    /**
731
     * return the name for image use in hotspot question
732
     * to be unique, name is quiz-[utc unix timestamp].jpg.
733
     *
734
     * @param string $prefix
735
     * @param string $extension
736
     *
737
     * @return string
738
     */
739
    public function generatePictureName($prefix = 'quiz-', $extension = 'jpg')
740
    {
741
        // image name is quiz-xxx.jpg in folder images/
742
        $utcTime = time();
743
744
        return $prefix.$utcTime.'.'.$extension;
745
    }
746
747
    /**
748
     * deletes the picture.
749
     *
750
     * @author Olivier Brouckaert
751
     *
752
     * @return bool - true if removed, otherwise false
753
     */
754
    public function removePicture()
755
    {
756
        $picturePath = $this->getHotSpotFolderInCourse();
757
758
        // if the question has got an ID and if the picture exists
759
        if ($this->id) {
760
            $picture = $this->picture;
761
            $this->picture = '';
762
763
            return @unlink($picturePath.'/'.$picture) ? true : false;
764
        }
765
766
        return false;
767
    }
768
769
    /**
770
     * Exports a picture to another question.
771
     *
772
     * @author Olivier Brouckaert
773
     *
774
     * @param int   $questionId - ID of the target question
775
     * @param array $courseInfo
776
     *
777
     * @return bool - true if copied, otherwise false
778
     */
779
    public function exportPicture($questionId, $courseInfo)
780
    {
781
        if (empty($questionId) || empty($courseInfo)) {
782
            return false;
783
        }
784
785
        $course_id = $courseInfo['real_id'];
786
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
787
        $destination_path = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/images';
788
        $source_path = $this->getHotSpotFolderInCourse();
789
790
        // if the question has got an ID and if the picture exists
791
        if (!$this->id || empty($this->picture)) {
792
            return false;
793
        }
794
795
        $picture = $this->generatePictureName();
796
797
        if (file_exists($source_path.'/'.$this->picture)) {
798
            // for backward compatibility
799
            $result = @copy(
800
                $source_path.'/'.$this->picture,
801
                $destination_path.'/'.$picture
802
            );
803
        } else {
804
            $imageInfo = DocumentManager::get_document_data_by_id(
805
                $this->picture,
806
                $courseInfo['code']
807
            );
808
            if (file_exists($imageInfo['absolute_path'])) {
809
                $result = @copy(
810
                    $imageInfo['absolute_path'],
811
                    $destination_path.'/'.$picture
812
                );
813
            }
814
        }
815
816
        // If copy was correct then add to the database
817
        if (!$result) {
818
            return false;
819
        }
820
821
        $sql = "UPDATE $TBL_QUESTIONS SET
822
                picture = '".Database::escape_string($picture)."'
823
                WHERE c_id = $course_id AND id='".intval($questionId)."'";
824
        Database::query($sql);
825
826
        $documentId = add_document(
827
            $courseInfo,
828
            '/images/'.$picture,
829
            'file',
830
            filesize($destination_path.'/'.$picture),
831
            $picture
832
        );
833
834
        if (!$documentId) {
835
            return false;
836
        }
837
838
        return api_item_property_update(
839
            $courseInfo,
840
            TOOL_DOCUMENT,
841
            $documentId,
842
            'DocumentAdded',
843
            api_get_user_id()
844
        );
845
    }
846
847
    /**
848
     * Saves the picture coming from POST into a temporary file
849
     * Temporary pictures are used when we don't want to save a picture right after a form submission.
850
     * For example, if we first show a confirmation box.
851
     *
852
     * @author Olivier Brouckaert
853
     *
854
     * @param string $picture     - temporary path of the picture to move
855
     * @param string $pictureName - Name of the picture
856
     */
857
    public function setTmpPicture($picture, $pictureName)
858
    {
859
        $picturePath = $this->getHotSpotFolderInCourse();
860
        $pictureName = explode('.', $pictureName);
861
        $Extension = $pictureName[sizeof($pictureName) - 1];
862
863
        // saves the picture into a temporary file
864
        @move_uploaded_file($picture, $picturePath.'/tmp.'.$Extension);
865
    }
866
867
    /**
868
     * Set title.
869
     *
870
     * @param string $title
871
     */
872
    public function setTitle($title)
873
    {
874
        $this->question = $title;
875
    }
876
877
    /**
878
     * Sets extra info.
879
     *
880
     * @param string $extra
881
     */
882
    public function setExtra($extra)
883
    {
884
        $this->extra = $extra;
885
    }
886
887
    /**
888
     * updates the question in the data base
889
     * if an exercise ID is provided, we add that exercise ID into the exercise list.
890
     *
891
     * @author Olivier Brouckaert
892
     *
893
     * @param Exercise $exercise
894
     */
895
    public function save($exercise)
896
    {
897
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
898
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
899
        $em = Database::getManager();
900
        $exerciseId = $exercise->id;
901
902
        $id = $this->id;
903
        $question = $this->question;
904
        $description = $this->description;
905
        $weighting = $this->weighting;
906
        $position = $this->position;
907
        $type = $this->type;
908
        $picture = $this->picture;
909
        $level = $this->level;
910
        $extra = $this->extra;
911
        $c_id = $this->course['real_id'];
912
        $categoryId = $this->category;
913
914
        // question already exists
915
        if (!empty($id)) {
916
            $params = [
917
                'question' => $question,
918
                'description' => $description,
919
                'ponderation' => $weighting,
920
                'position' => $position,
921
                'type' => $type,
922
                'picture' => $picture,
923
                'extra' => $extra,
924
                'level' => $level,
925
            ];
926
            if ($exercise->questionFeedbackEnabled) {
927
                $params['feedback'] = $this->feedback;
928
            }
929
            Database::update(
930
                $TBL_QUESTIONS,
931
                $params,
932
                ['c_id = ? AND id = ?' => [$c_id, $id]]
933
            );
934
            $this->saveCategory($categoryId);
935
936
            if (!empty($exerciseId)) {
937
                api_item_property_update(
938
                    $this->course,
939
                    TOOL_QUIZ,
940
                    $id,
941
                    'QuizQuestionUpdated',
942
                    api_get_user_id()
943
                );
944
            }
945
            if (api_get_setting('search_enabled') == 'true') {
946
                if ($exerciseId != 0) {
947
                    $this->search_engine_edit($exerciseId);
948
                } else {
949
                    /**
950
                     * actually there is *not* an user interface for
951
                     * creating questions without a relation with an exercise.
952
                     */
953
                }
954
            }
955
        } else {
956
            // creates a new question
957
            $sql = "SELECT max(position)
958
                    FROM $TBL_QUESTIONS as question,
959
                    $TBL_EXERCISE_QUESTION as test_question
960
                    WHERE
961
                        question.id = test_question.question_id AND
962
                        test_question.exercice_id = ".intval($exerciseId)." AND
963
                        question.c_id = $c_id AND
964
                        test_question.c_id = $c_id ";
965
            $result = Database::query($sql);
966
            $current_position = Database::result($result, 0, 0);
967
            $this->updatePosition($current_position + 1);
968
            $position = $this->position;
969
970
            $params = [
971
                'c_id' => $c_id,
972
                'question' => $question,
973
                'description' => $description,
974
                'ponderation' => $weighting,
975
                'position' => $position,
976
                'type' => $type,
977
                'picture' => $picture,
978
                'extra' => $extra,
979
                'level' => $level,
980
            ];
981
982
            if ($exercise->questionFeedbackEnabled) {
983
                $params['feedback'] = $this->feedback;
984
            }
985
            $this->id = Database::insert($TBL_QUESTIONS, $params);
986
987
            if ($this->id) {
988
                $sql = "UPDATE $TBL_QUESTIONS SET id = iid WHERE iid = {$this->id}";
989
                Database::query($sql);
990
991
                api_item_property_update(
992
                    $this->course,
993
                    TOOL_QUIZ,
994
                    $this->id,
995
                    'QuizQuestionAdded',
996
                    api_get_user_id()
997
                );
998
999
                // If hotspot, create first answer
1000
                if ($type == HOT_SPOT || $type == HOT_SPOT_ORDER) {
1001
                    $quizAnswer = new CQuizAnswer();
1002
                    $quizAnswer
1003
                        ->setCId($c_id)
1004
                        ->setQuestionId($this->id)
1005
                        ->setAnswer('')
1006
                        ->setPonderation(10)
1007
                        ->setPosition(1)
1008
                        ->setHotspotCoordinates('0;0|0|0')
1009
                        ->setHotspotType('square');
1010
1011
                    $em->persist($quizAnswer);
1012
                    $em->flush();
1013
1014
                    $id = $quizAnswer->getIid();
1015
1016
                    if ($id) {
1017
                        $quizAnswer
1018
                            ->setId($id)
1019
                            ->setIdAuto($id);
1020
1021
                        $em->merge($quizAnswer);
1022
                        $em->flush();
1023
                    }
1024
                }
1025
1026
                if ($type == HOT_SPOT_DELINEATION) {
1027
                    $quizAnswer = new CQuizAnswer();
1028
                    $quizAnswer
1029
                        ->setCId($c_id)
1030
                        ->setQuestionId($this->id)
1031
                        ->setAnswer('')
1032
                        ->setPonderation(10)
1033
                        ->setPosition(1)
1034
                        ->setHotspotCoordinates('0;0|0|0')
1035
                        ->setHotspotType('delineation');
1036
1037
                    $em->persist($quizAnswer);
1038
                    $em->flush();
1039
1040
                    $id = $quizAnswer->getIid();
1041
1042
                    if ($id) {
1043
                        $quizAnswer
1044
                            ->setId($id)
1045
                            ->setIdAuto($id);
1046
1047
                        $em->merge($quizAnswer);
1048
                        $em->flush();
1049
                    }
1050
                }
1051
1052
                if (api_get_setting('search_enabled') == 'true') {
1053
                    if ($exerciseId != 0) {
1054
                        $this->search_engine_edit($exerciseId, true);
1055
                    } else {
1056
                        /**
1057
                         * actually there is *not* an user interface for
1058
                         * creating questions without a relation with an exercise.
1059
                         */
1060
                    }
1061
                }
1062
            }
1063
        }
1064
1065
        // if the question is created in an exercise
1066
        if ($exerciseId) {
1067
            // adds the exercise into the exercise list of this question
1068
            $this->addToList($exerciseId, true);
1069
        }
1070
    }
1071
1072
    public function search_engine_edit(
1073
        $exerciseId,
1074
        $addQs = false,
1075
        $rmQs = false
1076
    ) {
1077
        // update search engine and its values table if enabled
1078
        if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
1079
            $course_id = api_get_course_id();
1080
            // get search_did
1081
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
1082
            if ($addQs || $rmQs) {
1083
                //there's only one row per question on normal db and one document per question on search engine db
1084
                $sql = 'SELECT * FROM %s
1085
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_second_level=%s LIMIT 1';
1086
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
1087
            } else {
1088
                $sql = 'SELECT * FROM %s
1089
                    WHERE course_code=\'%s\' AND tool_id=\'%s\'
1090
                    AND ref_id_high_level=%s AND ref_id_second_level=%s LIMIT 1';
1091
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
1092
            }
1093
            $res = Database::query($sql);
1094
1095
            if (Database::num_rows($res) > 0 || $addQs) {
1096
                $di = new ChamiloIndexer();
1097
                if ($addQs) {
1098
                    $question_exercises = [(int) $exerciseId];
1099
                } else {
1100
                    $question_exercises = [];
1101
                }
1102
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
1103
                $di->connectDb(null, null, $lang);
1104
1105
                // retrieve others exercise ids
1106
                $se_ref = Database::fetch_array($res);
1107
                $se_doc = $di->get_document((int) $se_ref['search_did']);
1108
                if ($se_doc !== false) {
1109
                    if (($se_doc_data = $di->get_document_data($se_doc)) !== false) {
1110
                        $se_doc_data = unserialize($se_doc_data);
1111
                        if (isset($se_doc_data[SE_DATA]['type']) &&
1112
                            $se_doc_data[SE_DATA]['type'] == SE_DOCTYPE_EXERCISE_QUESTION
1113
                        ) {
1114
                            if (isset($se_doc_data[SE_DATA]['exercise_ids']) &&
1115
                                is_array($se_doc_data[SE_DATA]['exercise_ids'])
1116
                            ) {
1117
                                foreach ($se_doc_data[SE_DATA]['exercise_ids'] as $old_value) {
1118
                                    if (!in_array($old_value, $question_exercises)) {
1119
                                        $question_exercises[] = $old_value;
1120
                                    }
1121
                                }
1122
                            }
1123
                        }
1124
                    }
1125
                }
1126
                if ($rmQs) {
1127
                    while (($key = array_search($exerciseId, $question_exercises)) !== false) {
1128
                        unset($question_exercises[$key]);
1129
                    }
1130
                }
1131
1132
                // build the chunk to index
1133
                $ic_slide = new IndexableChunk();
1134
                $ic_slide->addValue("title", $this->question);
1135
                $ic_slide->addCourseId($course_id);
1136
                $ic_slide->addToolId(TOOL_QUIZ);
1137
                $xapian_data = [
1138
                    SE_COURSE_ID => $course_id,
1139
                    SE_TOOL_ID => TOOL_QUIZ,
1140
                    SE_DATA => [
1141
                        'type' => SE_DOCTYPE_EXERCISE_QUESTION,
1142
                        'exercise_ids' => $question_exercises,
1143
                        'question_id' => (int) $this->id,
1144
                    ],
1145
                    SE_USER => (int) api_get_user_id(),
1146
                ];
1147
                $ic_slide->xapian_data = serialize($xapian_data);
1148
                $ic_slide->addValue("content", $this->description);
1149
1150
                //TODO: index answers, see also form validation on question_admin.inc.php
1151
1152
                $di->remove_document($se_ref['search_did']);
1153
                $di->addChunk($ic_slide);
1154
1155
                //index and return search engine document id
1156
                if (!empty($question_exercises)) { // if empty there is nothing to index
1157
                    $did = $di->index();
1158
                    unset($di);
1159
                }
1160
                if ($did || $rmQs) {
1161
                    // save it to db
1162
                    if ($addQs || $rmQs) {
1163
                        $sql = "DELETE FROM %s
1164
                            WHERE course_code = '%s' AND tool_id = '%s' AND ref_id_second_level = '%s'";
1165
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
1166
                    } else {
1167
                        $sql = "DELETE FROM %S
1168
                            WHERE
1169
                                course_code = '%s'
1170
                                AND tool_id = '%s'
1171
                                AND tool_id = '%s'
1172
                                AND ref_id_high_level = '%s'
1173
                                AND ref_id_second_level = '%s'";
1174
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
1175
                    }
1176
                    Database::query($sql);
1177
                    if ($rmQs) {
1178
                        if (!empty($question_exercises)) {
1179
                            $sql = "INSERT INTO %s (
1180
                                    id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
1181
                                )
1182
                                VALUES (
1183
                                    NULL, '%s', '%s', %s, %s, %s
1184
                                )";
1185
                            $sql = sprintf(
1186
                                $sql,
1187
                                $tbl_se_ref,
1188
                                $course_id,
1189
                                TOOL_QUIZ,
1190
                                array_shift($question_exercises),
1191
                                $this->id,
1192
                                $did
1193
                            );
1194
                            Database::query($sql);
1195
                        }
1196
                    } else {
1197
                        $sql = "INSERT INTO %s (
1198
                                id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
1199
                            )
1200
                            VALUES (
1201
                                NULL , '%s', '%s', %s, %s, %s
1202
                            )";
1203
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id, $did);
1204
                        Database::query($sql);
1205
                    }
1206
                }
1207
            }
1208
        }
1209
    }
1210
1211
    /**
1212
     * adds an exercise into the exercise list.
1213
     *
1214
     * @author Olivier Brouckaert
1215
     *
1216
     * @param int  $exerciseId - exercise ID
1217
     * @param bool $fromSave   - from $this->save() or not
1218
     */
1219
    public function addToList($exerciseId, $fromSave = false)
1220
    {
1221
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1222
        $id = $this->id;
1223
        // checks if the exercise ID is not in the list
1224
        if (!in_array($exerciseId, $this->exerciseList)) {
1225
            $this->exerciseList[] = $exerciseId;
1226
            $new_exercise = new Exercise();
1227
            $new_exercise->read($exerciseId);
1228
            $count = $new_exercise->selectNbrQuestions();
1229
            $count++;
1230
            $sql = "INSERT INTO $exerciseRelQuestionTable (c_id, question_id, exercice_id, question_order)
1231
                    VALUES ({$this->course['real_id']}, ".intval($id).", ".intval($exerciseId).", '$count')";
1232
            Database::query($sql);
1233
1234
            // we do not want to reindex if we had just saved adnd indexed the question
1235
            if (!$fromSave) {
1236
                $this->search_engine_edit($exerciseId, true);
1237
            }
1238
        }
1239
    }
1240
1241
    /**
1242
     * removes an exercise from the exercise list.
1243
     *
1244
     * @author Olivier Brouckaert
1245
     *
1246
     * @param int $exerciseId - exercise ID
1247
     *
1248
     * @return bool - true if removed, otherwise false
1249
     */
1250
    public function removeFromList($exerciseId)
1251
    {
1252
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1253
        $id = $this->id;
1254
1255
        // searches the position of the exercise ID in the list
1256
        $pos = array_search($exerciseId, $this->exerciseList);
1257
        $course_id = api_get_course_int_id();
1258
1259
        // exercise not found
1260
        if ($pos === false) {
1261
            return false;
1262
        } else {
1263
            // deletes the position in the array containing the wanted exercise ID
1264
            unset($this->exerciseList[$pos]);
1265
            //update order of other elements
1266
            $sql = "SELECT question_order
1267
                    FROM $TBL_EXERCISE_QUESTION
1268
                    WHERE
1269
                        c_id = $course_id
1270
                        AND question_id = ".intval($id)."
1271
                        AND exercice_id = ".intval($exerciseId);
1272
            $res = Database::query($sql);
1273
            if (Database::num_rows($res) > 0) {
1274
                $row = Database::fetch_array($res);
1275
                if (!empty($row['question_order'])) {
1276
                    $sql = "UPDATE $TBL_EXERCISE_QUESTION
1277
                        SET question_order = question_order-1
1278
                        WHERE
1279
                            c_id = $course_id
1280
                            AND exercice_id = ".intval($exerciseId)."
1281
                            AND question_order > ".$row['question_order'];
1282
                    Database::query($sql);
1283
                }
1284
            }
1285
1286
            $sql = "DELETE FROM $TBL_EXERCISE_QUESTION
1287
                    WHERE
1288
                        c_id = $course_id
1289
                        AND question_id = ".intval($id)."
1290
                        AND exercice_id = ".intval($exerciseId);
1291
            Database::query($sql);
1292
1293
            return true;
1294
        }
1295
    }
1296
1297
    /**
1298
     * Deletes a question from the database
1299
     * the parameter tells if the question is removed from all exercises (value = 0),
1300
     * or just from one exercise (value = exercise ID).
1301
     *
1302
     * @author Olivier Brouckaert
1303
     *
1304
     * @param int $deleteFromEx - exercise ID if the question is only removed from one exercise
1305
     */
1306
    public function delete($deleteFromEx = 0)
1307
    {
1308
        $course_id = api_get_course_int_id();
1309
1310
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1311
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1312
        $TBL_REPONSES = Database::get_course_table(TABLE_QUIZ_ANSWER);
1313
        $TBL_QUIZ_QUESTION_REL_CATEGORY = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
1314
1315
        $id = intval($this->id);
1316
1317
        // if the question must be removed from all exercises
1318
        if (!$deleteFromEx) {
1319
            //update the question_order of each question to avoid inconsistencies
1320
            $sql = "SELECT exercice_id, question_order 
1321
                    FROM $TBL_EXERCISE_QUESTION
1322
                    WHERE c_id = $course_id AND question_id = ".intval($id)."";
1323
1324
            $res = Database::query($sql);
1325
            if (Database::num_rows($res) > 0) {
1326
                while ($row = Database::fetch_array($res)) {
1327
                    if (!empty($row['question_order'])) {
1328
                        $sql = "UPDATE $TBL_EXERCISE_QUESTION
1329
                                SET question_order = question_order-1
1330
                                WHERE
1331
                                    c_id = $course_id AND 
1332
                                    exercice_id = ".intval($row['exercice_id'])." AND 
1333
                                    question_order > ".$row['question_order'];
1334
                        Database::query($sql);
1335
                    }
1336
                }
1337
            }
1338
1339
            $sql = "DELETE FROM $TBL_EXERCISE_QUESTION
1340
                    WHERE c_id = $course_id AND question_id = ".$id;
1341
            Database::query($sql);
1342
1343
            $sql = "DELETE FROM $TBL_QUESTIONS
1344
                    WHERE c_id = $course_id AND id = ".$id;
1345
            Database::query($sql);
1346
1347
            $sql = "DELETE FROM $TBL_REPONSES
1348
                    WHERE c_id = $course_id AND question_id = ".$id;
1349
            Database::query($sql);
1350
1351
            // remove the category of this question in the question_rel_category table
1352
            $sql = "DELETE FROM $TBL_QUIZ_QUESTION_REL_CATEGORY
1353
                    WHERE 
1354
                        c_id = $course_id AND 
1355
                        question_id = ".$id;
1356
            Database::query($sql);
1357
1358
            api_item_property_update(
1359
                $this->course,
1360
                TOOL_QUIZ,
1361
                $id,
1362
                'QuizQuestionDeleted',
1363
                api_get_user_id()
1364
            );
1365
            $this->removePicture();
1366
        } else {
1367
            // just removes the exercise from the list
1368
            $this->removeFromList($deleteFromEx);
1369
            if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
1370
                // disassociate question with this exercise
1371
                $this->search_engine_edit($deleteFromEx, false, true);
1372
            }
1373
1374
            api_item_property_update(
1375
                $this->course,
1376
                TOOL_QUIZ,
1377
                $id,
1378
                'QuizQuestionDeleted',
1379
                api_get_user_id()
1380
            );
1381
        }
1382
    }
1383
1384
    /**
1385
     * Duplicates the question.
1386
     *
1387
     * @author Olivier Brouckaert
1388
     *
1389
     * @param array $courseInfo Course info of the destination course
1390
     *
1391
     * @return false|string ID of the new question
1392
     */
1393
    public function duplicate($courseInfo = [])
1394
    {
1395
        $courseInfo = empty($courseInfo) ? $this->course : $courseInfo;
1396
1397
        if (empty($courseInfo)) {
1398
            return false;
1399
        }
1400
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
1401
        $TBL_QUESTION_OPTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1402
1403
        $question = $this->question;
1404
        $description = $this->description;
1405
        $weighting = $this->weighting;
1406
        $position = $this->position;
1407
        $type = $this->type;
1408
        $level = (int) $this->level;
1409
        $extra = $this->extra;
1410
1411
        // Using the same method used in the course copy to transform URLs
1412
        if ($this->course['id'] != $courseInfo['id']) {
1413
            $description = DocumentManager::replaceUrlWithNewCourseCode(
1414
                $description,
1415
                $this->course['code'],
1416
                $courseInfo['id']
1417
            );
1418
            $question = DocumentManager::replaceUrlWithNewCourseCode(
1419
                $question,
1420
                $this->course['code'],
1421
                $courseInfo['id']
1422
            );
1423
        }
1424
1425
        $course_id = $courseInfo['real_id'];
1426
1427
        // Read the source options
1428
        $options = self::readQuestionOption($this->id, $this->course['real_id']);
1429
1430
        // Inserting in the new course db / or the same course db
1431
        $params = [
1432
            'c_id' => $course_id,
1433
            'question' => $question,
1434
            'description' => $description,
1435
            'ponderation' => $weighting,
1436
            'position' => $position,
1437
            'type' => $type,
1438
            'level' => $level,
1439
            'extra' => $extra,
1440
        ];
1441
        $newQuestionId = Database::insert($questionTable, $params);
1442
1443
        if ($newQuestionId) {
1444
            $sql = "UPDATE $questionTable 
1445
                    SET id = iid
1446
                    WHERE iid = $newQuestionId";
1447
            Database::query($sql);
1448
1449
            if (!empty($options)) {
1450
                // Saving the quiz_options
1451
                foreach ($options as $item) {
1452
                    $item['question_id'] = $newQuestionId;
1453
                    $item['c_id'] = $course_id;
1454
                    unset($item['id']);
1455
                    unset($item['iid']);
1456
                    $id = Database::insert($TBL_QUESTION_OPTIONS, $item);
1457
                    if ($id) {
1458
                        $sql = "UPDATE $TBL_QUESTION_OPTIONS 
1459
                                SET id = iid
1460
                                WHERE iid = $id";
1461
                        Database::query($sql);
1462
                    }
1463
                }
1464
            }
1465
1466
            // Duplicates the picture of the hotspot
1467
            $this->exportPicture($newQuestionId, $courseInfo);
1468
        }
1469
1470
        return $newQuestionId;
1471
    }
1472
1473
    /**
1474
     * @return string
1475
     */
1476
    public function get_question_type_name()
1477
    {
1478
        $key = self::$questionTypes[$this->type];
1479
1480
        return get_lang($key[1]);
1481
    }
1482
1483
    /**
1484
     * @param string $type
1485
     */
1486
    public static function get_question_type($type)
1487
    {
1488
        if ($type == ORAL_EXPRESSION && api_get_setting('enable_record_audio') !== 'true') {
1489
            return null;
1490
        }
1491
1492
        return self::$questionTypes[$type];
1493
    }
1494
1495
    /**
1496
     * @return array
1497
     */
1498
    public static function get_question_type_list()
1499
    {
1500
        if (api_get_setting('enable_record_audio') !== 'true') {
1501
            self::$questionTypes[ORAL_EXPRESSION] = null;
1502
            unset(self::$questionTypes[ORAL_EXPRESSION]);
1503
        }
1504
        if (api_get_setting('enable_quiz_scenario') !== 'true') {
1505
            self::$questionTypes[HOT_SPOT_DELINEATION] = null;
1506
            unset(self::$questionTypes[HOT_SPOT_DELINEATION]);
1507
        }
1508
1509
        return self::$questionTypes;
1510
    }
1511
1512
    /**
1513
     * Returns an instance of the class corresponding to the type.
1514
     *
1515
     * @param int $type the type of the question
1516
     *
1517
     * @return $this instance of a Question subclass (or of Questionc class by default)
1518
     */
1519
    public static function getInstance($type)
1520
    {
1521
        if (!is_null($type)) {
1522
            list($file_name, $class_name) = self::get_question_type($type);
1523
            if (!empty($file_name)) {
1524
                if (class_exists($class_name)) {
1525
                    return new $class_name();
1526
                } else {
1527
                    echo 'Can\'t instanciate class '.$class_name.' of type '.$type;
1528
                }
1529
            }
1530
        }
1531
1532
        return null;
1533
    }
1534
1535
    /**
1536
     * Creates the form to create / edit a question
1537
     * A subclass can redefine this function to add fields...
1538
     *
1539
     * @param FormValidator $form
1540
     * @param Exercise      $exercise
1541
     */
1542
    public function createForm(&$form, $exercise)
1543
    {
1544
        echo '<style>
1545
                .media { display:none;}
1546
            </style>';
1547
1548
        // question name
1549
        if (api_get_configuration_value('save_titles_as_html')) {
1550
            $editorConfig = ['ToolbarSet' => 'Minimal'];
1551
            $form->addHtmlEditor(
1552
                'questionName',
1553
                get_lang('Question'),
1554
                false,
1555
                false,
1556
                $editorConfig,
1557
                true
1558
            );
1559
        } else {
1560
            $form->addElement('text', 'questionName', get_lang('Question'));
1561
        }
1562
1563
        $form->addRule('questionName', get_lang('GiveQuestion'), 'required');
1564
1565
        // default content
1566
        $isContent = isset($_REQUEST['isContent']) ? intval($_REQUEST['isContent']) : null;
1567
1568
        // Question type
1569
        $answerType = isset($_REQUEST['answerType']) ? intval($_REQUEST['answerType']) : null;
1570
        $form->addElement('hidden', 'answerType', $answerType);
1571
1572
        // html editor
1573
        $editorConfig = [
1574
            'ToolbarSet' => 'TestQuestionDescription',
1575
            'Height' => '150',
1576
        ];
1577
1578
        if (!api_is_allowed_to_edit(null, true)) {
1579
            $editorConfig['UserStatus'] = 'student';
1580
        }
1581
1582
        $form->addButtonAdvancedSettings('advanced_params');
1583
        $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
1584
        $form->addHtmlEditor(
1585
            'questionDescription',
1586
            get_lang('QuestionDescription'),
1587
            false,
1588
            false,
1589
            $editorConfig
1590
        );
1591
1592
        // hidden values
1593
        $my_id = isset($_REQUEST['myid']) ? intval($_REQUEST['myid']) : null;
1594
        $form->addElement('hidden', 'myid', $my_id);
1595
1596
        if ($this->type != MEDIA_QUESTION) {
1597
            // Advanced parameters
1598
            $select_level = self::get_default_levels();
1599
            $form->addElement(
1600
                'select',
1601
                'questionLevel',
1602
                get_lang('Difficulty'),
1603
                $select_level
1604
            );
1605
1606
            // Categories
1607
            $tabCat = TestCategory::getCategoriesIdAndName();
1608
            $form->addElement(
1609
                'select',
1610
                'questionCategory',
1611
                get_lang('Category'),
1612
                $tabCat
1613
            );
1614
1615
            global $text;
1616
1617
            switch ($this->type) {
1618
                case UNIQUE_ANSWER:
1619
                    $buttonGroup = [];
1620
                    $buttonGroup[] = $form->addButtonSave(
1621
                        $text,
1622
                        'submitQuestion',
1623
                        true
1624
                    );
1625
                    $buttonGroup[] = $form->addButton(
1626
                        'convertAnswer',
1627
                        get_lang('ConvertToMultipleAnswer'),
1628
                        'dot-circle-o',
1629
                        'default',
1630
                        null,
1631
                        null,
1632
                        null,
1633
                        true
1634
                    );
1635
                    $form->addGroup($buttonGroup);
1636
                    break;
1637
                case MULTIPLE_ANSWER:
1638
                    $buttonGroup = [];
1639
                    $buttonGroup[] = $form->addButtonSave(
1640
                        $text,
1641
                        'submitQuestion',
1642
                        true
1643
                    );
1644
                    $buttonGroup[] = $form->addButton(
1645
                        'convertAnswer',
1646
                        get_lang('ConvertToUniqueAnswer'),
1647
                        'check-square-o',
1648
                        'default',
1649
                        null,
1650
                        null,
1651
                        null,
1652
                        true
1653
                    );
1654
                    $form->addGroup($buttonGroup);
1655
                    break;
1656
            }
1657
            //Medias
1658
            //$course_medias = self::prepare_course_media_select(api_get_course_int_id());
1659
            //$form->addElement('select', 'parent_id', get_lang('AttachToMedia'), $course_medias);
1660
        }
1661
1662
        $form->addElement('html', '</div>');
1663
1664
        if (!isset($_GET['fromExercise'])) {
1665
            switch ($answerType) {
1666
                case 1:
1667
                    $this->question = get_lang('DefaultUniqueQuestion');
1668
                    break;
1669
                case 2:
1670
                    $this->question = get_lang('DefaultMultipleQuestion');
1671
                    break;
1672
                case 3:
1673
                    $this->question = get_lang('DefaultFillBlankQuestion');
1674
                    break;
1675
                case 4:
1676
                    $this->question = get_lang('DefaultMathingQuestion');
1677
                    break;
1678
                case 5:
1679
                    $this->question = get_lang('DefaultOpenQuestion');
1680
                    break;
1681
                case 9:
1682
                    $this->question = get_lang('DefaultMultipleQuestion');
1683
                    break;
1684
            }
1685
        }
1686
1687
        if (!is_null($exercise)) {
1688
            if ($exercise->questionFeedbackEnabled && $this->showFeedback($exercise)) {
1689
                $form->addTextarea('feedback', get_lang('FeedbackIfNotCorrect'));
1690
            }
1691
        }
1692
1693
        // default values
1694
        $defaults = [];
1695
        $defaults['questionName'] = $this->question;
1696
        $defaults['questionDescription'] = $this->description;
1697
        $defaults['questionLevel'] = $this->level;
1698
        $defaults['questionCategory'] = $this->category;
1699
        $defaults['feedback'] = $this->feedback;
1700
1701
        // Came from he question pool
1702
        if (isset($_GET['fromExercise'])) {
1703
            $form->setDefaults($defaults);
1704
        }
1705
1706
        if (!empty($_REQUEST['myid'])) {
1707
            $form->setDefaults($defaults);
1708
        } else {
1709
            if ($isContent == 1) {
1710
                $form->setDefaults($defaults);
1711
            }
1712
        }
1713
    }
1714
1715
    /**
1716
     * function which process the creation of questions.
1717
     *
1718
     * @param FormValidator $form
1719
     * @param Exercise      $exercise
1720
     */
1721
    public function processCreation($form, $exercise)
1722
    {
1723
        $this->updateTitle($form->getSubmitValue('questionName'));
1724
        $this->updateDescription($form->getSubmitValue('questionDescription'));
1725
        $this->updateLevel($form->getSubmitValue('questionLevel'));
1726
        $this->updateCategory($form->getSubmitValue('questionCategory'));
1727
        $this->setFeedback($form->getSubmitValue('feedback'));
1728
1729
        //Save normal question if NOT media
1730
        if ($this->type != MEDIA_QUESTION) {
1731
            $this->save($exercise);
1732
1733
            // modify the exercise
1734
            $exercise->addToList($this->id);
1735
            $exercise->update_question_positions();
1736
        }
1737
    }
1738
1739
    /**
1740
     * abstract function which creates the form to create / edit the answers of the question.
1741
     *
1742
     * @param FormValidator $form
1743
     */
1744
    abstract public function createAnswersForm($form);
1745
1746
    /**
1747
     * abstract function which process the creation of answers.
1748
     *
1749
     * @param FormValidator $form
1750
     * @param Exercise      $exercise
1751
     */
1752
    abstract public function processAnswersCreation($form, $exercise);
1753
1754
    /**
1755
     * Displays the menu of question types.
1756
     *
1757
     * @param Exercise $objExercise
1758
     */
1759
    public static function display_type_menu($objExercise)
1760
    {
1761
        $feedback_type = $objExercise->feedback_type;
1762
        $exerciseId = $objExercise->id;
1763
1764
        // 1. by default we show all the question types
1765
        $question_type_custom_list = self::get_question_type_list();
1766
1767
        if (!isset($feedback_type)) {
1768
            $feedback_type = 0;
1769
        }
1770
1771
        if ($feedback_type == 1) {
1772
            //2. but if it is a feedback DIRECT we only show the UNIQUE_ANSWER type that is currently available
1773
            $question_type_custom_list = [
1774
                UNIQUE_ANSWER => self::$questionTypes[UNIQUE_ANSWER],
1775
                HOT_SPOT_DELINEATION => self::$questionTypes[HOT_SPOT_DELINEATION],
1776
            ];
1777
        } else {
1778
            unset($question_type_custom_list[HOT_SPOT_DELINEATION]);
1779
        }
1780
1781
        echo '<div class="well">';
1782
        echo '<ul class="question_menu">';
1783
        foreach ($question_type_custom_list as $i => $a_type) {
1784
            // @todo remove require_once classes are already loaded using composer
1785
            // include the class of the type
1786
            require_once $a_type[0];
1787
            // get the picture of the type and the langvar which describes it
1788
            $img = $explanation = '';
1789
            eval('$img = '.$a_type[1].'::$typePicture;');
1790
            eval('$explanation = get_lang('.$a_type[1].'::$explanationLangVar);');
1791
            echo '<li>';
1792
            echo '<div class="icon-image">';
1793
            $icon = '<a href="admin.php?'.api_get_cidreq().'&newQuestion=yes&answerType='.$i.'">'.
1794
                Display::return_icon($img, $explanation, null, ICON_SIZE_BIG).'</a>';
1795
1796
            if ($objExercise->force_edit_exercise_in_lp === false) {
1797
                if ($objExercise->exercise_was_added_in_lp == true) {
1798
                    $img = pathinfo($img);
1799
                    $img = $img['filename'].'_na.'.$img['extension'];
1800
                    $icon = Display::return_icon($img, $explanation, null, ICON_SIZE_BIG);
1801
                }
1802
            }
1803
1804
            echo $icon;
1805
            echo '</div>';
1806
            echo '</li>';
1807
        }
1808
1809
        echo '<li>';
1810
        echo '<div class="icon_image_content">';
1811
        if ($objExercise->exercise_was_added_in_lp == true) {
1812
            echo Display::return_icon(
1813
                'database_na.png',
1814
                get_lang('GetExistingQuestion'),
1815
                null,
1816
                ICON_SIZE_BIG
1817
            );
1818
        } else {
1819
            if ($feedback_type == 1) {
1820
                echo $url = "<a href=\"question_pool.php?".api_get_cidreq()."&type=1&fromExercise=$exerciseId\">";
1821
            } else {
1822
                echo $url = '<a href="question_pool.php?'.api_get_cidreq().'&fromExercise='.$exerciseId.'">';
1823
            }
1824
            echo Display::return_icon(
1825
                'database.png',
1826
                get_lang('GetExistingQuestion'),
1827
                null,
1828
                ICON_SIZE_BIG
1829
            );
1830
        }
1831
        echo '</a>';
1832
        echo '</div></li>';
1833
        echo '</ul>';
1834
        echo '</div>';
1835
    }
1836
1837
    /**
1838
     * @param int    $question_id
1839
     * @param string $name
1840
     * @param int    $course_id
1841
     * @param int    $position
1842
     *
1843
     * @return false|string
1844
     */
1845
    public static function saveQuestionOption($question_id, $name, $course_id, $position = 0)
1846
    {
1847
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1848
        $params['question_id'] = intval($question_id);
1849
        $params['name'] = $name;
1850
        $params['position'] = $position;
1851
        $params['c_id'] = $course_id;
1852
        $result = self::readQuestionOption($question_id, $course_id);
1853
        $last_id = Database::insert($table, $params);
1854
        if ($last_id) {
1855
            $sql = "UPDATE $table SET id = iid WHERE iid = $last_id";
1856
            Database::query($sql);
1857
        }
1858
1859
        return $last_id;
1860
    }
1861
1862
    /**
1863
     * @param int $question_id
1864
     * @param int $course_id
1865
     */
1866
    public static function deleteAllQuestionOptions($question_id, $course_id)
1867
    {
1868
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1869
        Database::delete(
1870
            $table,
1871
            [
1872
                'c_id = ? AND question_id = ?' => [
1873
                    $course_id,
1874
                    $question_id,
1875
                ],
1876
            ]
1877
        );
1878
    }
1879
1880
    /**
1881
     * @param int   $id
1882
     * @param array $params
1883
     * @param int   $course_id
1884
     *
1885
     * @return bool|int
1886
     */
1887
    public static function updateQuestionOption($id, $params, $course_id)
1888
    {
1889
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1890
        $result = Database::update(
1891
            $table,
1892
            $params,
1893
            ['c_id = ? AND id = ?' => [$course_id, $id]]
1894
        );
1895
1896
        return $result;
1897
    }
1898
1899
    /**
1900
     * @param int $question_id
1901
     * @param int $course_id
1902
     *
1903
     * @return array
1904
     */
1905
    public static function readQuestionOption($question_id, $course_id)
1906
    {
1907
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1908
        $result = Database::select(
1909
            '*',
1910
            $table,
1911
            [
1912
                'where' => [
1913
                    'c_id = ? AND question_id = ?' => [
1914
                        $course_id,
1915
                        $question_id,
1916
                    ],
1917
                ],
1918
                'order' => 'id ASC',
1919
            ]
1920
        );
1921
1922
        return $result;
1923
    }
1924
1925
    /**
1926
     * Shows question title an description.
1927
     *
1928
     * @param Exercise $exercise
1929
     * @param int      $counter
1930
     * @param array    $score
1931
     *
1932
     * @return string HTML string with the header of the question (before the answers table)
1933
     */
1934
    public function return_header($exercise, $counter = null, $score = [])
1935
    {
1936
        $counterLabel = '';
1937
        if (!empty($counter)) {
1938
            $counterLabel = intval($counter);
1939
        }
1940
        $score_label = get_lang('Wrong');
1941
        $class = 'error';
1942
        if ($score['pass'] == true) {
1943
            $score_label = get_lang('Correct');
1944
            $class = 'success';
1945
        }
1946
1947
        if (in_array($this->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION])) {
1948
            $score['revised'] = isset($score['revised']) ? $score['revised'] : false;
1949
            if ($score['revised'] == true) {
1950
                $score_label = get_lang('Revised');
1951
                $class = '';
1952
            } else {
1953
                $score_label = get_lang('NotRevised');
1954
                $class = 'warning';
1955
                $weight = float_format($score['weight'], 1);
1956
                $score['result'] = " ? / ".$weight;
1957
                $model = ExerciseLib::getCourseScoreModel();
1958
                if (!empty($model)) {
1959
                    $score['result'] = " ? ";
1960
                }
1961
1962
                $hide = api_get_configuration_value('hide_free_question_score');
1963
                if ($hide === true) {
1964
                    $score['result'] = '-';
1965
                }
1966
            }
1967
        }
1968
1969
        // display question category, if any
1970
        $header = '';
1971
        if ($exercise->display_category_name) {
1972
            $header = TestCategory::returnCategoryAndTitle($this->id);
1973
        }
1974
        $show_media = '';
1975
        if ($show_media) {
1976
            $header .= $this->show_media_content();
1977
        }
1978
        $scoreCurrent = [
1979
            'used' => $score['score'],
1980
            'missing' => $score['weight'],
1981
        ];
1982
        $header .= Display::page_subheader2($counterLabel.'. '.$this->question);
1983
        $header .= $exercise->getQuestionRibbon($class, $score_label, $score['result'], $scoreCurrent);
1984
        if ($this->type != READING_COMPREHENSION) {
1985
            // Do not show the description (the text to read) if the question is of type READING_COMPREHENSION
1986
            $header .= Display::div(
1987
                $this->description,
1988
                ['class' => 'question_description']
1989
            );
1990
        } else {
1991
            if ($score['pass'] == true) {
1992
                $message = Display::div(
1993
                    sprintf(
1994
                        get_lang('ReadingQuestionCongratsSpeedXReachedForYWords'),
1995
                        ReadingComprehension::$speeds[$this->level],
1996
                        $this->getWordsCount()
1997
                    )
1998
                );
1999
            } else {
2000
                $message = Display::div(
2001
                    sprintf(
2002
                        get_lang('ReadingQuestionCongratsSpeedXNotReachedForYWords'),
2003
                        ReadingComprehension::$speeds[$this->level],
2004
                        $this->getWordsCount()
2005
                    )
2006
                );
2007
            }
2008
            $header .= $message.'<br />';
2009
        }
2010
2011
        if (isset($score['pass']) && $score['pass'] === false) {
2012
            if ($this->showFeedback($exercise)) {
2013
                $header .= $this->returnFormatFeedback();
2014
            }
2015
        }
2016
2017
        return $header;
2018
    }
2019
2020
    /**
2021
     * Create a question from a set of parameters.
2022
     *
2023
     * @param   int     Quiz ID
2024
     * @param   string  Question name
2025
     * @param   int     Maximum result for the question
2026
     * @param   int     Type of question (see constants at beginning of question.class.php)
2027
     * @param   int     Question level/category
2028
     * @param string $quiz_id
2029
     */
2030
    public function create_question(
2031
        $quiz_id,
2032
        $question_name,
2033
        $question_description = '',
2034
        $max_score = 0,
2035
        $type = 1,
2036
        $level = 1
2037
    ) {
2038
        $course_id = api_get_course_int_id();
2039
        $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2040
        $tbl_quiz_rel_question = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
2041
2042
        $quiz_id = intval($quiz_id);
2043
        $max_score = (float) $max_score;
2044
        $type = intval($type);
2045
        $level = intval($level);
2046
2047
        // Get the max position
2048
        $sql = "SELECT max(position) as max_position
2049
                FROM $tbl_quiz_question q 
2050
                INNER JOIN $tbl_quiz_rel_question r
2051
                ON
2052
                    q.id = r.question_id AND
2053
                    exercice_id = $quiz_id AND
2054
                    q.c_id = $course_id AND
2055
                    r.c_id = $course_id";
2056
        $rs_max = Database::query($sql);
2057
        $row_max = Database::fetch_object($rs_max);
2058
        $max_position = $row_max->max_position + 1;
2059
2060
        $params = [
2061
            'c_id' => $course_id,
2062
            'question' => $question_name,
2063
            'description' => $question_description,
2064
            'ponderation' => $max_score,
2065
            'position' => $max_position,
2066
            'type' => $type,
2067
            'level' => $level,
2068
        ];
2069
        $question_id = Database::insert($tbl_quiz_question, $params);
2070
2071
        if ($question_id) {
2072
            $sql = "UPDATE $tbl_quiz_question  
2073
                    SET id = iid WHERE iid = $question_id";
2074
            Database::query($sql);
2075
2076
            // Get the max question_order
2077
            $sql = "SELECT max(question_order) as max_order
2078
                    FROM $tbl_quiz_rel_question
2079
                    WHERE c_id = $course_id AND exercice_id = $quiz_id ";
2080
            $rs_max_order = Database::query($sql);
2081
            $row_max_order = Database::fetch_object($rs_max_order);
2082
            $max_order = $row_max_order->max_order + 1;
2083
            // Attach questions to quiz
2084
            $sql = "INSERT INTO $tbl_quiz_rel_question (c_id, question_id, exercice_id, question_order)
2085
                    VALUES($course_id, $question_id, $quiz_id, $max_order)";
2086
            Database::query($sql);
2087
        }
2088
2089
        return $question_id;
2090
    }
2091
2092
    /**
2093
     * @return array the image filename of the question type
2094
     */
2095
    public function get_type_icon_html()
2096
    {
2097
        $type = $this->selectType();
2098
        $tabQuestionList = self::get_question_type_list(); // [0]=file to include [1]=type name
2099
2100
        require_once $tabQuestionList[$type][0];
2101
2102
        $img = $explanation = null;
2103
        eval('$img = '.$tabQuestionList[$type][1].'::$typePicture;');
2104
        eval('$explanation = get_lang('.$tabQuestionList[$type][1].'::$explanationLangVar);');
2105
2106
        return [$img, $explanation];
2107
    }
2108
2109
    /**
2110
     * Get course medias.
2111
     *
2112
     * @param int course id
2113
     * @param int $course_id
2114
     *
2115
     * @return array
2116
     */
2117
    public static function get_course_medias(
2118
        $course_id,
2119
        $start = 0,
2120
        $limit = 100,
2121
        $sidx = "question",
2122
        $sord = "ASC",
2123
        $where_condition = []
2124
    ) {
2125
        $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2126
        $default_where = [
2127
            'c_id = ? AND parent_id = 0 AND type = ?' => [
2128
                $course_id,
2129
                MEDIA_QUESTION,
2130
            ],
2131
        ];
2132
        $result = Database::select(
2133
            '*',
2134
            $table_question,
2135
            [
2136
                'limit' => " $start, $limit",
2137
                'where' => $default_where,
2138
                'order' => "$sidx $sord",
2139
            ]
2140
        );
2141
2142
        return $result;
2143
    }
2144
2145
    /**
2146
     * Get count course medias.
2147
     *
2148
     * @param int course id
2149
     *
2150
     * @return int
2151
     */
2152
    public static function get_count_course_medias($course_id)
2153
    {
2154
        $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2155
        $result = Database::select(
2156
            'count(*) as count',
2157
            $table_question,
2158
            [
2159
                'where' => [
2160
                    'c_id = ? AND parent_id = 0 AND type = ?' => [
2161
                        $course_id,
2162
                        MEDIA_QUESTION,
2163
                    ],
2164
                ],
2165
            ],
2166
            'first'
2167
        );
2168
2169
        if ($result && isset($result['count'])) {
2170
            return $result['count'];
2171
        }
2172
2173
        return 0;
2174
    }
2175
2176
    /**
2177
     * @param int $course_id
2178
     *
2179
     * @return array
2180
     */
2181
    public static function prepare_course_media_select($course_id)
2182
    {
2183
        $medias = self::get_course_medias($course_id);
2184
        $media_list = [];
2185
        $media_list[0] = get_lang('NoMedia');
2186
2187
        if (!empty($medias)) {
2188
            foreach ($medias as $media) {
2189
                $media_list[$media['id']] = empty($media['question']) ? get_lang('Untitled') : $media['question'];
2190
            }
2191
        }
2192
2193
        return $media_list;
2194
    }
2195
2196
    /**
2197
     * @return array
2198
     */
2199
    public static function get_default_levels()
2200
    {
2201
        $levels = [
2202
            1 => 1,
2203
            2 => 2,
2204
            3 => 3,
2205
            4 => 4,
2206
            5 => 5,
2207
        ];
2208
2209
        return $levels;
2210
    }
2211
2212
    /**
2213
     * @return string
2214
     */
2215
    public function show_media_content()
2216
    {
2217
        $html = '';
2218
        if ($this->parent_id != 0) {
2219
            $parent_question = self::read($this->parent_id);
2220
            $html = $parent_question->show_media_content();
2221
        } else {
2222
            $html .= Display::page_subheader($this->selectTitle());
2223
            $html .= $this->selectDescription();
2224
        }
2225
2226
        return $html;
2227
    }
2228
2229
    /**
2230
     * Swap between unique and multiple type answers.
2231
     *
2232
     * @return UniqueAnswer|MultipleAnswer
2233
     */
2234
    public function swapSimpleAnswerTypes()
2235
    {
2236
        $oppositeAnswers = [
2237
            UNIQUE_ANSWER => MULTIPLE_ANSWER,
2238
            MULTIPLE_ANSWER => UNIQUE_ANSWER,
2239
        ];
2240
        $this->type = $oppositeAnswers[$this->type];
2241
        Database::update(
2242
            Database::get_course_table(TABLE_QUIZ_QUESTION),
2243
            ['type' => $this->type],
2244
            ['c_id = ? AND id = ?' => [$this->course['real_id'], $this->id]]
2245
        );
2246
        $answerClasses = [
2247
            UNIQUE_ANSWER => 'UniqueAnswer',
2248
            MULTIPLE_ANSWER => 'MultipleAnswer',
2249
        ];
2250
        $swappedAnswer = new $answerClasses[$this->type]();
2251
        foreach ($this as $key => $value) {
2252
            $swappedAnswer->$key = $value;
2253
        }
2254
2255
        return $swappedAnswer;
2256
    }
2257
2258
    /**
2259
     * @param array $score
2260
     *
2261
     * @return bool
2262
     */
2263
    public function isQuestionWaitingReview($score)
2264
    {
2265
        $isReview = false;
2266
        if (!empty($score)) {
2267
            if (!empty($score['comments']) || $score['score'] > 0) {
2268
                $isReview = true;
2269
            }
2270
        }
2271
2272
        return $isReview;
2273
    }
2274
2275
    /**
2276
     * @param string $value
2277
     */
2278
    public function setFeedback($value)
2279
    {
2280
        $this->feedback = $value;
2281
    }
2282
2283
    /**
2284
     * @param Exercise $exercise
2285
     *
2286
     * @return bool
2287
     */
2288
    public function showFeedback($exercise)
2289
    {
2290
        return
2291
            in_array($this->type, $this->questionTypeWithFeedback) &&
2292
            $exercise->feedback_type != EXERCISE_FEEDBACK_TYPE_EXAM;
2293
    }
2294
2295
    /**
2296
     * @return string
2297
     */
2298
    public function returnFormatFeedback()
2299
    {
2300
        return '<br />'.Display::return_message($this->feedback, 'normal', false);
2301
    }
2302
2303
    /**
2304
     * Resizes a picture || Warning!: can only be called after uploadPicture,
2305
     * or if picture is already available in object.
2306
     *
2307
     * @param string $Dimension - Resizing happens proportional according to given dimension: height|width|any
2308
     * @param int    $Max       - Maximum size
2309
     *
2310
     * @return bool|null - true if success, false if failed
2311
     *
2312
     * @author Toon Keppens
2313
     */
2314
    private function resizePicture($Dimension, $Max)
2315
    {
2316
        // if the question has an ID
2317
        if (!$this->id) {
2318
            return false;
2319
        }
2320
2321
        $picturePath = $this->getHotSpotFolderInCourse().'/'.$this->getPictureFilename();
2322
2323
        // Get dimensions from current image.
2324
        $my_image = new Image($picturePath);
2325
2326
        $current_image_size = $my_image->get_image_size();
2327
        $current_width = $current_image_size['width'];
2328
        $current_height = $current_image_size['height'];
2329
2330
        if ($current_width < $Max && $current_height < $Max) {
2331
            return true;
2332
        } elseif ($current_height == '') {
2333
            return false;
2334
        }
2335
2336
        // Resize according to height.
2337
        if ($Dimension == "height") {
2338
            $resize_scale = $current_height / $Max;
2339
            $new_width = ceil($current_width / $resize_scale);
2340
        }
2341
2342
        // Resize according to width
2343
        if ($Dimension == "width") {
2344
            $new_width = $Max;
2345
        }
2346
2347
        // Resize according to height or width, both should not be larger than $Max after resizing.
2348
        if ($Dimension == "any") {
2349
            if ($current_height > $current_width || $current_height == $current_width) {
2350
                $resize_scale = $current_height / $Max;
2351
                $new_width = ceil($current_width / $resize_scale);
2352
            }
2353
            if ($current_height < $current_width) {
2354
                $new_width = $Max;
2355
            }
2356
        }
2357
2358
        $my_image->resize($new_width);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $new_width does not seem to be defined for all execution paths leading up to this point.
Loading history...
2359
        $result = $my_image->send_image($picturePath);
2360
2361
        if ($result) {
2362
            return true;
2363
        }
2364
2365
        return false;
2366
    }
2367
}
2368