Test Setup Failed
Push — master ( 4e700f...c7183e )
by Julito
63:12
created

Question::createAnswersForm()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
nc 1
dl 0
loc 1
c 0
b 0
f 0
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
 * @package chamilo.exercise
16
 */
17
abstract class Question
18
{
19
    public $id;
20
    public $question;
21
    public $description;
22
    public $weighting;
23
    public $position;
24
    public $type;
25
    public $level;
26
    public $picture;
27
    public $exerciseList; // array with the list of exercises which this question is in
28
    public $category_list;
29
    public $parent_id;
30
    public $category;
31
    public $isContent;
32
    public $course;
33
    public $feedback;
34
    public static $typePicture = 'new_question.png';
35
    public static $explanationLangVar = '';
36
    public $question_table_class = 'table table-striped';
37
    public $questionTypeWithFeedback;
38
    public $extra;
39
    public static $questionTypes = array(
40
        UNIQUE_ANSWER => array('unique_answer.class.php', 'UniqueAnswer'),
41
        MULTIPLE_ANSWER => array('multiple_answer.class.php', 'MultipleAnswer'),
42
        FILL_IN_BLANKS => array('fill_blanks.class.php', 'FillBlanks'),
43
        MATCHING => array('matching.class.php', 'Matching'),
44
        FREE_ANSWER => array('freeanswer.class.php', 'FreeAnswer'),
45
        ORAL_EXPRESSION => array('oral_expression.class.php', 'OralExpression'),
46
        HOT_SPOT => array('hotspot.class.php', 'HotSpot'),
47
        HOT_SPOT_DELINEATION => array('hotspot.class.php', 'HotspotDelineation'),
48
        MULTIPLE_ANSWER_COMBINATION => array('multiple_answer_combination.class.php', 'MultipleAnswerCombination'),
49
        UNIQUE_ANSWER_NO_OPTION => array('unique_answer_no_option.class.php', 'UniqueAnswerNoOption'),
50
        MULTIPLE_ANSWER_TRUE_FALSE => array('multiple_answer_true_false.class.php', 'MultipleAnswerTrueFalse'),
51
        MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE => array(
52
            'multiple_answer_combination_true_false.class.php',
53
            'MultipleAnswerCombinationTrueFalse'
54
        ),
55
        GLOBAL_MULTIPLE_ANSWER => array('global_multiple_answer.class.php', 'GlobalMultipleAnswer'),
56
        CALCULATED_ANSWER => array('calculated_answer.class.php', 'CalculatedAnswer'),
57
        UNIQUE_ANSWER_IMAGE => ['UniqueAnswerImage.php', 'UniqueAnswerImage'],
58
        DRAGGABLE => ['Draggable.php', 'Draggable'],
59
        MATCHING_DRAGGABLE => ['MatchingDraggable.php', 'MatchingDraggable'],
60
        //MEDIA_QUESTION => array('media_question.class.php' , 'MediaQuestion')
61
        ANNOTATION => ['Annotation.php', 'Annotation'],
62
        READING_COMPREHENSION => ['ReadingComprehension.php', 'ReadingComprehension']
63
    );
64
65
    /**
66
     * constructor of the class
67
     *
68
     * @author Olivier Brouckaert
69
     */
70
    public function __construct()
71
    {
72
        $this->id = 0;
73
        $this->question = '';
74
        $this->description = '';
75
        $this->weighting = 0;
76
        $this->position = 1;
77
        $this->picture = '';
78
        $this->level = 1;
79
        $this->category = 0;
80
        // This variable is used when loading an exercise like an scenario with
81
        // an special hotspot: final_overlap, final_missing, final_excess
82
        $this->extra = '';
83
        $this->exerciseList = array();
84
        $this->course = api_get_course_info();
85
        $this->category_list = array();
86
        $this->parent_id = 0;
87
        // See BT#12611
88
        $this->questionTypeWithFeedback = [
89
            MATCHING,
90
            MATCHING_DRAGGABLE,
91
            DRAGGABLE,
92
            FILL_IN_BLANKS,
93
            FREE_ANSWER,
94
            ORAL_EXPRESSION,
95
            CALCULATED_ANSWER,
96
            ANNOTATION
97
        ];
98
    }
99
100
    /**
101
     * @return int|null
102
     */
103
    public function getIsContent()
104
    {
105
        $isContent = null;
106
        if (isset($_REQUEST['isContent'])) {
107
            $isContent = intval($_REQUEST['isContent']);
108
        }
109
110
        return $this->isContent = $isContent;
111
    }
112
113
    /**
114
     * Reads question information from the data base
115
     *
116
     * @param int $id - question ID
117
     * @param int $course_id
118
     *
119
     * @return Question
120
     *
121
     * @author Olivier Brouckaert
122
     */
123
    public static function read($id, $course_id = null)
124
    {
125
        $id = intval($id);
126
        if (!empty($course_id)) {
127
            $course_info = api_get_course_info_by_id($course_id);
128
        } else {
129
            $course_info = api_get_course_info();
130
        }
131
132
        $course_id = $course_info['real_id'];
133
134
        if (empty($course_id) || $course_id == -1) {
135
            return false;
136
        }
137
138
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
139
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
140
141
        $sql = "SELECT *
142
                FROM $TBL_QUESTIONS
143
                WHERE c_id = $course_id AND id = $id ";
144
        $result = Database::query($sql);
145
146
        // if the question has been found
147
        if ($object = Database::fetch_object($result)) {
148
            $objQuestion = self::getInstance($object->type);
149
            if (!empty($objQuestion)) {
150
                $objQuestion->id = (int) $id;
151
                $objQuestion->question = $object->question;
152
                $objQuestion->description = $object->description;
153
                $objQuestion->weighting = $object->ponderation;
154
                $objQuestion->position = $object->position;
155
                $objQuestion->type = (int) $object->type;
156
                $objQuestion->picture = $object->picture;
157
                $objQuestion->level = (int) $object->level;
158
                $objQuestion->extra = $object->extra;
159
                $objQuestion->course = $course_info;
160
                $objQuestion->feedback = isset($object->feedback) ? $object->feedback : '';
161
                $objQuestion->category = TestCategory::getCategoryForQuestion($id);
162
163
                $tblQuiz = Database::get_course_table(TABLE_QUIZ_TEST);
164
                $sql = "SELECT DISTINCT q.exercice_id
165
                        FROM $TBL_EXERCISE_QUESTION q
166
                        INNER JOIN $tblQuiz e
167
                        ON e.c_id = q.c_id AND e.id = q.exercice_id
168
                        WHERE
169
                            q.c_id = $course_id AND
170
                            q.question_id = $id AND
171
                            e.active >= 0";
172
173
                $result = Database::query($sql);
174
175
                // fills the array with the exercises which this question is in
176
                if ($result) {
177
                    while ($obj = Database::fetch_object($result)) {
178
                        $objQuestion->exerciseList[] = $obj->exercice_id;
179
                    }
180
                }
181
182
                return $objQuestion;
183
            }
184
        }
185
186
        // question not found
187
        return false;
188
    }
189
190
    /**
191
     * returns the question ID
192
     *
193
     * @author Olivier Brouckaert
194
     *
195
     * @return integer - question ID
196
     */
197
    public function selectId()
198
    {
199
        return $this->id;
200
    }
201
202
    /**
203
     * returns the question title
204
     *
205
     * @author Olivier Brouckaert
206
     * @return string - question title
207
     */
208
    public function selectTitle()
209
    {
210
        if (!api_get_configuration_value('save_titles_as_html')) {
211
            return $this->question;
212
        }
213
214
        return Display::div($this->question, ['style' => 'display: inline-block;']);
215
    }
216
217
    /**
218
     * @param int $itemNumber
219
     * @return string
220
     */
221
    public function getTitleToDisplay($itemNumber)
222
    {
223
        $showQuestionTitleHtml = api_get_configuration_value('save_titles_as_html');
224
225
        $title = $showQuestionTitleHtml ? '' : '<strong>';
226
        $title .= $itemNumber.'. '.$this->selectTitle();
227
        $title .= $showQuestionTitleHtml ? '' : '</strong>';
228
229
        return Display::div(
230
            $title,
231
            ['class' => 'question_title']
232
        );
233
    }
234
235
    /**
236
     * returns the question description
237
     *
238
     * @author Olivier Brouckaert
239
     * @return string - question description
240
     */
241
    public function selectDescription()
242
    {
243
        return $this->description;
244
    }
245
246
    /**
247
     * returns the question weighting
248
     *
249
     * @author Olivier Brouckaert
250
     * @return integer - question weighting
251
     */
252
    public function selectWeighting()
253
    {
254
        return $this->weighting;
255
    }
256
257
    /**
258
     * returns the question position
259
     *
260
     * @author Olivier Brouckaert
261
     * @return integer - question position
262
     */
263
    public function selectPosition()
264
    {
265
        return $this->position;
266
    }
267
268
    /**
269
     * returns the answer type
270
     *
271
     * @author Olivier Brouckaert
272
     * @return integer - answer type
273
     */
274
    public function selectType()
275
    {
276
        return $this->type;
277
    }
278
279
    /**
280
     * returns the level of the question
281
     *
282
     * @author Nicolas Raynaud
283
     * @return integer - level of the question, 0 by default.
284
     */
285
    public function getLevel()
286
    {
287
        return $this->level;
288
    }
289
290
    /**
291
     * returns the picture name
292
     *
293
     * @author Olivier Brouckaert
294
     * @return string - picture name
295
     */
296
    public function selectPicture()
297
    {
298
        return $this->picture;
299
    }
300
301
    /**
302
     * @return string
303
     */
304
    public function selectPicturePath()
305
    {
306
        if (!empty($this->picture)) {
307
            return api_get_path(WEB_COURSE_PATH).$this->course['directory'].'/document/images/'.$this->getPictureFilename();
308
        }
309
310
        return '';
311
    }
312
313
    /**
314
     * @return int|string
315
     */
316
    public function getPictureId()
317
    {
318
        // for backward compatibility
319
        // when in field picture we had the filename not the document id
320
        if (preg_match("/quiz-.*/", $this->picture)) {
321
            return DocumentManager::get_document_id(
322
                $this->course,
323
                $this->selectPicturePath(),
324
                api_get_session_id()
325
            );
326
        }
327
328
        return $this->picture;
329
    }
330
331
    /**
332
     * @param int $courseId
333
     * @param int $sessionId
334
     * @return string
335
     */
336
    public function getPictureFilename($courseId = 0, $sessionId = 0)
337
    {
338
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
339
        $sessionId = empty($sessionId) ? api_get_session_id() : (int) $sessionId;
340
341
        if (empty($courseId)) {
342
            return '';
343
        }
344
        // for backward compatibility
345
        // when in field picture we had the filename not the document id
346
        if (preg_match("/quiz-.*/", $this->picture)) {
347
            return $this->picture;
348
        }
349
350
        $pictureId = $this->getPictureId();
351
        $courseInfo = $this->course;
352
        $documentInfo = DocumentManager::get_document_data_by_id(
353
            $pictureId,
354
            $courseInfo['code'],
355
            false,
356
            $sessionId
357
        );
358
        $documentFilename = '';
359
        if ($documentInfo) {
360
            // document in document/images folder
361
            $documentFilename = pathinfo(
362
                $documentInfo['path'],
363
                PATHINFO_BASENAME
364
            );
365
        }
366
367
        return $documentFilename;
368
    }
369
370
    /**
371
     * returns the array with the exercise ID list
372
     *
373
     * @author Olivier Brouckaert
374
     * @return array - list of exercise ID which the question is in
375
     */
376
    public function selectExerciseList()
377
    {
378
        return $this->exerciseList;
379
    }
380
381
    /**
382
     * returns the number of exercises which this question is in
383
     *
384
     * @author Olivier Brouckaert
385
     * @return integer - number of exercises
386
     */
387
    public function selectNbrExercises()
388
    {
389
        return sizeof($this->exerciseList);
390
    }
391
392
    /**
393
     * changes the question title
394
     *
395
     * @param string $title - question title
396
     *
397
     * @author Olivier Brouckaert
398
     */
399
    public function updateTitle($title)
400
    {
401
        $this->question = $title;
402
    }
403
404
    /**
405
     * @param int $id
406
     */
407
    public function updateParentId($id)
408
    {
409
        $this->parent_id = intval($id);
410
    }
411
412
    /**
413
     * changes the question description
414
     *
415
     * @param string $description - question description
416
     *
417
     * @author Olivier Brouckaert
418
     *
419
     */
420
    public function updateDescription($description)
421
    {
422
        $this->description = $description;
423
    }
424
425
    /**
426
     * changes the question weighting
427
     *
428
     * @param integer $weighting - question weighting
429
     *
430
     * @author Olivier Brouckaert
431
     */
432
    public function updateWeighting($weighting)
433
    {
434
        $this->weighting = $weighting;
435
    }
436
437
    /**
438
     * @param array $category
439
     *
440
     * @author Hubert Borderiou 12-10-2011
441
     *
442
     */
443
    public function updateCategory($category)
444
    {
445
        $this->category = $category;
446
    }
447
448
    /**
449
     * @param int $value
450
     *
451
     * @author Hubert Borderiou 12-10-2011
452
     */
453
    public function updateScoreAlwaysPositive($value)
454
    {
455
        $this->scoreAlwaysPositive = $value;
456
    }
457
458
    /**
459
     * @param int $value
460
     *
461
     * @author Hubert Borderiou 12-10-2011
462
     */
463
    public function updateUncheckedMayScore($value)
464
    {
465
        $this->uncheckedMayScore = $value;
466
    }
467
468
    /**
469
     * Save category of a question
470
     *
471
     * A question can have n categories if category is empty,
472
     * then question has no category then delete the category entry
473
     *
474
     * @param array $category_list
475
     *
476
     * @author Julio Montoya - Adding multiple cat support
477
     */
478
    public function saveCategories($category_list)
479
    {
480
        if (!empty($category_list)) {
481
            $this->deleteCategory();
482
            $TBL_QUESTION_REL_CATEGORY = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
483
484
            // update or add category for a question
485
            foreach ($category_list as $category_id) {
486
                $category_id = intval($category_id);
487
                $question_id = intval($this->id);
488
                $sql = "SELECT count(*) AS nb
489
                        FROM $TBL_QUESTION_REL_CATEGORY
490
                        WHERE
491
                            category_id = $category_id
492
                            AND question_id = $question_id
493
                            AND c_id=".api_get_course_int_id();
494
                $res = Database::query($sql);
495
                $row = Database::fetch_array($res);
496
                if ($row['nb'] > 0) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
497
                    // DO nothing
498
                } else {
499
                    $sql = "INSERT INTO $TBL_QUESTION_REL_CATEGORY (c_id, question_id, category_id)
500
                            VALUES (".api_get_course_int_id().", $question_id, $category_id)";
501
                    Database::query($sql);
502
                }
503
            }
504
        }
505
    }
