Passed
Push — webservicelpcreate ( d8cb35 )
by
unknown
13:48
created

Question::selectId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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