Passed
Pull Request — 1.11.x (#6306)
by Yannick
18:44
created

Question::saveCategories()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
2919
        return Display::label($title, 'warning');
2920
    }
2921
2922
    /**
2923
     * @param $exerciseId
2924
     * @param $mediaId
2925
     * @return array|bool
2926
     */
2927
    public function getQuestionsPerMediaWithCategories($exerciseId, $mediaId)
2928
    {
2929
        $exerciseId = intval($exerciseId);
2930
        $mediaId = intval($mediaId);
2931
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
2932
        $questionRelExerciseTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
2933
2934
        $sql = "SELECT * FROM $questionTable q INNER JOIN $questionRelExerciseTable r ON (q.iid = r.question_id)
2935
                WHERE (r.exercise_id = $exerciseId AND q.parent_id = $mediaId) ";
2936
2937
        $result = Database::query($sql);
2938
        if (Database::num_rows($result)) {
2939
            return Database::store_result($result);
2940
        }
2941
        return false;
2942
    }
2943
    /**
2944
     * @param int $exerciseId
2945
     * @param int $mediaId
2946
     * @return array
2947
     */
2948
    public function getQuestionCategoriesOfMediaQuestions($exerciseId, $mediaId)
2949
    {
2950
        $questions = $this->getQuestionsPerMediaWithCategories($exerciseId, $mediaId);
2951
        $questionCategoryList = array();
2952
        if (!empty($questions)) {
2953
            foreach ($questions as $question) {
2954
                $categories = TestCategory::getCategoryForQuestionWithCategoryData($question['iid']);
2955
                if (!empty($categories)) {
2956
                    foreach ($categories as $category) {
2957
                        $questionCategoryList[$question['iid']][] = $category['iid'];
2958
                    }
2959
                }
2960
            }
2961
        }
2962
        return $questionCategoryList;
2963
    }
2964
2965
    /**
2966
     * @param int $exerciseId
2967
     * @param int $mediaId
2968
     * @return array
2969
     */
2970
    public function allQuestionWithMediaHaveTheSameCategory($exerciseId, $mediaId, $categoryListToCompare = array(), $ignoreQuestionId = null, $returnCategoryId = false)
2971
    {
2972
        $questions = $this->getQuestionCategoriesOfMediaQuestions($exerciseId, $mediaId);
2973
        $result = false;
2974
        $categoryId = null;
2975
        if (empty($questions)) {
2976
            $result = true;
2977
        } else {
2978
            $tempArray = array();
2979
            foreach ($questions as $categories) {
2980
                $diff = array_diff($tempArray, $categories);
2981
                $categoryId = $categories[0];
2982
                $tempArray = $categories;
2983
                if (empty($diff)) {
2984
                    $result = true;
2985
                    continue;
2986
                } else {
2987
                    $result = false;
2988
                    break;
2989
                }
2990
            }
2991
        }
2992
        if (isset($categoryListToCompare) && !empty($categoryListToCompare)) {
2993
            $result = false;
2994
            foreach ($questions as $questionId => $categories) {
2995
                if ($ignoreQuestionId == $questionId) {
2996
                    continue;
2997
                }
2998
                $diff = array_diff($categoryListToCompare, $categories);
2999
                $categoryId = $categories[0];
3000
                if (empty($diff)) {
3001
                    $result = true;
3002
                    continue;
3003
                } else {
3004
                    $result = false;
3005
                    break;
3006
                }
3007
            }
3008
        }
3009
3010
        if ($returnCategoryId) {
3011
            return $categoryId;
3012
        }
3013
        return $result;
3014
    }
3015
}
3016