506
507
    /**
508
     * in this version, a question can only have 1 category
509
     * if category is 0, then question has no category then delete the category entry
510
     * @param int $categoryId
511
     * @return bool
512
     *
513
     * @author Hubert Borderiou 12-10-2011
514
     */
515
    public function saveCategory($categoryId)
516
    {
517
        $courseId = api_get_course_int_id();
518
        if (empty($courseId)) {
519
            return false;
520
        }
521
        if ($categoryId <= 0) {
522
            $this->deleteCategory();
523
        } else {
524
            // update or add category for a question
525
            $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
526
            $categoryId = intval($categoryId);
527
            $question_id = intval($this->id);
528
            $sql = "SELECT count(*) AS nb FROM $table
529
                    WHERE
530
                        question_id = $question_id AND
531
                        c_id = ".$courseId;
532
            $res = Database::query($sql);
533
            $row = Database::fetch_array($res);
534 View Code Duplication
            if ($row['nb'] > 0) {
535
                $sql = "UPDATE $table
536
                        SET category_id = $categoryId
537
                        WHERE
538
                            question_id = $question_id AND
539
                            c_id = ".$courseId;
540
                Database::query($sql);
541
            } else {
542
                $sql = "INSERT INTO $table (c_id, question_id, category_id)
543
                        VALUES (".$courseId.", $question_id, $categoryId)";
544
                Database::query($sql);
545
            }
546
547
            return true;
548
        }
549
    }
550
551
    /**
552
     * @author hubert borderiou 12-10-2011
553
     * delete any category entry for question id
554
     * delete the category for question
555
     */
556 View Code Duplication
    public function deleteCategory()
557
    {
558
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
559
        $question_id = intval($this->id);
560
        $sql = "DELETE FROM $table
561
                WHERE
562
                    question_id = $question_id AND
563
                    c_id = ".api_get_course_int_id();
564
        Database::query($sql);
565
    }
566
567
    /**
568
     * changes the question position
569
     *
570
     * @param integer $position - question position
571
     *
572
     * @author Olivier Brouckaert
573
     */
574
    public function updatePosition($position)
575
    {
576
        $this->position = $position;
577
    }
578
579
    /**
580
     * changes the question level
581
     *
582
     * @param integer $level - question level
583
     *
584
     * @author Nicolas Raynaud
585
     */
586
    public function updateLevel($level)
587
    {
588
        $this->level = $level;
589
    }
590
591
    /**
592
     * changes the answer type. If the user changes the type from "unique answer" to "multiple answers"
593
     * (or conversely) answers are not deleted, otherwise yes
594
     *
595
     * @param integer $type - answer type
596
     *
597
     * @author Olivier Brouckaert
598
     */
599
    public function updateType($type)
600
    {
601
        $TBL_REPONSES = Database::get_course_table(TABLE_QUIZ_ANSWER);
602
        $course_id = $this->course['real_id'];
603
604
        if (empty($course_id)) {
605
            $course_id = api_get_course_int_id();
606
        }
607
        // if we really change the type
608
        if ($type != $this->type) {
609
            // if we don't change from "unique answer" to "multiple answers" (or conversely)
610
            if (
611
                !in_array($this->type, array(UNIQUE_ANSWER, MULTIPLE_ANSWER)) ||
612
                !in_array($type, array(UNIQUE_ANSWER, MULTIPLE_ANSWER))
613
            ) {
614
                // removes old answers
615
                $sql = "DELETE FROM $TBL_REPONSES
616
                        WHERE c_id = $course_id AND question_id = ".intval($this->id);
617
                Database::query($sql);
618
            }
619
620
            $this->type = $type;
621
        }
622
    }
623
624
    /**
625
     * Get default hot spot folder in documents
626
     * @return string
627
     */
628
    public function getHotSpotFolderInCourse()
