Question   F
last analyzed

Complexity

Total Complexity 323

Size/Duplication

Total Lines 2840
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 323
eloc 1335
c 1
b 0
f 0
dl 0
loc 2840
rs 0.8

78 Methods

Rating   Name   Duplication   Size   Complexity  
A getTitleToDisplay() 0 20 5
F search_engine_edit() 0 138 27
A uploadPicture() 0 39 4
A updateDescription() 0 3 1
C read() 0 77 15
A deleteCategory() 0 15 4
A selectId() 0 3 1
A selectPosition() 0 3 1
A setTitle() 0 3 1
A selectExerciseList() 0 3 1
A selectWeighting() 0 3 1
A selectPicturePath() 0 7 2
A selectTitle() 0 7 2
A selectNbrExercises() 0 3 1
A selectPicture() 0 3 1
A saveCategories() 0 24 4
B exportPicture() 0 71 10
A updateCategory() 0 3 1
A getPictureFilename() 0 32 6
A setTmpPicture() 0 8 1
A __construct() 0 32 1
A getPictureId() 0 13 2
A getHotSpotFolderInCourse() 0 29 5
A updateTitle() 0 3 1
A generatePictureName() 0 6 1
A updateType() 0 17 4
A setMandatory() 0 3 1
A removePicture() 0 13 3
A getLevel() 0 3 1
C save() 0 169 13
A removeFromList() 0 45 5
B getShowHideConfiguration() 0 32 6
A updateLevel() 0 3 1
A selectDescription() 0 3 1
A setExtra() 0 3 1
A selectType() 0 3 1
A updateScoreAlwaysPositive() 0 3 1
A updateWeighting() 0 3 1
A updateUncheckedMayScore() 0 3 1
A addToList() 0 21 5
B saveCategory() 0 50 8
A updateParentId() 0 3 1
A updatePosition() 0 3 1
A getIsContent() 0 8 2
C delete() 0 114 11
A getInstance() 0 15 4
B duplicate() 0 77 7
A getTypePicture() 0 3 1
A deleteAllQuestionOptions() 0 8 1
F return_header() 0 159 37
A getExerciseListWhereQuestionExists() 0 13 1
A show_media_content() 0 12 2
A saveQuestionOption() 0 11 1
C resizePicture() 0 52 12
A getExplanation() 0 3 1
A getMasterQuizForQuestion() 0 22 3
A updateQuestionOption() 0 15 2
A countQuizzesUsingQuestion() 0 21 3
A isQuestionWaitingReview() 0 10 4
A get_question_type_name() 0 5 1
A returnFormatFeedback() 0 3 1
A getCountExercise() 0 13 1
A create_question() 0 55 2
A countAnswers() 0 10 1
A getQuestionTypeList() 0 12 3
F createForm() 0 261 34
A setFeedback() 0 3 1
A showFeedback() 0 9 3
A existsInAnotherExercise() 0 5 1
A processCreation() 0 27 4
A swapSimpleAnswerTypes() 0 29 2
A get_count_course_medias() 0 22 3
A get_question_type() 0 7 3
A prepare_course_media_select() 0 13 4
C displayTypeMenu() 0 89 10
A get_default_levels() 0 8 1
A get_course_medias() 0 23 1
A readQuestionOption() 0 14 1

How to fix   Complexity   

Complex Class

Complex classes like Question often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Question, and based on these observations, apply Extract Interface, too.

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