629
    {
630
        if (empty($this->course) || empty($this->course['directory'])) {
631
            // Stop everything if course is not set.
632
            api_not_allowed();
633
        }
634
635
        $pictureAbsolutePath = api_get_path(SYS_COURSE_PATH).$this->course['directory'].'/document/images/';
636
        $picturePath = basename($pictureAbsolutePath);
637
638
        if (!is_dir($picturePath)) {
639
            create_unexisting_directory(
640
                $this->course,
641
                api_get_user_id(),
642
                0,
643
                0,
644
                0,
645
                dirname($pictureAbsolutePath),
646
                '/'.$picturePath,
647
                $picturePath
648
            );
649
        }
650
651
        return $pictureAbsolutePath;
652
    }
653
654
    /**
655
     * adds a picture to the question
656
     *
657
     * @param string $picture - temporary path of the picture to upload
658
     *
659
     * @return boolean - true if uploaded, otherwise false
660
     *
661
     * @author Olivier Brouckaert
662
     */
663
    public function uploadPicture($picture)
664
    {
665
        $picturePath = $this->getHotSpotFolderInCourse();
666
667
        // if the question has got an ID
668
        if ($this->id) {
669
            $pictureFilename = self::generatePictureName();
670
            $img = new Image($picture);
671
            $img->send_image($picturePath.'/'.$pictureFilename, -1, 'jpg');
672
            $document_id = add_document(
673
                $this->course,
674
                '/images/'.$pictureFilename,
675
                'file',
676
                filesize($picturePath.'/'.$pictureFilename),
677
                $pictureFilename
678
            );
679
680
            if ($document_id) {
681
                $this->picture = $document_id;
682
683
                if (!file_exists($picturePath.'/'.$pictureFilename)) {
684
                    return false;
685
                }
686
687
                api_item_property_update(
688
                    $this->course,
689
                    TOOL_DOCUMENT,
690
                    $document_id,
691
                    'DocumentAdded',
692
                    api_get_user_id()
693
                );
694
695
                $this->resizePicture('width', 800);
696
697
                return true;
698
            }
699
        }
700
701
        return false;
702
    }
703
704
    /**
705
     * return the name for image use in hotspot question
706
     * to be unique, name is quiz-[utc unix timestamp].jpg
707
     * @param string $prefix
708
     * @param string $extension
709
     * @return string
710
     */
711
    public function generatePictureName($prefix = 'quiz-', $extension = 'jpg')
712
    {
713
        // image name is quiz-xxx.jpg in folder images/
714
        $utcTime = time();
715
        return $prefix.$utcTime.'.'.$extension;
716
    }
717
718
    /**
719
     * Resizes a picture || Warning!: can only be called after uploadPicture,
720
     * or if picture is already available in object.
721
     * @param string $Dimension - Resizing happens proportional according to given dimension: height|width|any
722
     * @param integer $Max - Maximum size
723
     *
724
     * @return boolean|null - true if success, false if failed
725
     *
726
     * @author Toon Keppens
727
     */
728
    private function resizePicture($Dimension, $Max)
729
    {
730
        // if the question has an ID
731
        if (!$this->id) {
732
            return false;
733
        }
734
735
        $picturePath = $this->getHotSpotFolderInCourse().'/'.$this->getPictureFilename();
736
737
        // Get dimensions from current image.
738
        $my_image = new Image($picturePath);
739
740
        $current_image_size = $my_image->get_image_size();
741
        $current_width = $current_image_size['width'];
742
        $current_height = $current_image_size['height'];
743
744
        if ($current_width < $Max && $current_height < $Max) {
745
            return true;
746
        } elseif ($current_height == '') {
747
            return false;
748
        }
749
750
        // Resize according to height.
751
        if ($Dimension == "height") {
752
            $resize_scale = $current_height / $Max;
753
            $new_width = ceil($current_width / $resize_scale);
754
        }
755
756
        // Resize according to width
757
        if ($Dimension == "width") {
758
            $new_width = $Max;
759
        }
760
761
        // Resize according to height or width, both should not be larger than $Max after resizing.
762
        if ($Dimension == "any") {
763
            if ($current_height > $current_width || $current_height == $current_width) {
764
                $resize_scale = $current_height / $Max;
765
                $new_width = ceil($current_width / $resize_scale);
766
            }
767
            if ($current_height < $current_width) {
768
                $new_width = $Max;
769
            }
770
        }
771
772
        $my_image->resize($new_width);
773
        $result = $my_image->send_image($picturePath);
774
775
        if ($result) {
776
            return true;
777
        }
778
779
        return false;
780
    }
781
782
    /**
783
     * deletes the picture
784
     *
785
     * @author Olivier Brouckaert
786
     * @return boolean - true if removed, otherwise false
787
     */
788
    public function removePicture()
789
    {
790
        $picturePath = $this->getHotSpotFolderInCourse();
791
792
        // if the question has got an ID and if the picture exists
793
        if ($this->id) {
794
            $picture = $this->picture;
795
            $this->picture = '';
796
797
            return @unlink($picturePath.'/'.$picture) ? true : false;
798
        }
799
800
        return false;
801
    }
802
803
    /**
804
     * Exports a picture to another question
805
     *
806
     * @author Olivier Brouckaert
807
     * @param integer $questionId - ID of the target question
808
     * @param array $courseInfo
809
     * @return boolean - true if copied, otherwise false
810
     */
811
    public function exportPicture($questionId, $courseInfo)
812
    {
813
        if (empty($questionId) || empty($courseInfo)) {
814
            return false;
815
        }
816
817
        $course_id = $courseInfo['real_id'];
818
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
819
        $destination_path = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/images';
820
        $source_path = $this->getHotSpotFolderInCourse();
821
822
        // if the question has got an ID and if the picture exists
823
        if (!$this->id || empty($this->picture)) {
824
            return false;
825
        }
826
827
        $picture = $this->generatePictureName();
828
829
        if (file_exists($source_path.'/'.$this->picture)) {
830
            // for backward compatibility
831
            $result = @copy(
832
                $source_path.'/'.$this->picture,
833
                $destination_path.'/'.$picture
834
            );
835
        } else {
836
            $imageInfo = DocumentManager::get_document_data_by_id(
837
                $this->picture,
838
                $courseInfo['code']
839
            );
840
            if (file_exists($imageInfo['absolute_path'])) {
841
                $result = @copy(
842
                    $imageInfo['absolute_path'],
843
                    $destination_path.'/'.$picture
844
                );
845
            }
846
        }
847
848
        // If copy was correct then add to the database
849
        if (!$result) {
850
            return false;
851
        }
852
853
        $sql = "UPDATE $TBL_QUESTIONS SET
854
                picture = '".Database::escape_string($picture)."'
855
                WHERE c_id = $course_id AND id='".intval($questionId)."'";
856
        Database::query($sql);
857
858
        $documentId = add_document(
859
            $courseInfo,
860
            '/images/'.$picture,
861
            'file',
862
            filesize($destination_path.'/'.$picture),
863
            $picture
864
        );
865
866
        if (!$documentId) {
867
            return false;
868
        }
869
870
        return api_item_property_update(
871
            $courseInfo,
872
            TOOL_DOCUMENT,
873
            $documentId,
874
            'DocumentAdded',
875
            api_get_user_id()
876
        );
877
    }
878
879
    /**
880
     * Saves the picture coming from POST into a temporary file
881
     * Temporary pictures are used when we don't want to save a picture right after a form submission.
882
     * For example, if we first show a confirmation box.
883
     *
884
     * @author Olivier Brouckaert
885
     * @param string $picture - temporary path of the picture to move
886
     * @param string $pictureName - Name of the picture
887
     */
888
    public function setTmpPicture($picture, $pictureName)
889
    {
890
        $picturePath = $this->getHotSpotFolderInCourse();
891
        $pictureName = explode('.', $pictureName);
892
        $Extension = $pictureName[sizeof($pictureName) - 1];
893
894
        // saves the picture into a temporary file
895
        @move_uploaded_file($picture, $picturePath.'/tmp.'.$Extension);
896
    }
897
898
    /**
899
     * Set title
900
     * @param string $title
901
     */
902
    public function setTitle($title)
903
    {
904
        $this->question = $title;
905
    }
906
907
    /**
908
     * Sets extra info
909
     * @param string $extra
910
     */
911
    public function setExtra($extra)
912
    {
913
        $this->extra = $extra;
914
    }
915
916
    /**
917
     * updates the question in the data base
918
     * if an exercise ID is provided, we add that exercise ID into the exercise list
919
     *
920
     * @author Olivier Brouckaert
921
     * @param Exercise $exercise
922
     */
923
    public function save($exercise)
924
    {
925
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
926
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
927
        $em = Database::getManager();
928
        $exerciseId = $exercise->id;
929
930
        $id = $this->id;
931
        $question = $this->question;
932
        $description = $this->description;
933
        $weighting = $this->weighting;
934
        $position = $this->position;
935
        $type = $this->type;
936
        $picture = $this->picture;
937
        $level = $this->level;
938
        $extra = $this->extra;
939
        $c_id = $this->course['real_id'];
940
        $categoryId = $this->category;
941
942
        // question already exists
943
        if (!empty($id)) {
944
            $params = [
945
                'question' => $question,
946
                'description' => $description,
947
                'ponderation' => $weighting,
948
                'position' => $position,
949
                'type' => $type,
950
                'picture' => $picture,
951
                'extra' => $extra,
952
                'level' => $level,
953
            ];
954
            if ($exercise->questionFeedbackEnabled) {
955
                $params['feedback'] = $this->feedback;
956
            }
957
            Database::update(
958
                $TBL_QUESTIONS,
959
                $params,
960
                ['c_id = ? AND id = ?' => [$c_id, $id]]
961
            );
962
            $this->saveCategory($categoryId);
963
964
            if (!empty($exerciseId)) {
965
                api_item_property_update(
966
                    $this->course,
967
                    TOOL_QUIZ,
968
                    $id,
969
                    'QuizQuestionUpdated',
970
                    api_get_user_id()
971
                );
972
            }
973
            if (api_get_setting('search_enabled') == 'true') {
974
                if ($exerciseId != 0) {
975
                    $this->search_engine_edit($exerciseId);
976
                } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
977
                    /**
978
                     * actually there is *not* an user interface for
979
                     * creating questions without a relation with an exercise
980
                     */
981
                }
982
            }
983
        } else {
984
            // creates a new question
985
            $sql = "SELECT max(position)
986
                    FROM $TBL_QUESTIONS as question,
987
                    $TBL_EXERCISE_QUESTION as test_question
988
                    WHERE
989
                        question.id = test_question.question_id AND
990
                        test_question.exercice_id = ".intval($exerciseId)." AND
991
                        question.c_id = $c_id AND
992
                        test_question.c_id = $c_id ";
993
            $result = Database::query($sql);
994
            $current_position = Database::result($result, 0, 0);
995
            $this->updatePosition($current_position + 1);
996
            $position = $this->position;
997
998
            $params = [
999
                'c_id' => $c_id,
1000
                'question' => $question,
1001
                'description' => $description,
1002
                'ponderation' => $weighting,
1003
                'position' => $position,
1004
                'type' => $type,
1005
                'picture'  => $picture,
1006
                'extra' => $extra,
1007
                'level' => $level
1008
            ];
1009
1010
            if ($exercise->questionFeedbackEnabled) {
1011
                $params['feedback'] = $this->feedback;
1012
            }
1013
1014
            $this->id = Database::insert($TBL_QUESTIONS, $params);
1015
1016
            if ($this->id) {
1017
                $sql = "UPDATE $TBL_QUESTIONS SET id = iid WHERE iid = {$this->id}";
1018
                Database::query($sql);
1019
1020
                api_item_property_update(
1021
                    $this->course,
1022
                    TOOL_QUIZ,
1023
                    $this->id,
1024
                    'QuizQuestionAdded',
1025
                    api_get_user_id()
1026
                );
1027
1028
                // If hotspot, create first answer
1029 View Code Duplication
                if ($type == HOT_SPOT || $type == HOT_SPOT_ORDER) {
1030
                    $quizAnswer = new CQuizAnswer();
1031
                    $quizAnswer
1032
                        ->setCId($c_id)
1033
                        ->setQuestionId($this->id)
1034
                        ->setAnswer('')
1035
                        ->setPonderation(10)
1036
                        ->setPosition(1)
1037
                        ->setHotspotCoordinates('0;0|0|0')
1038
                        ->setHotspotType('square');
1039
1040
                    $em->persist($quizAnswer);
1041
                    $em->flush();
1042
1043
                    $id = $quizAnswer->getIid();
1044
1045
                    if ($id) {
1046
                        $quizAnswer
1047
                            ->setId($id)
1048
                            ->setIdAuto($id);
1049
1050
                        $em->merge($quizAnswer);
1051
                        $em->flush();
1052
                    }
1053
                }
1054
1055 View Code Duplication
                if ($type == HOT_SPOT_DELINEATION) {
1056
                    $quizAnswer = new CQuizAnswer();
1057
                    $quizAnswer
1058
                        ->setCId($c_id)
1059
                        ->setQuestionId($this->id)
1060
                        ->setAnswer('')
1061
                        ->setPonderation(10)
1062
                        ->setPosition(1)
1063
                        ->setHotspotCoordinates('0;0|0|0')
1064
                        ->setHotspotType('delineation');
1065
1066
                    $em->persist($quizAnswer);
1067
                    $em->flush();
1068
1069
                    $id = $quizAnswer->getIid();
1070
1071
                    if ($id) {
1072
                        $quizAnswer
1073
                            ->setId($id)
1074
                            ->setIdAuto($id);
1075
1076
                        $em->merge($quizAnswer);
1077
                        $em->flush();
1078
                    }
1079
                }
1080
1081
                if (api_get_setting('search_enabled') == 'true') {
1082
                    if ($exerciseId != 0) {
1083
                        $this->search_engine_edit($exerciseId, true);
1084
                    } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
1085
                        /**
1086
                         * actually there is *not* an user interface for
1087
                         * creating questions without a relation with an exercise
1088
                         */
1089
                    }
1090
                }
1091
            }
1092
        }
1093
1094
        // if the question is created in an exercise
1095
        if ($exerciseId) {
1096
            // adds the exercise into the exercise list of this question
1097
            $this->addToList($exerciseId, true);
1098
        }
1099
    }
1100
1101
    public function search_engine_edit(
1102
        $exerciseId,
1103
        $addQs = false,
1104
        $rmQs = false
1105
    ) {
1106
        // update search engine and its values table if enabled
1107
        if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
1108
            $course_id = api_get_course_id();
1109
            // get search_did
1110
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
1111 View Code Duplication
            if ($addQs || $rmQs) {
1112
                //there's only one row per question on normal db and one document per question on search engine db
1113
                $sql = 'SELECT * FROM %s
1114
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_second_level=%s LIMIT 1';
1115
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
1116
            } else {
1117
                $sql = 'SELECT * FROM %s
1118
                    WHERE course_code=\'%s\' AND tool_id=\'%s\'
1119
                    AND ref_id_high_level=%s AND ref_id_second_level=%s LIMIT 1';
1120
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
1121
            }
1122
            $res = Database::query($sql);
1123
1124
            if (Database::num_rows($res) > 0 || $addQs) {
1125
                require_once(api_get_path(LIBRARY_PATH).'search/ChamiloIndexer.class.php');
1126
                require_once(api_get_path(LIBRARY_PATH).'search/IndexableChunk.class.php');
1127
1128
                $di = new ChamiloIndexer();
1129
                if ($addQs) {
1130
                    $question_exercises = array((int) $exerciseId);
1131
                } else {
1132
                    $question_exercises = array();
1133
                }
1134
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
1135
                $di->connectDb(null, null, $lang);
1136
1137
                // retrieve others exercise ids
1138
                $se_ref = Database::fetch_array($res);
1139
                $se_doc = $di->get_document((int) $se_ref['search_did']);
1140
                if ($se_doc !== false) {
1141
                    if (($se_doc_data = $di->get_document_data($se_doc)) !== false) {
1142
                        $se_doc_data = unserialize($se_doc_data);
1143
                        if (
1144
                            isset($se_doc_data[SE_DATA]['type']) &&
1145
                            $se_doc_data[SE_DATA]['type'] == SE_DOCTYPE_EXERCISE_QUESTION
1146
                        ) {
1147
                            if (
1148
                                isset($se_doc_data[SE_DATA]['exercise_ids']) &&
1149
                                is_array($se_doc_data[SE_DATA]['exercise_ids'])
1150
                            ) {
1151
                                foreach ($se_doc_data[SE_DATA]['exercise_ids'] as $old_value) {
1152
                                    if (!in_array($old_value, $question_exercises)) {
1153
                                        $question_exercises[] = $old_value;
1154
                                    }
1155
                                }
1156
                            }
1157
                        }
1158
                    }
1159
                }
1160
                if ($rmQs) {
1161
                    while (($key = array_search($exerciseId, $question_exercises)) !== false) {
1162
                        unset($question_exercises[$key]);
1163
                    }
1164
                }
1165
1166
                // build the chunk to index
1167
                $ic_slide = new IndexableChunk();
1168
                $ic_slide->addValue("title", $this->question);
1169
                $ic_slide->addCourseId($course_id);
1170
                $ic_slide->addToolId(TOOL_QUIZ);
1171
                $xapian_data = array(
1172
                    SE_COURSE_ID => $course_id,
1173
                    SE_TOOL_ID => TOOL_QUIZ,
1174
                    SE_DATA => array(
1175
                        'type' => SE_DOCTYPE_EXERCISE_QUESTION,
1176
                        'exercise_ids' => $question_exercises,
1177
                        'question_id' => (int) $this->id
1178
                    ),
1179
                    SE_USER => (int) api_get_user_id(),
1180
                );
1181
                $ic_slide->xapian_data = serialize($xapian_data);
1182
                $ic_slide->addValue("content", $this->description);
1183
1184
                //TODO: index answers, see also form validation on question_admin.inc.php
1185
1186
                $di->remove_document((int) $se_ref['search_did']);
1187
                $di->addChunk($ic_slide);
1188
1189
                //index and return search engine document id
1190
                if (!empty($question_exercises)) { // if empty there is nothing to index
1191
                    $did = $di->index();
1192
                    unset($di);
1193
                }
1194
                if ($did || $rmQs) {
1195
                    // save it to db
1196 View Code Duplication
                    if ($addQs || $rmQs) {
1197
                        $sql = "DELETE FROM %s
1198
                            WHERE course_code = '%s' AND tool_id = '%s' AND ref_id_second_level = '%s'";
1199
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
1200
                    } else {
1201
                        $sql = "DELETE FROM %S
1202
                            WHERE
1203
                                course_code = '%s'
1204
                                AND tool_id = '%s'
1205
                                AND tool_id = '%s'
1206
                                AND ref_id_high_level = '%s'
1207
                                AND ref_id_second_level = '%s'";
1208
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
1209
                    }
1210
                    Database::query($sql);
1211
                    if ($rmQs) {
1212
                        if (!empty($question_exercises)) {
1213
                            $sql = "INSERT INTO %s (
1214
                                    id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
1215
                                )
1216
                                VALUES (
1217
                                    NULL, '%s', '%s', %s, %s, %s
1218
                                )";
1219
                            $sql = sprintf(
1220
                                $sql,
1221
                                $tbl_se_ref,
1222
                                $course_id,
1223
                                TOOL_QUIZ,
1224
                                array_shift($question_exercises),
1225
                                $this->id,
1226
                                $did
1227
                            );
1228
                            Database::query($sql);
1229
                        }
1230
                    } else {
1231
                        $sql = "INSERT INTO %s (
1232
                                id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
1233
                            )
1234
                            VALUES (
1235
                                NULL , '%s', '%s', %s, %s, %s
1236
                            )";
1237
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id, $did);
1238
                        Database::query($sql);
1239
                    }
1240
                }
1241
1242
            }
1243
        }
1244
    }
1245
1246
    /**
1247
     * adds an exercise into the exercise list
1248
     *
1249
     * @author Olivier Brouckaert
1250
     * @param integer $exerciseId - exercise ID
1251
     * @param boolean $fromSave - from $this->save() or not
1252
     */
1253
    public function addToList($exerciseId, $fromSave = false)
1254
    {
1255
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1256
        $id = $this->id;
1257
        // checks if the exercise ID is not in the list
1258
        if (!in_array($exerciseId, $this->exerciseList)) {
1259
            $this->exerciseList[] = $exerciseId;
1260
            $new_exercise = new Exercise();
1261
            $new_exercise->read($exerciseId);
1262
            $count = $new_exercise->selectNbrQuestions();
1263
            $count++;
1264
            $sql = "INSERT INTO $exerciseRelQuestionTable (c_id, question_id, exercice_id, question_order)
1265
                    VALUES ({$this->course['real_id']}, ".intval($id).", ".intval($exerciseId).", '$count')";
1266
            Database::query($sql);
1267
1268
            // we do not want to reindex if we had just saved adnd indexed the question
1269
            if (!$fromSave) {
1270
                $this->search_engine_edit($exerciseId, true);
1271
            }
1272
        }
1273
    }
1274
1275
    /**
1276
     * removes an exercise from the exercise list
1277
     *
1278
     * @author Olivier Brouckaert
1279
     * @param integer $exerciseId - exercise ID
1280
     * @return boolean - true if removed, otherwise false
1281
     */
1282
    public function removeFromList($exerciseId)
1283
    {
1284
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1285
        $id = $this->id;
1286
1287
        // searches the position of the exercise ID in the list
1288
        $pos = array_search($exerciseId, $this->exerciseList);
1289
        $course_id = api_get_course_int_id();
1290
1291
        // exercise not found
1292
        if ($pos === false) {
1293
            return false;
1294
        } else {
1295
            // deletes the position in the array containing the wanted exercise ID
1296
            unset($this->exerciseList[$pos]);
1297
            //update order of other elements
1298
            $sql = "SELECT question_order
1299
                    FROM $TBL_EXERCISE_QUESTION
1300
                    WHERE
1301
                        c_id = $course_id
1302
                        AND question_id = ".intval($id)."
1303
                        AND exercice_id = " . intval($exerciseId);
1304
            $res = Database::query($sql);
1305 View Code Duplication
            if (Database::num_rows($res) > 0) {
1306
                $row = Database::fetch_array($res);
1307
                if (!empty($row['question_order'])) {
1308
                    $sql = "UPDATE $TBL_EXERCISE_QUESTION
1309
                        SET question_order = question_order-1
1310
                        WHERE
1311
                            c_id = $course_id
1312
                            AND exercice_id = ".intval($exerciseId)."
1313
                            AND question_order > " . $row['question_order'];
1314
                    Database::query($sql);
1315
                }
1316
            }
1317
1318
            $sql = "DELETE FROM $TBL_EXERCISE_QUESTION
1319
                    WHERE
1320
                        c_id = $course_id
1321
                        AND question_id = ".intval($id)."
1322
                        AND exercice_id = " . intval($exerciseId);
1323
            Database::query($sql);
1324
1325
            return true;
1326
        }
1327
    }
1328
1329
    /**
1330
     * Deletes a question from the database
1331
     * the parameter tells if the question is removed from all exercises (value = 0),
1332
     * or just from one exercise (value = exercise ID)
1333
     *
1334
     * @author Olivier Brouckaert
1335
     * @param integer $deleteFromEx - exercise ID if the question is only removed from one exercise
1336
     */
1337
    public function delete($deleteFromEx = 0)
1338
    {
1339
        $course_id = api_get_course_int_id();
1340
1341
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1342
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1343
        $TBL_REPONSES = Database::get_course_table(TABLE_QUIZ_ANSWER);
1344
        $TBL_QUIZ_QUESTION_REL_CATEGORY = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
1345
1346
        $id = intval($this->id);
1347
1348
        // if the question must be removed from all exercises
1349
        if (!$deleteFromEx) {
1350
            //update the question_order of each question to avoid inconsistencies
1351
            $sql = "SELECT exercice_id, question_order FROM $TBL_EXERCISE_QUESTION
1352
                    WHERE c_id = $course_id AND question_id = ".intval($id)."";
1353
1354
            $res = Database::query($sql);
1355 View Code Duplication
            if (Database::num_rows($res) > 0) {
1356
                while ($row = Database::fetch_array($res)) {
1357
                    if (!empty($row['question_order'])) {
1358
                        $sql = "UPDATE $TBL_EXERCISE_QUESTION
1359
                                SET question_order = question_order-1
1360
                                WHERE
1361
                                    c_id= $course_id
1362
                                    AND exercice_id = ".intval($row['exercice_id'])."
1363
                                    AND question_order > " . $row['question_order'];
1364
                        Database::query($sql);
1365
                    }
1366
                }
1367
            }
1368
1369
            $sql = "DELETE FROM $TBL_EXERCISE_QUESTION
1370
                    WHERE c_id = $course_id AND question_id = ".$id;
1371
            Database::query($sql);
1372
1373
            $sql = "DELETE FROM $TBL_QUESTIONS
1374
                    WHERE c_id = $course_id AND id = ".$id;
1375
            Database::query($sql);
1376
1377
            $sql = "DELETE FROM $TBL_REPONSES
1378
                    WHERE c_id = $course_id AND question_id = ".$id;
1379
            Database::query($sql);
1380
1381
            // remove the category of this question in the question_rel_category table
1382
            $sql = "DELETE FROM $TBL_QUIZ_QUESTION_REL_CATEGORY
1383
                    WHERE 
1384
                        c_id = $course_id AND 
1385
                        question_id = ".$id;
1386
            Database::query($sql);
1387
1388
            api_item_property_update(
1389
                $this->course,
1390
                TOOL_QUIZ,
1391
                $id,
1392
                'QuizQuestionDeleted',
1393
                api_get_user_id()
1394
            );
1395
            $this->removePicture();
1396
1397
        } else {
1398
            // just removes the exercise from the list
1399
            $this->removeFromList($deleteFromEx);
1400
            if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
1401
                // disassociate question with this exercise
1402
                $this->search_engine_edit($deleteFromEx, false, true);
1403
            }
1404
1405
            api_item_property_update(
1406
                $this->course,
1407
                TOOL_QUIZ,
1408
                $id,
1409
                'QuizQuestionDeleted',
1410
                api_get_user_id()
1411
            );
1412
        }
1413
    }
1414
1415
    /**
1416
     * Duplicates the question
1417
     *
1418
     * @author Olivier Brouckaert
1419
     * @param  array   $course_info Course info of the destination course
1420
     * @return false|string     ID of the new question
1421
     */
1422
    public function duplicate($course_info = [])
1423
    {
1424
        if (empty($course_info)) {
1425
            $course_info = $this->course;
1426
        } else {
1427
            $course_info = $course_info;
0 ignored issues
show
Bug introduced by
Why assign $course_info to itself?

This checks looks for cases where a variable has been assigned to itself.

This assignement can be removed without consequences.

Loading history...
1428
        }
1429
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1430
        $TBL_QUESTION_OPTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1431
1432
        $question = $this->question;
1433
        $description = $this->description;
1434
        $weighting = $this->weighting;
1435
        $position = $this->position;
1436
        $type = $this->type;
1437
        $level = intval($this->level);
1438
        $extra = $this->extra;
1439
1440
        // Using the same method used in the course copy to transform URLs
1441
        if ($this->course['id'] != $course_info['id']) {
1442
            $description = DocumentManager::replace_urls_inside_content_html_from_copy_course(
1443
                $description,
1444
                $this->course['code'],
1445
                $course_info['id']
1446
            );
1447
            $question = DocumentManager::replace_urls_inside_content_html_from_copy_course(
1448
                $question,
1449
                $this->course['code'],
1450
                $course_info['id']
1451
            );
1452
        }
1453
1454
        $course_id = $course_info['real_id'];
1455
1456
        // Read the source options
1457
        $options = self::readQuestionOption($this->id, $this->course['real_id']);
1458
1459
        // Inserting in the new course db / or the same course db
1460
        $params = [
1461
            'c_id' => $course_id,
1462
            'question' => $question,
1463
            'description' => $description,
1464
            'ponderation' => $weighting,
1465
            'position' => $position,
1466
            'type' => $type,
1467
            'level' => $level,
1468
            'extra' => $extra
1469
        ];
1470
        $newQuestionId = Database::insert($TBL_QUESTIONS, $params);
1471
1472
        if ($newQuestionId) {
1473
            $sql = "UPDATE $TBL_QUESTIONS 
1474
                    SET id = iid
1475
                    WHERE iid = $newQuestionId";
1476
            Database::query($sql);
1477
1478
            if (!empty($options)) {
1479
                // Saving the quiz_options
1480
                foreach ($options as $item) {
1481
                    $item['question_id'] = $newQuestionId;
1482
                    $item['c_id'] = $course_id;
1483
                    unset($item['id']);
1484
                    unset($item['iid']);
1485
                    $id = Database::insert($TBL_QUESTION_OPTIONS, $item);
1486
                    if ($id) {
1487
                        $sql = "UPDATE $TBL_QUESTION_OPTIONS 
1488
                                SET id = iid
1489
                                WHERE iid = $id";
1490
                        Database::query($sql);
1491
                    }
1492
                }
1493
            }
1494
1495
            // Duplicates the picture of the hotspot
1496
            $this->exportPicture($newQuestionId, $course_info);
1497
        }
1498
1499
        return $newQuestionId;
1500
    }
1501
1502
    /**
1503
     * @return string
1504
     */
1505
    public function get_question_type_name()
1506
    {
1507
        $key = self::$questionTypes[$this->type];
1508
1509
        return get_lang($key[1]);
1510
    }
1511
1512
    /**
1513
     * @param string $type
1514
     * @return null
1515
     */
1516
    public static function get_question_type($type)
1517
    {
1518
        if ($type == ORAL_EXPRESSION && api_get_setting('enable_record_audio') !== 'true') {
1519
            return null;
1520
        }
1521
        return self::$questionTypes[$type];
1522
    }
1523
1524
    /**
1525
     * @return array
1526
     */
1527
    public static function get_question_type_list()
1528
    {
1529
        if (api_get_setting('enable_record_audio') !== 'true') {
1530
            self::$questionTypes[ORAL_EXPRESSION] = null;
1531
            unset(self::$questionTypes[ORAL_EXPRESSION]);
1532
        }
1533
        if (api_get_setting('enable_quiz_scenario') !== 'true') {
1534
            self::$questionTypes[HOT_SPOT_DELINEATION] = null;
1535
            unset(self::$questionTypes[HOT_SPOT_DELINEATION]);
1536
        }
1537
        return self::$questionTypes;
1538
    }
1539
1540
    /**
1541
     * Returns an instance of the class corresponding to the type
1542
     * @param integer $type the type of the question
1543
     * @return $this instance of a Question subclass (or of Questionc class by default)
1544
     */
1545
    public static function getInstance($type)
1546
    {
1547
        if (!is_null($type)) {
1548
            list($file_name, $class_name) = self::get_question_type($type);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to list($file_name, $class_name) is correct as self::get_question_type($type) (which targets Question::get_question_type()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1549
            if (!empty($file_name)) {
1550
                if (class_exists($class_name)) {
1551
                    return new $class_name();
1552
                } else {
1553
                    echo 'Can\'t instanciate class '.$class_name.' of type '.$type;
1554
                }
1555
            }
1556
        }
1557
1558
        return null;
1559
    }
1560
1561
    /**
1562
     * Creates the form to create / edit a question
1563
     * A subclass can redefine this function to add fields...
1564
     * @param FormValidator $form
1565
     * @param Exercise $exercise
1566
     */
1567
    public function createForm(&$form, $exercise)
1568
    {
1569
        echo '<style>
1570
                .media { display:none;}
1571
            </style>';
1572
1573
        // question name
1574 View Code Duplication
        if (api_get_configuration_value('save_titles_as_html')) {
1575
            $editorConfig = ['ToolbarSet' => 'Minimal'];
1576
            $form->addHtmlEditor(
1577
                'questionName',
1578
                get_lang('Question'),
1579
                false,
1580
                false,
1581
                $editorConfig,
1582
                true
1583
            );
1584
        } else {
1585
            $form->addElement('text', 'questionName', get_lang('Question'));
1586
        }
1587
1588
        $form->addRule('questionName', get_lang('GiveQuestion'), 'required');
1589
1590
        // default content
1591
        $isContent = isset($_REQUEST['isContent']) ? intval($_REQUEST['isContent']) : null;
1592
1593
        // Question type
1594
        $answerType = isset($_REQUEST['answerType']) ? intval($_REQUEST['answerType']) : null;
1595
        $form->addElement('hidden', 'answerType', $answerType);
1596
1597
        // html editor
1598
        $editorConfig = array(
1599
            'ToolbarSet' => 'TestQuestionDescription',
1600
            'Height' => '150'
1601
        );
1602
1603
        if (!api_is_allowed_to_edit(null, true)) {
1604
            $editorConfig['UserStatus'] = 'student';
1605
        }
1606
1607
        $form->addButtonAdvancedSettings('advanced_params');
1608
        $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
1609
        $form->addHtmlEditor(
1610
            'questionDescription',
1611
            get_lang('QuestionDescription'),
1612
            false,
1613
            false,
1614
            $editorConfig
1615
        );
1616
1617
        // hidden values
1618
        $my_id = isset($_REQUEST['myid']) ? intval($_REQUEST['myid']) : null;
1619
        $form->addElement('hidden', 'myid', $my_id);
1620
1621
        if ($this->type != MEDIA_QUESTION) {
1622
            // Advanced parameters
1623
            $select_level = self::get_default_levels();
1624
            $form->addElement('select', 'questionLevel', get_lang('Difficulty'), $select_level);
1625
1626
            // Categories
1627
            $tabCat = TestCategory::getCategoriesIdAndName();
1628
            $form->addElement('select', 'questionCategory', get_lang('Category'), $tabCat);
1629
1630
            global $text;
1631
1632
            switch ($this->type) {
1633 View Code Duplication
                case UNIQUE_ANSWER:
1634
                    $buttonGroup = array();
1635
                    $buttonGroup[] = $form->addButtonSave($text, 'submitQuestion', true);
1636
                    $buttonGroup[] = $form->addButton(
1637
                        'convertAnswer',
1638
                        get_lang('ConvertToMultipleAnswer'),
1639
                        'dot-circle-o',
1640
                        'default',
1641
                        null,
1642
                        null,
1643
                        null,
1644
                        true
1645
                    );
1646
                    $form->addGroup($buttonGroup);
1647
                    break;
1648 View Code Duplication
                case MULTIPLE_ANSWER:
1649
                    $buttonGroup = array();
1650
                    $buttonGroup[] = $form->addButtonSave($text, 'submitQuestion', true);
1651
                    $buttonGroup[] = $form->addButton(
1652
                        'convertAnswer',
1653
                        get_lang('ConvertToUniqueAnswer'),
1654
                        'check-square-o',
1655
                        'default',
1656
                        null,
1657
                        null,
1658
                        null,
1659
                        true
1660
                    );
1661
                    $form->addGroup($buttonGroup);
1662
                    break;
1663
            }
1664
1665
            //Medias
1666
            //$course_medias = self::prepare_course_media_select(api_get_course_int_id());
1667
            //$form->addElement('select', 'parent_id', get_lang('AttachToMedia'), $course_medias);
1668
        }
1669
1670
        $form->addElement('html', '</div>');
1671
1672
        if (!isset($_GET['fromExercise'])) {
1673
            switch ($answerType) {
1674
                case 1:
1675
                    $this->question = get_lang('DefaultUniqueQuestion');
1676
                    break;
1677
                case 2:
1678
                    $this->question = get_lang('DefaultMultipleQuestion');
1679
                    break;
1680
                case 3:
1681
                    $this->question = get_lang('DefaultFillBlankQuestion');
1682
                    break;
1683
                case 4:
1684
                    $this->question = get_lang('DefaultMathingQuestion');
1685
                    break;
1686
                case 5:
1687
                    $this->question = get_lang('DefaultOpenQuestion');
1688
                    break;
1689
                case 9:
1690
                    $this->question = get_lang('DefaultMultipleQuestion');
1691
                    break;
1692
            }
1693
        }
1694
1695
        if (!is_null($exercise)) {
1696
            if ($exercise->questionFeedbackEnabled && $this->showFeedback($exercise)) {
1697
                $form->addTextarea('feedback', get_lang('FeedbackIfNotCorrect'));
1698
            }
1699
        }
1700
1701
1702
        // default values
1703
        $defaults = array();
1704
        $defaults['questionName'] = $this->question;
1705
        $defaults['questionDescription'] = $this->description;
1706
        $defaults['questionLevel'] = $this->level;
1707
        $defaults['questionCategory'] = $this->category;
1708
        $defaults['feedback'] = $this->feedback;
1709
1710
        // Came from he question pool
1711
        if (isset($_GET['fromExercise'])) {
1712
            $form->setDefaults($defaults);
1713
        }
1714
1715 View Code Duplication
        if (!empty($_REQUEST['myid'])) {
1716
            $form->setDefaults($defaults);
1717
        } else {
1718
            if ($isContent == 1) {
1719
                $form->setDefaults($defaults);
1720
            }
1721
        }
1722
    }
1723
1724
    /**
1725
     * function which process the creation of questions
1726
     * @param FormValidator $form
1727
     * @param Exercise $exercise
1728
     */
1729
    public function processCreation($form, $exercise)
1730
    {
1731
        $this->updateTitle($form->getSubmitValue('questionName'));
1732
        $this->updateDescription($form->getSubmitValue('questionDescription'));
1733
        $this->updateLevel($form->getSubmitValue('questionLevel'));
1734
        $this->updateCategory($form->getSubmitValue('questionCategory'));
1735
        $this->setFeedback($form->getSubmitValue('feedback'));
1736
1737
        //Save normal question if NOT media
1738
        if ($this->type != MEDIA_QUESTION) {
1739
            $this->save($exercise);
1740
1741
            // modify the exercise
1742
            $exercise->addToList($this->id);
1743
            $exercise->update_question_positions();
1744
        }
1745
    }
1746
1747
    /**
1748
     * abstract function which creates the form to create / edit the answers of the question
1749
     * @param FormValidator $form
1750
     */
1751
    abstract public function createAnswersForm($form);
1752
1753
    /**
1754
     * abstract function which process the creation of answers
1755
     * @param FormValidator $form
1756
     * @param Exercise $exercise
1757
     */
1758
    abstract public function processAnswersCreation($form, $exercise);
1759
1760
    /**
1761
     * Displays the menu of question types
1762
     *
1763
     * @param Exercise $objExercise
1764
     */
1765
    public static function display_type_menu($objExercise)
1766
    {
1767
        $feedback_type = $objExercise->feedback_type;
1768
        $exerciseId = $objExercise->id;
1769
1770
        // 1. by default we show all the question types
1771
        $question_type_custom_list = self::get_question_type_list();
1772
1773
        if (!isset($feedback_type)) {
1774
            $feedback_type = 0;
1775
        }
1776
1777
        if ($feedback_type == 1) {
1778
            //2. but if it is a feedback DIRECT we only show the UNIQUE_ANSWER type that is currently available
1779
            $question_type_custom_list = array(
1780
                UNIQUE_ANSWER => self::$questionTypes[UNIQUE_ANSWER],
1781
                HOT_SPOT_DELINEATION => self::$questionTypes[HOT_SPOT_DELINEATION]
1782
            );
1783
        } else {
1784
            unset($question_type_custom_list[HOT_SPOT_DELINEATION]);
1785
        }
1786
1787
        echo '<div class="well">';
1788
        echo '<ul class="question_menu">';
1789
1790
        foreach ($question_type_custom_list as $i => $a_type) {
1791
            // include the class of the type
1792
            require_once $a_type[0];
1793
            // get the picture of the type and the langvar which describes it
1794
            $img = $explanation = '';
1795
            eval('$img = '.$a_type[1].'::$typePicture;');
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
1796
            eval('$explanation = get_lang('.$a_type[1].'::$explanationLangVar);');
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
1797
            echo '<li>';
1798
            echo '<div class="icon-image">';
1799
            $icon = '<a href="admin.php?'.api_get_cidreq().'&newQuestion=yes&answerType='.$i.'">'.
1800
                Display::return_icon($img, $explanation, null, ICON_SIZE_BIG).'</a>';
1801
1802
            if ($objExercise->force_edit_exercise_in_lp === false) {
1803
                if ($objExercise->exercise_was_added_in_lp == true) {
1804
                    $img = pathinfo($img);
1805
                    $img = $img['filename'].'_na.'.$img['extension'];
1806
                    $icon = Display::return_icon($img, $explanation, null, ICON_SIZE_BIG);
1807
                }
1808
            }
1809
1810
            echo $icon;
1811
            echo '</div>';
1812
            echo '</li>';
1813
        }
1814
1815
        echo '<li>';
1816
        echo '<div class="icon_image_content">';
1817
        if ($objExercise->exercise_was_added_in_lp == true) {
1818
            echo Display::return_icon('database_na.png', get_lang('GetExistingQuestion'), null, ICON_SIZE_BIG);
1819
        } else {
1820
            if ($feedback_type == 1) {
1821
                echo $url = "<a href=\"question_pool.php?".api_get_cidreq()."&type=1&fromExercise=$exerciseId\">";
1822
            } else {
1823
                echo $url = '<a href="question_pool.php?'.api_get_cidreq().'&fromExercise='.$exerciseId.'">';
1824
            }
1825
            echo Display::return_icon('database.png', get_lang('GetExistingQuestion'), null, ICON_SIZE_BIG);
1826
        }
1827
        echo '</a>';
1828
        echo '</div></li>';
1829
        echo '</ul>';
1830
        echo '</div>';
1831
    }
1832
1833
    /**
1834
     * @param int $question_id
1835
     * @param string $name
1836
     * @param int $course_id
1837
     * @param int $position
1838
     * @return false|string
1839
     */
1840
    public static function saveQuestionOption($question_id, $name, $course_id, $position = 0)
1841
    {
1842
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1843
        $params['question_id'] = intval($question_id);
1844
        $params['name'] = $name;
1845
        $params['position'] = $position;
1846
        $params['c_id'] = $course_id;
1847
        $result = self::readQuestionOption($question_id, $course_id);
1848
        $last_id = Database::insert($table, $params);
1849
        if ($last_id) {
1850
            $sql = "UPDATE $table SET id = iid WHERE iid = $last_id";
1851
            Database::query($sql);
1852
        }
1853
1854
        return $last_id;
1855
    }
1856
1857
    /**
1858
     * @param int $question_id
1859
     * @param int $course_id
1860
     */
1861
    public static function deleteAllQuestionOptions($question_id, $course_id)
1862
    {
1863
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1864
        Database::delete(
1865
            $table,
1866
            array(
1867
                'c_id = ? AND question_id = ?' => array(
1868
                    $course_id,
1869
                    $question_id
1870
                )
1871
            )
1872
        );
1873
    }
1874
1875
    /**
1876
     * @param int $id
1877
     * @param array $params
1878
     * @param int $course_id
1879
     * @return bool|int
1880
     */
1881
    public static function updateQuestionOption($id, $params, $course_id)
1882
    {
1883
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1884
        $result = Database::update(
1885
            $table,
1886
            $params,
1887
            array('c_id = ? AND id = ?' => array($course_id, $id))
1888
        );
1889
        return $result;
1890
    }
1891
1892
    /**
1893
     * @param int $question_id
1894
     * @param int $course_id
1895
     * @return array
1896
     */
1897
    static function readQuestionOption($question_id, $course_id)
1898
    {
1899
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1900
        $result = Database::select(
1901
            '*',
1902
            $table,
1903
            array(
1904
                'where' => array(
1905
                    'c_id = ? AND question_id = ?' => array(
1906
                        $course_id,
1907
                        $question_id
1908
                    )
1909
                ),
1910
                'order' => 'id ASC'
1911
            )
1912
        );
1913
1914
        return $result;
1915
    }
1916
1917
    /**
1918
     * Shows question title an description
1919
     *
1920
     * @param Exercise $exercise
1921
     * @param int $counter
1922
     * @param array $score
1923
     * @return string HTML string with the header of the question (before the answers table)
1924
     */
1925
    public function return_header($exercise, $counter = null, $score = [])
1926
    {
1927
        $counter_label = '';
1928
        if (!empty($counter)) {
1929
            $counter_label = intval($counter);
1930
        }
1931
        $score_label = get_lang('Wrong');
1932
        $class = 'error';
1933
        if ($score['pass'] == true) {
1934
            $score_label = get_lang('Correct');
1935
            $class = 'success';
1936
        }
1937
1938
        if (in_array($this->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION])) {
1939
            $score['revised'] = isset($score['revised']) ? $score['revised'] : false;
1940
            if ($score['revised'] == true) {
1941
                $score_label = get_lang('Revised');
1942
                $class = '';
1943
            } else {
1944
                $score_label = get_lang('NotRevised');
1945
                $class = 'warning';
1946
                $weight = float_format($score['weight'], 1);
1947
                $score['result'] = " ? / ".$weight;
1948
            }
1949
        }
1950
1951
        // display question category, if any
1952
        $header = TestCategory::returnCategoryAndTitle($this->id);
1953
        $show_media = null;
1954
        if ($show_media) {
1955
            $header .= $this->show_media_content();
1956
        }
1957
1958
        $header .= Display::page_subheader2($counter_label.". ".$this->question);
1959
        $header .= ExerciseLib::getQuestionRibbon($class, $score_label, $score['result']);
1960
        if ($this->type != READING_COMPREHENSION) {
1961
            // Do not show the description (the text to read) if the question is of type READING_COMPREHENSION
1962
            $header .= Display::div($this->description, array('class' => 'question_description'));
1963
        } else {
1964
            if ($score['pass'] == true) {
1965
                $message = Display::div(
1966
                    sprintf(
1967
                        get_lang('ReadingQuestionCongratsSpeedXReachedForYWords'),
1968
                        ReadingComprehension::$speeds[$this->level],
1969
                        $this->getWordsCount()
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Question as the method getWordsCount() does only exist in the following sub-classes of Question: ReadingComprehension. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1970
                    )
1971
                );
1972
            } else {
1973
                $message = Display::div(
1974
                    sprintf(
1975
                        get_lang('ReadingQuestionCongratsSpeedXNotReachedForYWords'),
1976
                        ReadingComprehension::$speeds[$this->level],
1977
                        $this->getWordsCount()
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Question as the method getWordsCount() does only exist in the following sub-classes of Question: ReadingComprehension. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1978
                    )
1979
                );
1980
            }
1981
            $header .= $message.'<br />';
1982
        }
1983
1984
        if (isset($score['pass']) && $score['pass'] === false) {
1985
            if ($this->showFeedback($exercise)) {
1986
                $header .= $this->returnFormatFeedback();
1987
            }
1988
        }
1989
1990
        return $header;
1991
    }
1992
1993
    /**
1994
     * Create a question from a set of parameters
1995
     * @param   int     Quiz ID
1996
     * @param   string  Question name
1997
     * @param   int     Maximum result for the question
1998
     * @param   int     Type of question (see constants at beginning of question.class.php)
1999
     * @param   int     Question level/category
2000
     * @param string $quiz_id
2001
     */
2002
    public function create_question(
2003
        $quiz_id,
2004
        $question_name,
2005
        $question_description = '',
2006
        $max_score = 0,
2007
        $type = 1,
2008
        $level = 1
2009
    ) {
2010
        $course_id = api_get_course_int_id();
2011
        $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2012
        $tbl_quiz_rel_question = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
2013
2014
        $quiz_id = intval($quiz_id);
2015
        $max_score = (float) $max_score;
2016
        $type = intval($type);
2017
        $level = intval($level);
2018
2019
        // Get the max position
2020
        $sql = "SELECT max(position) as max_position
2021
                FROM $tbl_quiz_question q 
2022
                INNER JOIN $tbl_quiz_rel_question r
2023
                ON
2024
                    q.id = r.question_id AND
2025
                    exercice_id = $quiz_id AND
2026
                    q.c_id = $course_id AND
2027
                    r.c_id = $course_id";
2028
        $rs_max = Database::query($sql);
2029
        $row_max = Database::fetch_object($rs_max);
2030
        $max_position = $row_max->max_position + 1;
2031
2032
        $params = [
2033
            'c_id' => $course_id,
2034
            'question' => $question_name,
2035
            'description' => $question_description,
2036
            'ponderation' => $max_score,
2037
            'position' => $max_position,
2038
            'type' => $type,
2039
            'level' => $level,
2040
        ];
2041
        $question_id = Database::insert($tbl_quiz_question, $params);
2042
2043
        if ($question_id) {
2044
            $sql = "UPDATE $tbl_quiz_question SET id = iid WHERE iid = $question_id";
2045
            Database::query($sql);
2046
2047
            // Get the max question_order
2048
            $sql = "SELECT max(question_order) as max_order
2049
                    FROM $tbl_quiz_rel_question
2050
                    WHERE c_id = $course_id AND exercice_id = $quiz_id ";
2051
            $rs_max_order = Database::query($sql);
2052
            $row_max_order = Database::fetch_object($rs_max_order);
2053
            $max_order = $row_max_order->max_order + 1;
2054
            // Attach questions to quiz
2055
            $sql = "INSERT INTO $tbl_quiz_rel_question (c_id, question_id, exercice_id, question_order)
2056
                    VALUES($course_id, $question_id, $quiz_id, $max_order)";
2057
            Database::query($sql);
2058
        }
2059
2060
        return $question_id;
2061
    }
2062
2063
    /**
2064
     * @return array the image filename of the question type
2065
     */
2066
    public function get_type_icon_html()
2067
    {
2068
        $type = $this->selectType();
2069
        $tabQuestionList = self::get_question_type_list(); // [0]=file to include [1]=type name
2070
2071
        require_once $tabQuestionList[$type][0];
2072
2073
        $img = $explanation = null;
2074
        eval('$img = '.$tabQuestionList[$type][1].'::$typePicture;');
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
2075
        eval('$explanation = get_lang('.$tabQuestionList[$type][1].'::$explanationLangVar);');
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
2076
        return array($img, $explanation);
2077
    }
2078
2079
    /**
2080
     * Get course medias
2081
     * @param int course id
2082
     * @param integer $course_id
2083
     *
2084
     * @return array
2085
     */
2086
    static function get_course_medias(
2087
        $course_id,
2088
        $start = 0,
2089
        $limit = 100,
2090
        $sidx = "question",
2091
        $sord = "ASC",
2092
        $where_condition = array()
2093
    ) {
2094
        $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2095
        $default_where = array('c_id = ? AND parent_id = 0 AND type = ?' => array($course_id, MEDIA_QUESTION));
2096
        $result = Database::select(
2097
            '*',
2098
            $table_question,
2099
            array(
2100
                'limit' => " $start, $limit",
2101
                'where' => $default_where,
2102
                'order' => "$sidx $sord"
2103
            )
2104
        );
2105
2106
        return $result;
2107
    }
2108
2109
    /**
2110
     * Get count course medias
2111
     * @param int course id
2112
     *
2113
     * @return int
2114
     */
2115
    static function get_count_course_medias($course_id)
2116
    {
2117
        $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2118
        $result = Database::select(
2119
            'count(*) as count',
2120
            $table_question,
2121
            array(
2122
                'where' => array(
2123
                    'c_id = ? AND parent_id = 0 AND type = ?' => array(
2124
                        $course_id,
2125
                        MEDIA_QUESTION,
2126
                    ),
2127
                )
2128
            ),
2129
            'first'
2130
        );
2131
2132
        if ($result && isset($result['count'])) {
2133
            return $result['count'];
2134
        }
2135
        return 0;
2136
    }
2137
2138
    /**
2139
     * @param int $course_id
2140
     * @return array
2141
     */
2142
    public static function prepare_course_media_select($course_id)
2143
    {
2144
        $medias = self::get_course_medias($course_id);
2145
        $media_list = array();
2146
        $media_list[0] = get_lang('NoMedia');
2147
2148
        if (!empty($medias)) {
2149
            foreach ($medias as $media) {
2150
                $media_list[$media['id']] = empty($media['question']) ? get_lang('Untitled') : $media['question'];
2151
            }
2152
        }
2153
        return $media_list;
2154
    }
2155
2156
    /**
2157
     * @return integer[]
2158
     */
2159
    public static function get_default_levels()
2160
    {
2161
        $select_level = array(
2162
            1 => 1,
2163
            2 => 2,
2164
            3 => 3,
2165
            4 => 4,
2166
            5 => 5
2167
        );
2168
2169
        return $select_level;
2170
    }
2171
2172
    /**
2173
     * @return string
2174
     */
2175
    public function show_media_content()
2176
    {
2177
        $html = '';
2178
        if ($this->parent_id != 0) {
2179
            $parent_question = self::read($this->parent_id);
2180
            $html = $parent_question->show_media_content();
2181
        } else {
2182
            $html .= Display::page_subheader($this->selectTitle());
2183
            $html .= $this->selectDescription();
2184
        }
2185
        return $html;
2186
    }
2187
2188
    /**
2189
     * Swap between unique and multiple type answers
2190
     * @return UniqueAnswer|MultipleAnswer
2191
     */
2192
    public function swapSimpleAnswerTypes()
2193
    {
2194
        $oppositeAnswers = array(
2195
            UNIQUE_ANSWER => MULTIPLE_ANSWER,
2196
            MULTIPLE_ANSWER => UNIQUE_ANSWER
2197
        );
2198
        $this->type = $oppositeAnswers[$this->type];
2199
        Database::update(
2200
            Database::get_course_table(TABLE_QUIZ_QUESTION),
2201
            array('type' => $this->type),
2202
            array('c_id = ? AND id = ?' => array($this->course['real_id'], $this->id))
2203
        );
2204
        $answerClasses = array(
2205
            UNIQUE_ANSWER => 'UniqueAnswer',
2206
            MULTIPLE_ANSWER => 'MultipleAnswer'
2207
        );
2208
        $swappedAnswer = new $answerClasses[$this->type];
2209
        foreach ($this as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $this of type this<Question> is not traversable.
Loading history...
2210
            $swappedAnswer->$key = $value;
2211
        }
2212
        return $swappedAnswer;
2213
    }
2214
2215
    /**
2216
     * @param array $score
2217
     * @return bool
2218
     */
2219
    public function isQuestionWaitingReview($score)
2220
    {
2221
        $isReview = false;
2222
        if (!empty($score['comments']) || $score['score'] > 0) {
2223
            $isReview = true;
2224
        }
2225
2226
        return $isReview;
2227
    }
2228
2229
    /**
2230
     * @param string $value
2231
     */
2232
    public function setFeedback($value)
2233
    {
2234
        $this->feedback = $value;
2235
    }
2236
2237
    /**
2238
     * @param Exercise $exercise
2239
     * @return bool
2240
     */
2241
    public function showFeedback($exercise)
2242
    {
2243
        return
2244
            in_array($this->type, $this->questionTypeWithFeedback) &&
2245
            $exercise->feedback_type != EXERCISE_FEEDBACK_TYPE_EXAM;
2246
    }
2247
2248
    /**
2249
     * @return string
2250
     */
2251
    public function returnFormatFeedback()
2252
    {
2253
        return Display::return_message($this->feedback, 'normal', false);
2254
    }
2255
}
2256