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

Question::resizePicture()   C

Complexity

Conditions 12
Paths 43

Size

Total Lines 52
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 27
nc 43
nop 2
dl 0
loc 52
rs 5.9842
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/* For licensing terms, see /license.txt */
3
4
use Chamilo\CourseBundle\Entity\CQuizAnswer;
5
6
/**
7
 * Class Question.
8
 *
9
 * This class allows to instantiate an object of type Question
10
 *
11
 * @author Olivier Brouckaert, original author
12
 * @author Patrick Cool, LaTeX support
13
 * @author Julio Montoya <[email protected]> lot of bug fixes
14
 * @author [email protected] - add question categories
15
 *
16
 * @package chamilo.exercise
17
 */
18
abstract class Question
19
{
20
    public $id;
21
    public $question;
22
    public $description;
23
    public $weighting;
24
    public $position;
25
    public $type;
26
    public $level;
27
    public $picture;
28
    public $exerciseList; // array with the list of exercises which this question is in
29
    public $category_list;
30
    public $parent_id;
31
    public $category;
32
    public $isContent;
33
    public $course;
34
    public $feedback;
35
    public static $typePicture = 'new_question.png';
36
    public static $explanationLangVar = '';
37
    public $question_table_class = 'table table-striped';
38
    public $questionTypeWithFeedback;
39
    public $extra;
40
    public $export = false;
41
    public static $questionTypes = [
42
        UNIQUE_ANSWER => ['unique_answer.class.php', 'UniqueAnswer'],
43
        MULTIPLE_ANSWER => ['multiple_answer.class.php', 'MultipleAnswer'],
44
        FILL_IN_BLANKS => ['fill_blanks.class.php', 'FillBlanks'],
45
        MATCHING => ['matching.class.php', 'Matching'],
46
        FREE_ANSWER => ['freeanswer.class.php', 'FreeAnswer'],
47
        ORAL_EXPRESSION => ['oral_expression.class.php', 'OralExpression'],
48
        HOT_SPOT => ['hotspot.class.php', 'HotSpot'],
49
        HOT_SPOT_DELINEATION => ['hotspot.class.php', 'HotspotDelineation'],
50
        MULTIPLE_ANSWER_COMBINATION => ['multiple_answer_combination.class.php', 'MultipleAnswerCombination'],
51
        UNIQUE_ANSWER_NO_OPTION => ['unique_answer_no_option.class.php', 'UniqueAnswerNoOption'],
52
        MULTIPLE_ANSWER_TRUE_FALSE => ['multiple_answer_true_false.class.php', 'MultipleAnswerTrueFalse'],
53
        MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE => [
54
            'multiple_answer_combination_true_false.class.php',
55
            'MultipleAnswerCombinationTrueFalse',
56
        ],
57
        GLOBAL_MULTIPLE_ANSWER => ['global_multiple_answer.class.php', 'GlobalMultipleAnswer'],
58
        CALCULATED_ANSWER => ['calculated_answer.class.php', 'CalculatedAnswer'],
59
        UNIQUE_ANSWER_IMAGE => ['UniqueAnswerImage.php', 'UniqueAnswerImage'],
60
        DRAGGABLE => ['Draggable.php', 'Draggable'],
61
        MATCHING_DRAGGABLE => ['MatchingDraggable.php', 'MatchingDraggable'],
62
        //MEDIA_QUESTION => array('media_question.class.php' , 'MediaQuestion')
63
        ANNOTATION => ['Annotation.php', 'Annotation'],
64
        READING_COMPREHENSION => ['ReadingComprehension.php', 'ReadingComprehension'],
65
    ];
66
67
    /**
68
     * constructor of the class.
69
     *
70
     * @author Olivier Brouckaert
71
     */
72
    public function __construct()
73
    {
74
        $this->id = 0;
75
        $this->question = '';
76
        $this->description = '';
77
        $this->weighting = 0;
78
        $this->position = 1;
79
        $this->picture = '';
80
        $this->level = 1;
81
        $this->category = 0;
82
        // This variable is used when loading an exercise like an scenario with
83
        // an special hotspot: final_overlap, final_missing, final_excess
84
        $this->extra = '';
85
        $this->exerciseList = [];
86
        $this->course = api_get_course_info();
87
        $this->category_list = [];
88
        $this->parent_id = 0;
89
        // See BT#12611
90
        $this->questionTypeWithFeedback = [
91
            MATCHING,
92
            MATCHING_DRAGGABLE,
93
            DRAGGABLE,
94
            FILL_IN_BLANKS,
95
            FREE_ANSWER,
96
            ORAL_EXPRESSION,
97
            CALCULATED_ANSWER,
98
            ANNOTATION,
99
        ];
100
    }
101
102
    /**
103
     * @return int|null
104
     */
105
    public function getIsContent()
106
    {
107
        $isContent = null;
108
        if (isset($_REQUEST['isContent'])) {
109
            $isContent = intval($_REQUEST['isContent']);
110
        }
111
112
        return $this->isContent = $isContent;
113
    }
114
115
    /**
116
     * Reads question information from the data base.
117
     *
118
     * @param int $id        - question ID
119
     * @param int $course_id
120
     *
121
     * @return Question
122
     *
123
     * @author Olivier Brouckaert
124
     */
125
    public static function read($id, $course_id = null)
126
    {
127
        $id = intval($id);
128
        if (!empty($course_id)) {
129
            $course_info = api_get_course_info_by_id($course_id);
130
        } else {
131
            $course_info = api_get_course_info();
132
        }
133
134
        $course_id = $course_info['real_id'];
135
136
        if (empty($course_id) || $course_id == -1) {
137
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type Question.
Loading history...
138
        }
139
140
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
141
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
142
143
        $sql = "SELECT *
144
                FROM $TBL_QUESTIONS
145
                WHERE c_id = $course_id AND id = $id ";
146
        $result = Database::query($sql);
147
148
        // if the question has been found
149
        if ($object = Database::fetch_object($result)) {
150
            $objQuestion = self::getInstance($object->type);
151
            if (!empty($objQuestion)) {
152
                $objQuestion->id = (int) $id;
153
                $objQuestion->question = $object->question;
154
                $objQuestion->description = $object->description;
155
                $objQuestion->weighting = $object->ponderation;
156
                $objQuestion->position = $object->position;
157
                $objQuestion->type = (int) $object->type;
158
                $objQuestion->picture = $object->picture;
159
                $objQuestion->level = (int) $object->level;
160
                $objQuestion->extra = $object->extra;
161
                $objQuestion->course = $course_info;
162
                $objQuestion->feedback = isset($object->feedback) ? $object->feedback : '';
163
                $objQuestion->category = TestCategory::getCategoryForQuestion($id, $course_id);
164
165
                $tblQuiz = Database::get_course_table(TABLE_QUIZ_TEST);
166
                $sql = "SELECT DISTINCT q.exercice_id
167
                        FROM $TBL_EXERCISE_QUESTION q
168
                        INNER JOIN $tblQuiz e
169
                        ON e.c_id = q.c_id AND e.id = q.exercice_id
170
                        WHERE
171
                            q.c_id = $course_id AND
172
                            q.question_id = $id AND
173
                            e.active >= 0";
174
175
                $result = Database::query($sql);
176
177
                // fills the array with the exercises which this question is in
178
                if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
179
                    while ($obj = Database::fetch_object($result)) {
180
                        $objQuestion->exerciseList[] = $obj->exercice_id;
181
                    }
182
                }
183
184
                return $objQuestion;
185
            }
186
        }
187
188
        // question not found
189
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type Question.
Loading history...
190
    }
191
192
    /**
193
     * returns the question ID.
194
     *
195
     * @author Olivier Brouckaert
196
     *
197
     * @return int - question ID
198
     */
199
    public function selectId()
200
    {
201
        return $this->id;
202
    }
203
204
    /**
205
     * returns the question title.
206
     *
207
     * @author Olivier Brouckaert
208
     *
209
     * @return string - question title
210
     */
211
    public function selectTitle()
212
    {
213
        if (!api_get_configuration_value('save_titles_as_html')) {
214
            return $this->question;
215
        }
216
217
        return Display::div($this->question, ['style' => 'display: inline-block;']);
218
    }
219
220
    /**
221
     * @param int $itemNumber
222
     *
223
     * @return string
224
     */
225
    public function getTitleToDisplay($itemNumber)
226
    {
227
        $showQuestionTitleHtml = api_get_configuration_value('save_titles_as_html');
228
229
        $title = $showQuestionTitleHtml ? '' : '<strong>';
230
        $title .= $itemNumber.'. '.$this->selectTitle();
231
        $title .= $showQuestionTitleHtml ? '' : '</strong>';
232
233
        return Display::div(
234
            $title,
235
            ['class' => 'question_title']
236
        );
237
    }
238
239
    /**
240
     * returns the question description.
241
     *
242
     * @author Olivier Brouckaert
243
     *
244
     * @return string - question description
245
     */
246
    public function selectDescription()
247
    {
248
        return $this->description;
249
    }
250
251
    /**
252
     * returns the question weighting.
253
     *
254
     * @author Olivier Brouckaert
255
     *
256
     * @return int - question weighting
257
     */
258
    public function selectWeighting()
259
    {
260
        return $this->weighting;
261
    }
262
263
    /**
264
     * returns the question position.
265
     *
266
     * @author Olivier Brouckaert
267
     *
268
     * @return int - question position
269
     */
270
    public function selectPosition()
271
    {
272
        return $this->position;
273
    }
274
275
    /**
276
     * returns the answer type.
277
     *
278
     * @author Olivier Brouckaert
279
     *
280
     * @return int - answer type
281
     */
282
    public function selectType()
283
    {
284
        return $this->type;
285
    }
286
287
    /**
288
     * returns the level of the question.
289
     *
290
     * @author Nicolas Raynaud
291
     *
292
     * @return int - level of the question, 0 by default
293
     */
294
    public function getLevel()
295
    {
296
        return $this->level;
297
    }
298
299
    /**
300
     * returns the picture name.
301
     *
302
     * @author Olivier Brouckaert
303
     *
304
     * @return string - picture name
305
     */
306
    public function selectPicture()
307
    {
308
        return $this->picture;
309
    }
310
311
    /**
312
     * @return string
313
     */
314
    public function selectPicturePath()
315
    {
316
        if (!empty($this->picture)) {
317
            return api_get_path(WEB_COURSE_PATH).$this->course['directory'].'/document/images/'.$this->getPictureFilename();
318
        }
319
320
        return '';
321
    }
322
323
    /**
324
     * @return int|string
325
     */
326
    public function getPictureId()
327
    {
328
        // for backward compatibility
329
        // when in field picture we had the filename not the document id
330
        if (preg_match("/quiz-.*/", $this->picture)) {
331
            return DocumentManager::get_document_id(
332
                $this->course,
333
                $this->selectPicturePath(),
334
                api_get_session_id()
335
            );
336
        }
337
338
        return $this->picture;
339
    }
340
341
    /**
342
     * @param int $courseId
343
     * @param int $sessionId
344
     *
345
     * @return string
346
     */
347
    public function getPictureFilename($courseId = 0, $sessionId = 0)
348
    {
349
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
350
        $sessionId = empty($sessionId) ? api_get_session_id() : (int) $sessionId;
351
352
        if (empty($courseId)) {
353
            return '';
354
        }
355
        // for backward compatibility
356
        // when in field picture we had the filename not the document id
357
        if (preg_match("/quiz-.*/", $this->picture)) {
358
            return $this->picture;
359
        }
360
361
        $pictureId = $this->getPictureId();
362
        $courseInfo = $this->course;
363
        $documentInfo = DocumentManager::get_document_data_by_id(
364
            $pictureId,
0 ignored issues
show
Bug introduced by
It seems like $pictureId can also be of type string; however, parameter $id of DocumentManager::get_document_data_by_id() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

364
            /** @scrutinizer ignore-type */ $pictureId,
Loading history...
365
            $courseInfo['code'],
366
            false,
367
            $sessionId
368
        );
369
        $documentFilename = '';
370
        if ($documentInfo) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $documentInfo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
371
            // document in document/images folder
372
            $documentFilename = pathinfo(
373
                $documentInfo['path'],
374
                PATHINFO_BASENAME
375
            );
376
        }
377
378
        return $documentFilename;
379
    }
380
381
    /**
382
     * returns the array with the exercise ID list.
383
     *
384
     * @author Olivier Brouckaert
385
     *
386
     * @return array - list of exercise ID which the question is in
387
     */
388
    public function selectExerciseList()
389
    {
390
        return $this->exerciseList;
391
    }
392
393
    /**
394
     * returns the number of exercises which this question is in.
395
     *
396
     * @author Olivier Brouckaert
397
     *
398
     * @return int - number of exercises
399
     */
400
    public function selectNbrExercises()
401
    {
402
        return sizeof($this->exerciseList);
403
    }
404
405
    /**
406
     * changes the question title.
407
     *
408
     * @param string $title - question title
409
     *
410
     * @author Olivier Brouckaert
411
     */
412
    public function updateTitle($title)
413
    {
414
        $this->question = $title;
415
    }
416
417
    /**
418
     * @param int $id
419
     */
420
    public function updateParentId($id)
421
    {
422
        $this->parent_id = intval($id);
423
    }
424
425
    /**
426
     * changes the question description.
427
     *
428
     * @param string $description - question description
429
     *
430
     * @author Olivier Brouckaert
431
     */
432
    public function updateDescription($description)
433
    {
434
        $this->description = $description;
435
    }
436
437
    /**
438
     * changes the question weighting.
439
     *
440
     * @param int $weighting - question weighting
441
     *
442
     * @author Olivier Brouckaert
443
     */
444
    public function updateWeighting($weighting)
445
    {
446
        $this->weighting = $weighting;
447
    }
448
449
    /**
450
     * @param array $category
451
     *
452
     * @author Hubert Borderiou 12-10-2011
453
     */
454
    public function updateCategory($category)
455
    {
456
        $this->category = $category;
457
    }
458
459
    /**
460
     * @param int $value
461
     *
462
     * @author Hubert Borderiou 12-10-2011
463
     */
464
    public function updateScoreAlwaysPositive($value)
465
    {
466
        $this->scoreAlwaysPositive = $value;
0 ignored issues
show
Bug Best Practice introduced by
The property scoreAlwaysPositive does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
467
    }
468
469
    /**
470
     * @param int $value
471
     *
472
     * @author Hubert Borderiou 12-10-2011
473
     */
474
    public function updateUncheckedMayScore($value)
475
    {
476
        $this->uncheckedMayScore = $value;
0 ignored issues
show
Bug Best Practice introduced by
The property uncheckedMayScore does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
477
    }
478
479
    /**
480
     * Save category of a question.
481
     *
482
     * A question can have n categories if category is empty,
483
     * then question has no category then delete the category entry
484
     *
485
     * @param array $category_list
486
     *
487
     * @author Julio Montoya - Adding multiple cat support
488
     */
489
    public function saveCategories($category_list)
490
    {
491
        if (!empty($category_list)) {
492
            $this->deleteCategory();
493
            $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
494
495
            // update or add category for a question
496
            foreach ($category_list as $category_id) {
497
                $category_id = intval($category_id);
498
                $question_id = intval($this->id);
499
                $sql = "SELECT count(*) AS nb
500
                        FROM $table
501
                        WHERE
502
                            category_id = $category_id
503
                            AND question_id = $question_id
504
                            AND c_id=".api_get_course_int_id();
505
                $res = Database::query($sql);
506
                $row = Database::fetch_array($res);
507
                if ($row['nb'] > 0) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

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

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

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

could be turned into

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

This is much more concise to read.

Loading history...
508
                    // DO nothing
509
                } else {
510
                    $sql = "INSERT INTO $table (c_id, question_id, category_id)
511
                            VALUES (".api_get_course_int_id().", $question_id, $category_id)";
512
                    Database::query($sql);
513
                }
514
            }
515
        }
516
    }
517
518
    /**
519
     * in this version, a question can only have 1 category
520
     * if category is 0, then question has no category then delete the category entry.
521
     *
522
     * @param int $categoryId
523
     * @param int $courseId
524
     *
525
     * @return bool
526
     *
527
     * @author Hubert Borderiou 12-10-2011
528
     */
529
    public function saveCategory($categoryId, $courseId = 0)
530
    {
531
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
532
533
        if (empty($courseId)) {
534
            return false;
535
        }
536
537
        if ($categoryId <= 0) {
538
            $this->deleteCategory($courseId);
539
        } else {
540
            // update or add category for a question
541
            $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
542
            $categoryId = (int) $categoryId;
543
            $question_id = (int) $this->id;
544
            $sql = "SELECT count(*) AS nb FROM $table
545
                    WHERE
546
                        question_id = $question_id AND
547
                        c_id = ".$courseId;
548
            $res = Database::query($sql);
549
            $row = Database::fetch_array($res);
550
            if ($row['nb'] > 0) {
551
                $sql = "UPDATE $table
552
                        SET category_id = $categoryId
553
                        WHERE
554
                            question_id = $question_id AND
555
                            c_id = ".$courseId;
556
                Database::query($sql);
557
            } else {
558
                $sql = "INSERT INTO $table (c_id, question_id, category_id)
559
                        VALUES (".$courseId.", $question_id, $categoryId)";
560
                Database::query($sql);
561
            }
562
563
            return true;
564
        }
565
    }
566
567
    /**
568
     * @author hubert borderiou 12-10-2011
569
     *
570
     * @param int $courseId
571
     *                      delete any category entry for question id
572
     *                      delete the category for question
573
     *
574
     * @return bool
575
     */
576
    public function deleteCategory($courseId = 0)
577
    {
578
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
579
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
580
        $questionId = (int) $this->id;
581
        if (empty($courseId) || empty($questionId)) {
582
            return false;
583
        }
584
        $sql = "DELETE FROM $table
585
                WHERE
586
                    question_id = $questionId AND
587
                    c_id = ".$courseId;
588
        Database::query($sql);
589
590
        return true;
591
    }
592
593
    /**
594
     * changes the question position.
595
     *
596
     * @param int $position - question position
597
     *
598
     * @author Olivier Brouckaert
599
     */
600
    public function updatePosition($position)
601
    {
602
        $this->position = $position;
603
    }
604
605
    /**
606
     * changes the question level.
607
     *
608
     * @param int $level - question level
609
     *
610
     * @author Nicolas Raynaud
611
     */
612
    public function updateLevel($level)
613
    {
614
        $this->level = $level;
615
    }
616
617
    /**
618
     * changes the answer type. If the user changes the type from "unique answer" to "multiple answers"
619
     * (or conversely) answers are not deleted, otherwise yes.
620
     *
621
     * @param int $type - answer type
622
     *
623
     * @author Olivier Brouckaert
624
     */
625
    public function updateType($type)
626
    {
627
        $table = Database::get_course_table(TABLE_QUIZ_ANSWER);
628
        $course_id = $this->course['real_id'];
629
630
        if (empty($course_id)) {
631
            $course_id = api_get_course_int_id();
632
        }
633
        // if we really change the type
634
        if ($type != $this->type) {
635
            // if we don't change from "unique answer" to "multiple answers" (or conversely)
636
            if (!in_array($this->type, [UNIQUE_ANSWER, MULTIPLE_ANSWER]) ||
637
                !in_array($type, [UNIQUE_ANSWER, MULTIPLE_ANSWER])
638
            ) {
639
                // removes old answers
640
                $sql = "DELETE FROM $table
641
                        WHERE c_id = $course_id AND question_id = ".intval($this->id);
642
                Database::query($sql);
643
            }
644
645
            $this->type = $type;
646
        }
647
    }
648
649
    /**
650
     * Get default hot spot folder in documents.
651
     *
652
     * @return string
653
     */
654
    public function getHotSpotFolderInCourse()
655
    {
656
        if (empty($this->course) || empty($this->course['directory'])) {
657
            // Stop everything if course is not set.
658
            api_not_allowed();
659
        }
660
661
        $pictureAbsolutePath = api_get_path(SYS_COURSE_PATH).$this->course['directory'].'/document/images/';
662
        $picturePath = basename($pictureAbsolutePath);
663
664
        if (!is_dir($picturePath)) {
665
            create_unexisting_directory(
666
                $this->course,
667
                api_get_user_id(),
668
                0,
669
                0,
670
                0,
671
                dirname($pictureAbsolutePath),
672
                '/'.$picturePath,
673
                $picturePath
674
            );
675
        }
676
677
        return $pictureAbsolutePath;
678
    }
679
680
    /**
681
     * adds a picture to the question.
682
     *
683
     * @param string $picture - temporary path of the picture to upload
684
     *
685
     * @return bool - true if uploaded, otherwise false
686
     *
687
     * @author Olivier Brouckaert
688
     */
689
    public function uploadPicture($picture)
690
    {
691
        $picturePath = $this->getHotSpotFolderInCourse();
692
693
        // if the question has got an ID
694
        if ($this->id) {
695
            $pictureFilename = self::generatePictureName();
0 ignored issues
show
Bug Best Practice introduced by
The method Question::generatePictureName() is not static, but was called statically. ( Ignorable by Annotation )

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

695
            /** @scrutinizer ignore-call */ 
696
            $pictureFilename = self::generatePictureName();
Loading history...
696
            $img = new Image($picture);
697
            $img->send_image($picturePath.'/'.$pictureFilename, -1, 'jpg');
698
            $document_id = add_document(
699
                $this->course,
700
                '/images/'.$pictureFilename,
701
                'file',
702
                filesize($picturePath.'/'.$pictureFilename),
703
                $pictureFilename
704
            );
705
706
            if ($document_id) {
707
                $this->picture = $document_id;
708
709
                if (!file_exists($picturePath.'/'.$pictureFilename)) {
710
                    return false;
711
                }
712
713
                api_item_property_update(
714
                    $this->course,
715
                    TOOL_DOCUMENT,
716
                    $document_id,
717
                    'DocumentAdded',
718
                    api_get_user_id()
719
                );
720
721
                $this->resizePicture('width', 800);
722
723
                return true;
724
            }
725
        }
726
727
        return false;
728
    }
729
730
    /**
731
     * return the name for image use in hotspot question
732
     * to be unique, name is quiz-[utc unix timestamp].jpg.
733
     *
734
     * @param string $prefix
735
     * @param string $extension
736
     *
737
     * @return string
738
     */
739
    public function generatePictureName($prefix = 'quiz-', $extension = 'jpg')
740
    {
741
        // image name is quiz-xxx.jpg in folder images/
742
        $utcTime = time();
743
744
        return $prefix.$utcTime.'.'.$extension;
745
    }
746
747
    /**
748
     * deletes the picture.
749
     *
750
     * @author Olivier Brouckaert
751
     *
752
     * @return bool - true if removed, otherwise false
753
     */
754
    public function removePicture()
755
    {
756
        $picturePath = $this->getHotSpotFolderInCourse();
757
758
        // if the question has got an ID and if the picture exists
759
        if ($this->id) {
760
            $picture = $this->picture;
761
            $this->picture = '';
762
763
            return @unlink($picturePath.'/'.$picture) ? true : false;
764
        }
765
766
        return false;
767
    }
768
769
    /**
770
     * Exports a picture to another question.
771
     *
772
     * @author Olivier Brouckaert
773
     *
774
     * @param int   $questionId - ID of the target question
775
     * @param array $courseInfo
776
     *
777
     * @return bool - true if copied, otherwise false
778
     */
779
    public function exportPicture($questionId, $courseInfo)
780
    {
781
        if (empty($questionId) || empty($courseInfo)) {
782
            return false;
783
        }
784
785
        $course_id = $courseInfo['real_id'];
786
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
787
        $destination_path = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/images';
788
        $source_path = $this->getHotSpotFolderInCourse();
789
790
        // if the question has got an ID and if the picture exists
791
        if (!$this->id || empty($this->picture)) {
792
            return false;
793
        }
794
795
        $picture = $this->generatePictureName();
796
797
        if (file_exists($source_path.'/'.$this->picture)) {
798
            // for backward compatibility
799
            $result = @copy(
800
                $source_path.'/'.$this->picture,
801
                $destination_path.'/'.$picture
802
            );
803
        } else {
804
            $imageInfo = DocumentManager::get_document_data_by_id(
805
                $this->picture,
806
                $courseInfo['code']
807
            );
808
            if (file_exists($imageInfo['absolute_path'])) {
809
                $result = @copy(
810
                    $imageInfo['absolute_path'],
811
                    $destination_path.'/'.$picture
812
                );
813
            }
814
        }
815
816
        // If copy was correct then add to the database
817
        if (!$result) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.
Loading history...
818
            return false;
819
        }
820
821
        $sql = "UPDATE $TBL_QUESTIONS SET
822
                picture = '".Database::escape_string($picture)."'
823
                WHERE c_id = $course_id AND id='".intval($questionId)."'";
824
        Database::query($sql);
825
826
        $documentId = add_document(
827
            $courseInfo,
828
            '/images/'.$picture,
829
            'file',
830
            filesize($destination_path.'/'.$picture),
831
            $picture
832
        );
833
834
        if (!$documentId) {
835
            return false;
836
        }
837
838
        return api_item_property_update(
839
            $courseInfo,
840
            TOOL_DOCUMENT,
841
            $documentId,
842
            'DocumentAdded',
843
            api_get_user_id()
844
        );
845
    }
846
847
    /**
848
     * Saves the picture coming from POST into a temporary file
849
     * Temporary pictures are used when we don't want to save a picture right after a form submission.
850
     * For example, if we first show a confirmation box.
851
     *
852
     * @author Olivier Brouckaert
853
     *
854
     * @param string $picture     - temporary path of the picture to move
855
     * @param string $pictureName - Name of the picture
856
     */
857
    public function setTmpPicture($picture, $pictureName)
858
    {
859
        $picturePath = $this->getHotSpotFolderInCourse();
860
        $pictureName = explode('.', $pictureName);
861
        $Extension = $pictureName[sizeof($pictureName) - 1];
862
863
        // saves the picture into a temporary file
864
        @move_uploaded_file($picture, $picturePath.'/tmp.'.$Extension);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for move_uploaded_file(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

864
        /** @scrutinizer ignore-unhandled */ @move_uploaded_file($picture, $picturePath.'/tmp.'.$Extension);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
865
    }
866
867
    /**
868
     * Set title.
869
     *
870
     * @param string $title
871
     */
872
    public function setTitle($title)
873
    {
874
        $this->question = $title;
875
    }
876
877
    /**
878
     * Sets extra info.
879
     *
880
     * @param string $extra
881
     */
882
    public function setExtra($extra)
883
    {
884
        $this->extra = $extra;
885
    }
886
887
    /**
888
     * updates the question in the data base
889
     * if an exercise ID is provided, we add that exercise ID into the exercise list.
890
     *
891
     * @author Olivier Brouckaert
892
     *
893
     * @param Exercise $exercise
894
     */
895
    public function save($exercise)
896
    {
897
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
898
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
899
        $em = Database::getManager();
900
        $exerciseId = $exercise->id;
901
902
        $id = $this->id;
903
        $question = $this->question;
904
        $description = $this->description;
905
        $weighting = $this->weighting;
906
        $position = $this->position;
907
        $type = $this->type;
908
        $picture = $this->picture;
909
        $level = $this->level;
910
        $extra = $this->extra;
911
        $c_id = $this->course['real_id'];
912
        $categoryId = $this->category;
913
914
        // question already exists
915
        if (!empty($id)) {
916
            $params = [
917
                'question' => $question,
918
                'description' => $description,
919
                'ponderation' => $weighting,
920
                'position' => $position,
921
                'type' => $type,
922
                'picture' => $picture,
923
                'extra' => $extra,
924
                'level' => $level,
925
            ];
926
            if ($exercise->questionFeedbackEnabled) {
927
                $params['feedback'] = $this->feedback;
928
            }
929
            Database::update(
930
                $TBL_QUESTIONS,
931
                $params,
932
                ['c_id = ? AND id = ?' => [$c_id, $id]]
933
            );
934
            $this->saveCategory($categoryId);
935
936
            if (!empty($exerciseId)) {
937
                api_item_property_update(
938
                    $this->course,
939
                    TOOL_QUIZ,
940
                    $id,
941
                    'QuizQuestionUpdated',
942
                    api_get_user_id()
943
                );
944
            }
945
            if (api_get_setting('search_enabled') == 'true') {
946
                if ($exerciseId != 0) {
947
                    $this->search_engine_edit($exerciseId);
948
                } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

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

These else branches can be removed.

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

could be turned into

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

This is much more concise to read.

Loading history...
949
                    /**
950
                     * actually there is *not* an user interface for
951
                     * creating questions without a relation with an exercise.
952
                     */
953
                }
954
            }
955
        } else {
956
            // creates a new question
957
            $sql = "SELECT max(position)
958
                    FROM $TBL_QUESTIONS as question,
959
                    $TBL_EXERCISE_QUESTION as test_question
960
                    WHERE
961
                        question.id = test_question.question_id AND
962
                        test_question.exercice_id = ".intval($exerciseId)." AND
963
                        question.c_id = $c_id AND
964
                        test_question.c_id = $c_id ";
965
            $result = Database::query($sql);
966
            $current_position = Database::result($result, 0, 0);
967
            $this->updatePosition($current_position + 1);
968
            $position = $this->position;
969
970
            $params = [
971
                'c_id' => $c_id,
972
                'question' => $question,
973
                'description' => $description,
974
                'ponderation' => $weighting,
975
                'position' => $position,
976
                'type' => $type,
977
                'picture' => $picture,
978
                'extra' => $extra,
979
                'level' => $level,
980
            ];
981
982
            if ($exercise->questionFeedbackEnabled) {
983
                $params['feedback'] = $this->feedback;
984
            }
985
            $this->id = Database::insert($TBL_QUESTIONS, $params);
986
987
            if ($this->id) {
988
                $sql = "UPDATE $TBL_QUESTIONS SET id = iid WHERE iid = {$this->id}";
989
                Database::query($sql);
990
991
                api_item_property_update(
992
                    $this->course,
993
                    TOOL_QUIZ,
994
                    $this->id,
995
                    'QuizQuestionAdded',
996
                    api_get_user_id()
997
                );
998
999
                // If hotspot, create first answer
1000
                if ($type == HOT_SPOT || $type == HOT_SPOT_ORDER) {
1001
                    $quizAnswer = new CQuizAnswer();
1002
                    $quizAnswer
1003
                        ->setCId($c_id)
1004
                        ->setQuestionId($this->id)
1005
                        ->setAnswer('')
1006
                        ->setPonderation(10)
1007
                        ->setPosition(1)
1008
                        ->setHotspotCoordinates('0;0|0|0')
1009
                        ->setHotspotType('square');
1010
1011
                    $em->persist($quizAnswer);
1012
                    $em->flush();
1013
1014
                    $id = $quizAnswer->getIid();
1015
1016
                    if ($id) {
1017
                        $quizAnswer
1018
                            ->setId($id)
1019
                            ->setIdAuto($id);
1020
1021
                        $em->merge($quizAnswer);
1022
                        $em->flush();
1023
                    }
1024
                }
1025
1026
                if ($type == HOT_SPOT_DELINEATION) {
1027
                    $quizAnswer = new CQuizAnswer();
1028
                    $quizAnswer
1029
                        ->setCId($c_id)
1030
                        ->setQuestionId($this->id)
1031
                        ->setAnswer('')
1032
                        ->setPonderation(10)
1033
                        ->setPosition(1)
1034
                        ->setHotspotCoordinates('0;0|0|0')
1035
                        ->setHotspotType('delineation');
1036
1037
                    $em->persist($quizAnswer);
1038
                    $em->flush();
1039
1040
                    $id = $quizAnswer->getIid();
1041
1042
                    if ($id) {
1043
                        $quizAnswer
1044
                            ->setId($id)
1045
                            ->setIdAuto($id);
1046
1047
                        $em->merge($quizAnswer);
1048
                        $em->flush();
1049
                    }
1050
                }
1051
1052
                if (api_get_setting('search_enabled') == 'true') {
1053
                    if ($exerciseId != 0) {
1054
                        $this->search_engine_edit($exerciseId, true);
1055
                    } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

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

These else branches can be removed.

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

could be turned into

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

This is much more concise to read.

Loading history...
1056
                        /**
1057
                         * actually there is *not* an user interface for
1058
                         * creating questions without a relation with an exercise.
1059
                         */
1060
                    }
1061
                }
1062
            }
1063
        }
1064
1065
        // if the question is created in an exercise
1066
        if ($exerciseId) {
1067
            // adds the exercise into the exercise list of this question
1068
            $this->addToList($exerciseId, true);
1069
        }
1070
    }
1071
1072
    public function search_engine_edit(
1073
        $exerciseId,
1074
        $addQs = false,
1075
        $rmQs = false
1076
    ) {
1077
        // update search engine and its values table if enabled
1078
        if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
1079
            $course_id = api_get_course_id();
1080
            // get search_did
1081
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
1082
            if ($addQs || $rmQs) {
1083
                //there's only one row per question on normal db and one document per question on search engine db
1084
                $sql = 'SELECT * FROM %s
1085
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_second_level=%s LIMIT 1';
1086
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
1087
            } else {
1088
                $sql = 'SELECT * FROM %s
1089
                    WHERE course_code=\'%s\' AND tool_id=\'%s\'
1090
                    AND ref_id_high_level=%s AND ref_id_second_level=%s LIMIT 1';
1091
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
1092
            }
1093
            $res = Database::query($sql);
1094
1095
            if (Database::num_rows($res) > 0 || $addQs) {
1096
                $di = new ChamiloIndexer();
1097
                if ($addQs) {
1098
                    $question_exercises = [(int) $exerciseId];
1099
                } else {
1100
                    $question_exercises = [];
1101
                }
1102
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
1103
                $di->connectDb(null, null, $lang);
1104
1105
                // retrieve others exercise ids
1106
                $se_ref = Database::fetch_array($res);
1107
                $se_doc = $di->get_document((int) $se_ref['search_did']);
1108
                if ($se_doc !== false) {
1109
                    if (($se_doc_data = $di->get_document_data($se_doc)) !== false) {
1110
                        $se_doc_data = unserialize($se_doc_data);
1111
                        if (isset($se_doc_data[SE_DATA]['type']) &&
1112
                            $se_doc_data[SE_DATA]['type'] == SE_DOCTYPE_EXERCISE_QUESTION
1113
                        ) {
1114
                            if (isset($se_doc_data[SE_DATA]['exercise_ids']) &&
1115
                                is_array($se_doc_data[SE_DATA]['exercise_ids'])
1116
                            ) {
1117
                                foreach ($se_doc_data[SE_DATA]['exercise_ids'] as $old_value) {
1118
                                    if (!in_array($old_value, $question_exercises)) {
1119
                                        $question_exercises[] = $old_value;
1120
                                    }
1121
                                }
1122
                            }
1123
                        }
1124
                    }
1125
                }
1126
                if ($rmQs) {
1127
                    while (($key = array_search($exerciseId, $question_exercises)) !== false) {
1128
                        unset($question_exercises[$key]);
1129
                    }
1130
                }
1131
1132
                // build the chunk to index
1133
                $ic_slide = new IndexableChunk();
1134
                $ic_slide->addValue("title", $this->question);
1135
                $ic_slide->addCourseId($course_id);
1136
                $ic_slide->addToolId(TOOL_QUIZ);
1137
                $xapian_data = [
1138
                    SE_COURSE_ID => $course_id,
1139
                    SE_TOOL_ID => TOOL_QUIZ,
1140
                    SE_DATA => [
1141
                        'type' => SE_DOCTYPE_EXERCISE_QUESTION,
1142
                        'exercise_ids' => $question_exercises,
1143
                        'question_id' => (int) $this->id,
1144
                    ],
1145
                    SE_USER => (int) api_get_user_id(),
1146
                ];
1147
                $ic_slide->xapian_data = serialize($xapian_data);
1148
                $ic_slide->addValue("content", $this->description);
1149
1150
                //TODO: index answers, see also form validation on question_admin.inc.php
1151
1152
                $di->remove_document($se_ref['search_did']);
1153
                $di->addChunk($ic_slide);
1154
1155
                //index and return search engine document id
1156
                if (!empty($question_exercises)) { // if empty there is nothing to index
1157
                    $did = $di->index();
1158
                    unset($di);
1159
                }
1160
                if ($did || $rmQs) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $did does not seem to be defined for all execution paths leading up to this point.
Loading history...
1161
                    // save it to db
1162
                    if ($addQs || $rmQs) {
1163
                        $sql = "DELETE FROM %s
1164
                            WHERE course_code = '%s' AND tool_id = '%s' AND ref_id_second_level = '%s'";
1165
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
1166
                    } else {
1167
                        $sql = "DELETE FROM %S
1168
                            WHERE
1169
                                course_code = '%s'
1170
                                AND tool_id = '%s'
1171
                                AND tool_id = '%s'
1172
                                AND ref_id_high_level = '%s'
1173
                                AND ref_id_second_level = '%s'";
1174
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
1175
                    }
1176
                    Database::query($sql);
1177
                    if ($rmQs) {
1178
                        if (!empty($question_exercises)) {
1179
                            $sql = "INSERT INTO %s (
1180
                                    id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
1181
                                )
1182
                                VALUES (
1183
                                    NULL, '%s', '%s', %s, %s, %s
1184
                                )";
1185
                            $sql = sprintf(
1186
                                $sql,
1187
                                $tbl_se_ref,
1188
                                $course_id,
1189
                                TOOL_QUIZ,
1190
                                array_shift($question_exercises),
1191
                                $this->id,
1192
                                $did
1193
                            );
1194
                            Database::query($sql);
1195
                        }
1196
                    } else {
1197
                        $sql = "INSERT INTO %s (
1198
                                id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
1199
                            )
1200
                            VALUES (
1201
                                NULL , '%s', '%s', %s, %s, %s
1202
                            )";
1203
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id, $did);
1204
                        Database::query($sql);
1205
                    }
1206
                }
1207
            }
1208
        }
1209
    }
1210
1211
    /**
1212
     * adds an exercise into the exercise list.
1213
     *
1214
     * @author Olivier Brouckaert
1215
     *
1216
     * @param int  $exerciseId - exercise ID
1217
     * @param bool $fromSave   - from $this->save() or not
1218
     */
1219
    public function addToList($exerciseId, $fromSave = false)
1220
    {
1221
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1222
        $id = $this->id;
1223
        // checks if the exercise ID is not in the list
1224
        if (!in_array($exerciseId, $this->exerciseList)) {
1225
            $this->exerciseList[] = $exerciseId;
1226
            $new_exercise = new Exercise();
1227
            $new_exercise->read($exerciseId);
1228
            $count = $new_exercise->selectNbrQuestions();
1229
            $count++;
1230
            $sql = "INSERT INTO $exerciseRelQuestionTable (c_id, question_id, exercice_id, question_order)
1231
                    VALUES ({$this->course['real_id']}, ".intval($id).", ".intval($exerciseId).", '$count')";
1232
            Database::query($sql);
1233
1234
            // we do not want to reindex if we had just saved adnd indexed the question
1235
            if (!$fromSave) {
1236
                $this->search_engine_edit($exerciseId, true);
1237
            }
1238
        }
1239
    }
1240
1241
    /**
1242
     * removes an exercise from the exercise list.
1243
     *
1244
     * @author Olivier Brouckaert
1245
     *
1246
     * @param int $exerciseId - exercise ID
1247
     *
1248
     * @return bool - true if removed, otherwise false
1249
     */
1250
    public function removeFromList($exerciseId)
1251
    {
1252
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1253
        $id = $this->id;
1254
1255
        // searches the position of the exercise ID in the list
1256
        $pos = array_search($exerciseId, $this->exerciseList);
1257
        $course_id = api_get_course_int_id();
1258
1259
        // exercise not found
1260
        if ($pos === false) {
1261
            return false;
1262
        } else {
1263
            // deletes the position in the array containing the wanted exercise ID
1264
            unset($this->exerciseList[$pos]);
1265
            //update order of other elements
1266
            $sql = "SELECT question_order
1267
                    FROM $TBL_EXERCISE_QUESTION
1268
                    WHERE
1269
                        c_id = $course_id
1270
                        AND question_id = ".intval($id)."
1271
                        AND exercice_id = ".intval($exerciseId);
1272
            $res = Database::query($sql);
1273
            if (Database::num_rows($res) > 0) {
1274
                $row = Database::fetch_array($res);
1275
                if (!empty($row['question_order'])) {
1276
                    $sql = "UPDATE $TBL_EXERCISE_QUESTION
1277
                        SET question_order = question_order-1
1278
                        WHERE
1279
                            c_id = $course_id
1280
                            AND exercice_id = ".intval($exerciseId)."
1281
                            AND question_order > ".$row['question_order'];
1282
                    Database::query($sql);
1283
                }
1284
            }
1285
1286
            $sql = "DELETE FROM $TBL_EXERCISE_QUESTION
1287
                    WHERE
1288
                        c_id = $course_id
1289
                        AND question_id = ".intval($id)."
1290
                        AND exercice_id = ".intval($exerciseId);
1291
            Database::query($sql);
1292
1293
            return true;
1294
        }
1295
    }
1296
1297
    /**
1298
     * Deletes a question from the database
1299
     * the parameter tells if the question is removed from all exercises (value = 0),
1300
     * or just from one exercise (value = exercise ID).
1301
     *
1302
     * @author Olivier Brouckaert
1303
     *
1304
     * @param int $deleteFromEx - exercise ID if the question is only removed from one exercise
1305
     */
1306
    public function delete($deleteFromEx = 0)
1307
    {
1308
        $course_id = api_get_course_int_id();
1309
1310
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1311
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1312
        $TBL_REPONSES = Database::get_course_table(TABLE_QUIZ_ANSWER);
1313
        $TBL_QUIZ_QUESTION_REL_CATEGORY = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
1314
1315
        $id = intval($this->id);
1316
1317
        // if the question must be removed from all exercises
1318
        if (!$deleteFromEx) {
1319
            //update the question_order of each question to avoid inconsistencies
1320
            $sql = "SELECT exercice_id, question_order 
1321
                    FROM $TBL_EXERCISE_QUESTION
1322
                    WHERE c_id = $course_id AND question_id = ".intval($id)."";
1323
1324
            $res = Database::query($sql);
1325
            if (Database::num_rows($res) > 0) {
1326
                while ($row = Database::fetch_array($res)) {
1327
                    if (!empty($row['question_order'])) {
1328
                        $sql = "UPDATE $TBL_EXERCISE_QUESTION
1329
                                SET question_order = question_order-1
1330
                                WHERE
1331
                                    c_id = $course_id AND 
1332
                                    exercice_id = ".intval($row['exercice_id'])." AND 
1333
                                    question_order > ".$row['question_order'];
1334
                        Database::query($sql);
1335
                    }
1336
                }
1337
            }
1338
1339
            $sql = "DELETE FROM $TBL_EXERCISE_QUESTION
1340
                    WHERE c_id = $course_id AND question_id = ".$id;
1341
            Database::query($sql);
1342
1343
            $sql = "DELETE FROM $TBL_QUESTIONS
1344
                    WHERE c_id = $course_id AND id = ".$id;
1345
            Database::query($sql);
1346
1347
            $sql = "DELETE FROM $TBL_REPONSES
1348
                    WHERE c_id = $course_id AND question_id = ".$id;
1349
            Database::query($sql);
1350
1351
            // remove the category of this question in the question_rel_category table
1352
            $sql = "DELETE FROM $TBL_QUIZ_QUESTION_REL_CATEGORY
1353
                    WHERE 
1354
                        c_id = $course_id AND 
1355
                        question_id = ".$id;
1356
            Database::query($sql);
1357
1358
            api_item_property_update(
1359
                $this->course,
1360
                TOOL_QUIZ,
1361
                $id,
1362
                'QuizQuestionDeleted',
1363
                api_get_user_id()
1364
            );
1365
            $this->removePicture();
1366
        } else {
1367
            // just removes the exercise from the list
1368
            $this->removeFromList($deleteFromEx);
1369
            if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
1370
                // disassociate question with this exercise
1371
                $this->search_engine_edit($deleteFromEx, false, true);
1372
            }
1373
1374
            api_item_property_update(
1375
                $this->course,
1376
                TOOL_QUIZ,
1377
                $id,
1378
                'QuizQuestionDeleted',
1379
                api_get_user_id()
1380
            );
1381
        }
1382
    }
1383
1384
    /**
1385
     * Duplicates the question.
1386
     *
1387
     * @author Olivier Brouckaert
1388
     *
1389
     * @param array $courseInfo Course info of the destination course
1390
     *
1391
     * @return false|string ID of the new question
1392
     */
1393
    public function duplicate($courseInfo = [])
1394
    {
1395
        $courseInfo = empty($courseInfo) ? $this->course : $courseInfo;
1396
1397
        if (empty($courseInfo)) {
1398
            return false;
1399
        }
1400
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
1401
        $TBL_QUESTION_OPTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1402
1403
        $question = $this->question;
1404
        $description = $this->description;
1405
        $weighting = $this->weighting;
1406
        $position = $this->position;
1407
        $type = $this->type;
1408
        $level = (int) $this->level;
1409
        $extra = $this->extra;
1410
1411
        // Using the same method used in the course copy to transform URLs
1412
        if ($this->course['id'] != $courseInfo['id']) {
1413
            $description = DocumentManager::replaceUrlWithNewCourseCode(
1414
                $description,
1415
                $this->course['code'],
1416
                $courseInfo['id']
1417
            );
1418
            $question = DocumentManager::replaceUrlWithNewCourseCode(
1419
                $question,
1420
                $this->course['code'],
1421
                $courseInfo['id']
1422
            );
1423
        }
1424
1425
        $course_id = $courseInfo['real_id'];
1426
1427
        // Read the source options
1428
        $options = self::readQuestionOption($this->id, $this->course['real_id']);
1429
1430
        // Inserting in the new course db / or the same course db
1431
        $params = [
1432
            'c_id' => $course_id,
1433
            'question' => $question,
1434
            'description' => $description,
1435
            'ponderation' => $weighting,
1436
            'position' => $position,
1437
            'type' => $type,
1438
            'level' => $level,
1439
            'extra' => $extra,
1440
        ];
1441
        $newQuestionId = Database::insert($questionTable, $params);
1442
1443
        if ($newQuestionId) {
1444
            $sql = "UPDATE $questionTable 
1445
                    SET id = iid
1446
                    WHERE iid = $newQuestionId";
1447
            Database::query($sql);
1448
1449
            if (!empty($options)) {
1450
                // Saving the quiz_options
1451
                foreach ($options as $item) {
1452
                    $item['question_id'] = $newQuestionId;
1453
                    $item['c_id'] = $course_id;
1454
                    unset($item['id']);
1455
                    unset($item['iid']);
1456
                    $id = Database::insert($TBL_QUESTION_OPTIONS, $item);
1457
                    if ($id) {
1458
                        $sql = "UPDATE $TBL_QUESTION_OPTIONS 
1459
                                SET id = iid
1460
                                WHERE iid = $id";
1461
                        Database::query($sql);
1462
                    }
1463
                }
1464
            }
1465
1466
            // Duplicates the picture of the hotspot
1467
            $this->exportPicture($newQuestionId, $courseInfo);
1468
        }
1469
1470
        return $newQuestionId;
1471
    }
1472
1473
    /**
1474
     * @return string
1475
     */
1476
    public function get_question_type_name()
1477
    {
1478
        $key = self::$questionTypes[$this->type];
1479
1480
        return get_lang($key[1]);
1481
    }
1482
1483
    /**
1484
     * @param string $type
1485
     */
1486
    public static function get_question_type($type)
1487
    {
1488
        if ($type == ORAL_EXPRESSION && api_get_setting('enable_record_audio') !== 'true') {
1489
            return null;
1490
        }
1491
1492
        return self::$questionTypes[$type];
1493
    }
1494
1495
    /**
1496
     * @return array
1497
     */
1498
    public static function get_question_type_list()
1499
    {
1500
        if (api_get_setting('enable_record_audio') !== 'true') {
1501
            self::$questionTypes[ORAL_EXPRESSION] = null;
1502
            unset(self::$questionTypes[ORAL_EXPRESSION]);
1503
        }
1504
        if (api_get_setting('enable_quiz_scenario') !== 'true') {
1505
            self::$questionTypes[HOT_SPOT_DELINEATION] = null;
1506
            unset(self::$questionTypes[HOT_SPOT_DELINEATION]);
1507
        }
1508
1509
        return self::$questionTypes;
1510
    }
1511
1512
    /**
1513
     * Returns an instance of the class corresponding to the type.
1514
     *
1515
     * @param int $type the type of the question
1516
     *
1517
     * @return $this instance of a Question subclass (or of Questionc class by default)
1518
     */
1519
    public static function getInstance($type)
1520
    {
1521
        if (!is_null($type)) {
1522
            list($file_name, $class_name) = self::get_question_type($type);
1523
            if (!empty($file_name)) {
1524
                if (class_exists($class_name)) {
1525
                    return new $class_name();
1526
                } else {
1527
                    echo 'Can\'t instanciate class '.$class_name.' of type '.$type;
1528
                }
1529
            }
1530
        }
1531
1532
        return null;
1533
    }
1534
1535
    /**
1536
     * Creates the form to create / edit a question
1537
     * A subclass can redefine this function to add fields...
1538
     *
1539
     * @param FormValidator $form
1540
     * @param Exercise      $exercise
1541
     */
1542
    public function createForm(&$form, $exercise)
1543
    {
1544
        echo '<style>
1545
                .media { display:none;}
1546
            </style>';
1547
1548
        // question name
1549
        if (api_get_configuration_value('save_titles_as_html')) {
1550
            $editorConfig = ['ToolbarSet' => 'Minimal'];
1551
            $form->addHtmlEditor(
1552
                'questionName',
1553
                get_lang('Question'),
1554
                false,
1555
                false,
1556
                $editorConfig,
1557
                true
1558
            );
1559
        } else {
1560
            $form->addElement('text', 'questionName', get_lang('Question'));
1561
        }
1562
1563
        $form->addRule('questionName', get_lang('GiveQuestion'), 'required');
1564
1565
        // default content
1566
        $isContent = isset($_REQUEST['isContent']) ? intval($_REQUEST['isContent']) : null;
1567
1568
        // Question type
1569
        $answerType = isset($_REQUEST['answerType']) ? intval($_REQUEST['answerType']) : null;
1570
        $form->addElement('hidden', 'answerType', $answerType);
1571
1572
        // html editor
1573
        $editorConfig = [
1574
            'ToolbarSet' => 'TestQuestionDescription',
1575
            'Height' => '150',
1576
        ];
1577
1578
        if (!api_is_allowed_to_edit(null, true)) {
1579
            $editorConfig['UserStatus'] = 'student';
1580
        }
1581
1582
        $form->addButtonAdvancedSettings('advanced_params');
1583
        $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
1584
        $form->addHtmlEditor(
1585
            'questionDescription',
1586
            get_lang('QuestionDescription'),
1587
            false,
1588
            false,
1589
            $editorConfig
1590
        );
1591
1592
        // hidden values
1593
        $my_id = isset($_REQUEST['myid']) ? intval($_REQUEST['myid']) : null;
1594
        $form->addElement('hidden', 'myid', $my_id);
1595
1596
        if ($this->type != MEDIA_QUESTION) {
1597
            // Advanced parameters
1598
            $select_level = self::get_default_levels();
1599
            $form->addElement(
1600
                'select',
1601
                'questionLevel',
1602
                get_lang('Difficulty'),
1603
                $select_level
1604
            );
1605
1606
            // Categories
1607
            $tabCat = TestCategory::getCategoriesIdAndName();
1608
            $form->addElement(
1609
                'select',
1610
                'questionCategory',
1611
                get_lang('Category'),
1612
                $tabCat
1613
            );
1614
1615
            global $text;
1616
1617
            switch ($this->type) {
1618
                case UNIQUE_ANSWER:
1619
                    $buttonGroup = [];
1620
                    $buttonGroup[] = $form->addButtonSave(
1621
                        $text,
1622
                        'submitQuestion',
1623
                        true
1624
                    );
1625
                    $buttonGroup[] = $form->addButton(
1626
                        'convertAnswer',
1627
                        get_lang('ConvertToMultipleAnswer'),
1628
                        'dot-circle-o',
1629
                        'default',
1630
                        null,
1631
                        null,
1632
                        null,
1633
                        true
1634
                    );
1635
                    $form->addGroup($buttonGroup);
1636
                    break;
1637
                case MULTIPLE_ANSWER:
1638
                    $buttonGroup = [];
1639
                    $buttonGroup[] = $form->addButtonSave(
1640
                        $text,
1641
                        'submitQuestion',
1642
                        true
1643
                    );
1644
                    $buttonGroup[] = $form->addButton(
1645
                        'convertAnswer',
1646
                        get_lang('ConvertToUniqueAnswer'),
1647
                        'check-square-o',
1648
                        'default',
1649
                        null,
1650
                        null,
1651
                        null,
1652
                        true
1653
                    );
1654
                    $form->addGroup($buttonGroup);
1655
                    break;
1656
            }
1657
            //Medias
1658
            //$course_medias = self::prepare_course_media_select(api_get_course_int_id());
1659
            //$form->addElement('select', 'parent_id', get_lang('AttachToMedia'), $course_medias);
1660
        }
1661
1662
        $form->addElement('html', '</div>');
1663
1664
        if (!isset($_GET['fromExercise'])) {
1665
            switch ($answerType) {
1666
                case 1:
1667
                    $this->question = get_lang('DefaultUniqueQuestion');
1668
                    break;
1669
                case 2:
1670
                    $this->question = get_lang('DefaultMultipleQuestion');
1671
                    break;
1672
                case 3:
1673
                    $this->question = get_lang('DefaultFillBlankQuestion');
1674
                    break;
1675
                case 4:
1676
                    $this->question = get_lang('DefaultMathingQuestion');
1677
                    break;
1678
                case 5:
1679
                    $this->question = get_lang('DefaultOpenQuestion');
1680
                    break;
1681
                case 9:
1682
                    $this->question = get_lang('DefaultMultipleQuestion');
1683
                    break;
1684
            }
1685
        }
1686
1687
        if (!is_null($exercise)) {
1688
            if ($exercise->questionFeedbackEnabled && $this->showFeedback($exercise)) {
1689
                $form->addTextarea('feedback', get_lang('FeedbackIfNotCorrect'));
1690
            }
1691
        }
1692
1693
        // default values
1694
        $defaults = [];
1695
        $defaults['questionName'] = $this->question;
1696
        $defaults['questionDescription'] = $this->description;
1697
        $defaults['questionLevel'] = $this->level;
1698
        $defaults['questionCategory'] = $this->category;
1699
        $defaults['feedback'] = $this->feedback;
1700
1701
        // Came from he question pool
1702
        if (isset($_GET['fromExercise'])) {
1703
            $form->setDefaults($defaults);
1704
        }
1705
1706
        if (!empty($_REQUEST['myid'])) {
1707
            $form->setDefaults($defaults);
1708
        } else {
1709
            if ($isContent == 1) {
1710
                $form->setDefaults($defaults);
1711
            }
1712
        }
1713
    }
1714
1715
    /**
1716
     * function which process the creation of questions.
1717
     *
1718
     * @param FormValidator $form
1719
     * @param Exercise      $exercise
1720
     */
1721
    public function processCreation($form, $exercise)
1722
    {
1723
        $this->updateTitle($form->getSubmitValue('questionName'));
1724
        $this->updateDescription($form->getSubmitValue('questionDescription'));
1725
        $this->updateLevel($form->getSubmitValue('questionLevel'));
1726
        $this->updateCategory($form->getSubmitValue('questionCategory'));
1727
        $this->setFeedback($form->getSubmitValue('feedback'));
1728
1729
        //Save normal question if NOT media
1730
        if ($this->type != MEDIA_QUESTION) {
1731
            $this->save($exercise);
1732
1733
            // modify the exercise
1734
            $exercise->addToList($this->id);
1735
            $exercise->update_question_positions();
1736
        }
1737
    }
1738
1739
    /**
1740
     * abstract function which creates the form to create / edit the answers of the question.
1741
     *
1742
     * @param FormValidator $form
1743
     */
1744
    abstract public function createAnswersForm($form);
1745
1746
    /**
1747
     * abstract function which process the creation of answers.
1748
     *
1749
     * @param FormValidator $form
1750
     * @param Exercise      $exercise
1751
     */
1752
    abstract public function processAnswersCreation($form, $exercise);
1753
1754
    /**
1755
     * Displays the menu of question types.
1756
     *
1757
     * @param Exercise $objExercise
1758
     */
1759
    public static function display_type_menu($objExercise)
1760
    {
1761
        $feedback_type = $objExercise->feedback_type;
1762
        $exerciseId = $objExercise->id;
1763
1764
        // 1. by default we show all the question types
1765
        $question_type_custom_list = self::get_question_type_list();
1766
1767
        if (!isset($feedback_type)) {
1768
            $feedback_type = 0;
1769
        }
1770
1771
        if ($feedback_type == 1) {
1772
            //2. but if it is a feedback DIRECT we only show the UNIQUE_ANSWER type that is currently available
1773
            $question_type_custom_list = [
1774
                UNIQUE_ANSWER => self::$questionTypes[UNIQUE_ANSWER],
1775
                HOT_SPOT_DELINEATION => self::$questionTypes[HOT_SPOT_DELINEATION],
1776
            ];
1777
        } else {
1778
            unset($question_type_custom_list[HOT_SPOT_DELINEATION]);
1779
        }
1780
1781
        echo '<div class="well">';
1782
        echo '<ul class="question_menu">';
1783
        foreach ($question_type_custom_list as $i => $a_type) {
1784
            // @todo remove require_once classes are already loaded using composer
1785
            // include the class of the type
1786
            require_once $a_type[0];
1787
            // get the picture of the type and the langvar which describes it
1788
            $img = $explanation = '';
1789
            eval('$img = '.$a_type[1].'::$typePicture;');
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
1790
            eval('$explanation = get_lang('.$a_type[1].'::$explanationLangVar);');
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
1791
            echo '<li>';
1792
            echo '<div class="icon-image">';
1793
            $icon = '<a href="admin.php?'.api_get_cidreq().'&newQuestion=yes&answerType='.$i.'">'.
1794
                Display::return_icon($img, $explanation, null, ICON_SIZE_BIG).'</a>';
1795
1796
            if ($objExercise->force_edit_exercise_in_lp === false) {
1797
                if ($objExercise->exercise_was_added_in_lp == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1798
                    $img = pathinfo($img);
1799
                    $img = $img['filename'].'_na.'.$img['extension'];
1800
                    $icon = Display::return_icon($img, $explanation, null, ICON_SIZE_BIG);
1801
                }
1802
            }
1803
1804
            echo $icon;
1805
            echo '</div>';
1806
            echo '</li>';
1807
        }
1808
1809
        echo '<li>';
1810
        echo '<div class="icon_image_content">';
1811
        if ($objExercise->exercise_was_added_in_lp == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1812
            echo Display::return_icon(
1813
                'database_na.png',
1814
                get_lang('GetExistingQuestion'),
1815
                null,
1816
                ICON_SIZE_BIG
1817
            );
1818
        } else {
1819
            if ($feedback_type == 1) {
1820
                echo $url = "<a href=\"question_pool.php?".api_get_cidreq()."&type=1&fromExercise=$exerciseId\">";
1821
            } else {
1822
                echo $url = '<a href="question_pool.php?'.api_get_cidreq().'&fromExercise='.$exerciseId.'">';
1823
            }
1824
            echo Display::return_icon(
1825
                'database.png',
1826
                get_lang('GetExistingQuestion'),
1827
                null,
1828
                ICON_SIZE_BIG
1829
            );
1830
        }
1831
        echo '</a>';
1832
        echo '</div></li>';
1833
        echo '</ul>';
1834
        echo '</div>';
1835
    }
1836
1837
    /**
1838
     * @param int    $question_id
1839
     * @param string $name
1840
     * @param int    $course_id
1841
     * @param int    $position
1842
     *
1843
     * @return false|string
1844
     */
1845
    public static function saveQuestionOption($question_id, $name, $course_id, $position = 0)
1846
    {
1847
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1848
        $params['question_id'] = intval($question_id);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$params was never initialized. Although not strictly required by PHP, it is generally a good practice to add $params = array(); before regardless.
Loading history...
1849
        $params['name'] = $name;
1850
        $params['position'] = $position;
1851
        $params['c_id'] = $course_id;
1852
        $result = self::readQuestionOption($question_id, $course_id);
1853
        $last_id = Database::insert($table, $params);
1854
        if ($last_id) {
1855
            $sql = "UPDATE $table SET id = iid WHERE iid = $last_id";
1856
            Database::query($sql);
1857
        }
1858
1859
        return $last_id;
1860
    }
1861
1862
    /**
1863
     * @param int $question_id
1864
     * @param int $course_id
1865
     */
1866
    public static function deleteAllQuestionOptions($question_id, $course_id)
1867
    {
1868
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1869
        Database::delete(
1870
            $table,
1871
            [
1872
                'c_id = ? AND question_id = ?' => [
1873
                    $course_id,
1874
                    $question_id,
1875
                ],
1876
            ]
1877
        );
1878
    }
1879
1880
    /**
1881
     * @param int   $id
1882
     * @param array $params
1883
     * @param int   $course_id
1884
     *
1885
     * @return bool|int
1886
     */
1887
    public static function updateQuestionOption($id, $params, $course_id)
1888
    {
1889
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1890
        $result = Database::update(
1891
            $table,
1892
            $params,
1893
            ['c_id = ? AND id = ?' => [$course_id, $id]]
1894
        );
1895
1896
        return $result;
1897
    }
1898
1899
    /**
1900
     * @param int $question_id
1901
     * @param int $course_id
1902
     *
1903
     * @return array
1904
     */
1905
    public static function readQuestionOption($question_id, $course_id)
1906
    {
1907
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1908
        $result = Database::select(
1909
            '*',
1910
            $table,
1911
            [
1912
                'where' => [
1913
                    'c_id = ? AND question_id = ?' => [
1914
                        $course_id,
1915
                        $question_id,
1916
                    ],
1917
                ],
1918
                'order' => 'id ASC',
1919
            ]
1920
        );
1921
1922
        return $result;
1923
    }
1924
1925
    /**
1926
     * Shows question title an description.
1927
     *
1928
     * @param Exercise $exercise
1929
     * @param int      $counter
1930
     * @param array    $score
1931
     *
1932
     * @return string HTML string with the header of the question (before the answers table)
1933
     */
1934
    public function return_header($exercise, $counter = null, $score = [])
1935
    {
1936
        $counterLabel = '';
1937
        if (!empty($counter)) {
1938
            $counterLabel = intval($counter);
1939
        }
1940
        $score_label = get_lang('Wrong');
1941
        $class = 'error';
1942
        if ($score['pass'] == true) {
1943
            $score_label = get_lang('Correct');
1944
            $class = 'success';
1945
        }
1946
1947
        if (in_array($this->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION])) {
1948
            $score['revised'] = isset($score['revised']) ? $score['revised'] : false;
1949
            if ($score['revised'] == true) {
1950
                $score_label = get_lang('Revised');
1951
                $class = '';
1952
            } else {
1953
                $score_label = get_lang('NotRevised');
1954
                $class = 'warning';
1955
                $weight = float_format($score['weight'], 1);
1956
                $score['result'] = " ? / ".$weight;
1957
                $model = ExerciseLib::getCourseScoreModel();
1958
                if (!empty($model)) {
1959
                    $score['result'] = " ? ";
1960
                }
1961
1962
                $hide = api_get_configuration_value('hide_free_question_score');
1963
                if ($hide === true) {
1964
                    $score['result'] = '-';
1965
                }
1966
            }
1967
        }
1968
1969
        // display question category, if any
1970
        $header = '';
1971
        if ($exercise->display_category_name) {
1972
            $header = TestCategory::returnCategoryAndTitle($this->id);
1973
        }
1974
        $show_media = '';
1975
        if ($show_media) {
1976
            $header .= $this->show_media_content();
1977
        }
1978
        $scoreCurrent = [
1979
            'used' => $score['score'],
1980
            'missing' => $score['weight'],
1981
        ];
1982
        $header .= Display::page_subheader2($counterLabel.'. '.$this->question);
1983
        $header .= $exercise->getQuestionRibbon($class, $score_label, $score['result'], $scoreCurrent);
1984
        if ($this->type != READING_COMPREHENSION) {
1985
            // Do not show the description (the text to read) if the question is of type READING_COMPREHENSION
1986
            $header .= Display::div(
1987
                $this->description,
1988
                ['class' => 'question_description']
1989
            );
1990
        } else {
1991
            if ($score['pass'] == true) {
1992
                $message = Display::div(
1993
                    sprintf(
1994
                        get_lang('ReadingQuestionCongratsSpeedXReachedForYWords'),
1995
                        ReadingComprehension::$speeds[$this->level],
1996
                        $this->getWordsCount()
0 ignored issues
show
Bug introduced by
The method getWordsCount() does not exist on Question. It seems like you code against a sub-type of Question such as ReadingComprehension. ( Ignorable by Annotation )

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

1996
                        $this->/** @scrutinizer ignore-call */ 
1997
                               getWordsCount()
Loading history...
1997
                    )
1998
                );
1999
            } else {
2000
                $message = Display::div(
2001
                    sprintf(
2002
                        get_lang('ReadingQuestionCongratsSpeedXNotReachedForYWords'),
2003
                        ReadingComprehension::$speeds[$this->level],
2004
                        $this->getWordsCount()
2005
                    )
2006
                );
2007
            }
2008
            $header .= $message.'<br />';
2009
        }
2010
2011
        if (isset($score['pass']) && $score['pass'] === false) {
2012
            if ($this->showFeedback($exercise)) {
2013
                $header .= $this->returnFormatFeedback();
2014
            }
2015
        }
2016
2017
        return $header;
2018
    }
2019
2020
    /**
2021
     * Create a question from a set of parameters.
2022
     *
2023
     * @param   int     Quiz ID
2024
     * @param   string  Question name
2025
     * @param   int     Maximum result for the question
2026
     * @param   int     Type of question (see constants at beginning of question.class.php)
2027
     * @param   int     Question level/category
2028
     * @param string $quiz_id
2029
     */
2030
    public function create_question(
2031
        $quiz_id,
2032
        $question_name,
2033
        $question_description = '',
2034
        $max_score = 0,
2035
        $type = 1,
2036
        $level = 1
2037
    ) {
2038
        $course_id = api_get_course_int_id();
2039
        $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2040
        $tbl_quiz_rel_question = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
2041
2042
        $quiz_id = intval($quiz_id);
2043
        $max_score = (float) $max_score;
2044
        $type = intval($type);
2045
        $level = intval($level);
2046
2047
        // Get the max position
2048
        $sql = "SELECT max(position) as max_position
2049
                FROM $tbl_quiz_question q 
2050
                INNER JOIN $tbl_quiz_rel_question r
2051
                ON
2052
                    q.id = r.question_id AND
2053
                    exercice_id = $quiz_id AND
2054
                    q.c_id = $course_id AND
2055
                    r.c_id = $course_id";
2056
        $rs_max = Database::query($sql);
2057
        $row_max = Database::fetch_object($rs_max);
2058
        $max_position = $row_max->max_position + 1;
2059
2060
        $params = [
2061
            'c_id' => $course_id,
2062
            'question' => $question_name,
2063
            'description' => $question_description,
2064
            'ponderation' => $max_score,
2065
            'position' => $max_position,
2066
            'type' => $type,
2067
            'level' => $level,
2068
        ];
2069
        $question_id = Database::insert($tbl_quiz_question, $params);
2070
2071
        if ($question_id) {
2072
            $sql = "UPDATE $tbl_quiz_question  
2073
                    SET id = iid WHERE iid = $question_id";
2074
            Database::query($sql);
2075
2076
            // Get the max question_order
2077
            $sql = "SELECT max(question_order) as max_order
2078
                    FROM $tbl_quiz_rel_question
2079
                    WHERE c_id = $course_id AND exercice_id = $quiz_id ";
2080
            $rs_max_order = Database::query($sql);
2081
            $row_max_order = Database::fetch_object($rs_max_order);
2082
            $max_order = $row_max_order->max_order + 1;
2083
            // Attach questions to quiz
2084
            $sql = "INSERT INTO $tbl_quiz_rel_question (c_id, question_id, exercice_id, question_order)
2085
                    VALUES($course_id, $question_id, $quiz_id, $max_order)";
2086
            Database::query($sql);
2087
        }
2088
2089
        return $question_id;
2090
    }
2091
2092
    /**
2093
     * @return array the image filename of the question type
2094
     */
2095
    public function get_type_icon_html()
2096
    {
2097
        $type = $this->selectType();
2098
        $tabQuestionList = self::get_question_type_list(); // [0]=file to include [1]=type name
2099
2100
        require_once $tabQuestionList[$type][0];
2101
2102
        $img = $explanation = null;
2103
        eval('$img = '.$tabQuestionList[$type][1].'::$typePicture;');
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
2104
        eval('$explanation = get_lang('.$tabQuestionList[$type][1].'::$explanationLangVar);');
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
2105
2106
        return [$img, $explanation];
2107
    }
2108
2109
    /**
2110
     * Get course medias.
2111
     *
2112
     * @param int course id
2113
     * @param int $course_id
2114
     *
2115
     * @return array
2116
     */
2117
    public static function get_course_medias(
2118
        $course_id,
2119
        $start = 0,
2120
        $limit = 100,
2121
        $sidx = "question",
2122
        $sord = "ASC",
2123
        $where_condition = []
2124
    ) {
2125
        $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2126
        $default_where = [
2127
            'c_id = ? AND parent_id = 0 AND type = ?' => [
2128
                $course_id,
2129
                MEDIA_QUESTION,
2130
            ],
2131
        ];
2132
        $result = Database::select(
2133
            '*',
2134
            $table_question,
2135
            [
2136
                'limit' => " $start, $limit",
2137
                'where' => $default_where,
2138
                'order' => "$sidx $sord",
2139
            ]
2140
        );
2141
2142
        return $result;
2143
    }
2144
2145
    /**
2146
     * Get count course medias.
2147
     *
2148
     * @param int course id
2149
     *
2150
     * @return int
2151
     */
2152
    public static function get_count_course_medias($course_id)
2153
    {
2154
        $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2155
        $result = Database::select(
2156
            'count(*) as count',
2157
            $table_question,
2158
            [
2159
                'where' => [
2160
                    'c_id = ? AND parent_id = 0 AND type = ?' => [
2161
                        $course_id,
2162
                        MEDIA_QUESTION,
2163
                    ],
2164
                ],
2165
            ],
2166
            'first'
2167
        );
2168
2169
        if ($result && isset($result['count'])) {
2170
            return $result['count'];
2171
        }
2172
2173
        return 0;
2174
    }
2175
2176
    /**
2177
     * @param int $course_id
2178
     *
2179
     * @return array
2180
     */
2181
    public static function prepare_course_media_select($course_id)
2182
    {
2183
        $medias = self::get_course_medias($course_id);
2184
        $media_list = [];
2185
        $media_list[0] = get_lang('NoMedia');
2186
2187
        if (!empty($medias)) {
2188
            foreach ($medias as $media) {
2189
                $media_list[$media['id']] = empty($media['question']) ? get_lang('Untitled') : $media['question'];
2190
            }
2191
        }
2192
2193
        return $media_list;
2194
    }
2195
2196
    /**
2197
     * @return array
2198
     */
2199
    public static function get_default_levels()
2200
    {
2201
        $levels = [
2202
            1 => 1,
2203
            2 => 2,
2204
            3 => 3,
2205
            4 => 4,
2206
            5 => 5,
2207
        ];
2208
2209
        return $levels;
2210
    }
2211
2212
    /**
2213
     * @return string
2214
     */
2215
    public function show_media_content()
2216
    {
2217
        $html = '';
2218
        if ($this->parent_id != 0) {
2219
            $parent_question = self::read($this->parent_id);
2220
            $html = $parent_question->show_media_content();
2221
        } else {
2222
            $html .= Display::page_subheader($this->selectTitle());
2223
            $html .= $this->selectDescription();
2224
        }
2225
2226
        return $html;
2227
    }
2228
2229
    /**
2230
     * Swap between unique and multiple type answers.
2231
     *
2232
     * @return UniqueAnswer|MultipleAnswer
2233
     */
2234
    public function swapSimpleAnswerTypes()
2235
    {
2236
        $oppositeAnswers = [
2237
            UNIQUE_ANSWER => MULTIPLE_ANSWER,
2238
            MULTIPLE_ANSWER => UNIQUE_ANSWER,
2239
        ];
2240
        $this->type = $oppositeAnswers[$this->type];
2241
        Database::update(
2242
            Database::get_course_table(TABLE_QUIZ_QUESTION),
2243
            ['type' => $this->type],
2244
            ['c_id = ? AND id = ?' => [$this->course['real_id'], $this->id]]
2245
        );
2246
        $answerClasses = [
2247
            UNIQUE_ANSWER => 'UniqueAnswer',
2248
            MULTIPLE_ANSWER => 'MultipleAnswer',
2249
        ];
2250
        $swappedAnswer = new $answerClasses[$this->type]();
2251
        foreach ($this as $key => $value) {
2252
            $swappedAnswer->$key = $value;
2253
        }
2254
2255
        return $swappedAnswer;
2256
    }
2257
2258
    /**
2259
     * @param array $score
2260
     *
2261
     * @return bool
2262
     */
2263
    public function isQuestionWaitingReview($score)
2264
    {
2265
        $isReview = false;
2266
        if (!empty($score)) {
2267
            if (!empty($score['comments']) || $score['score'] > 0) {
2268
                $isReview = true;
2269
            }
2270
        }
2271
2272
        return $isReview;
2273
    }
2274
2275
    /**
2276
     * @param string $value
2277
     */
2278
    public function setFeedback($value)
2279
    {
2280
        $this->feedback = $value;
2281
    }
2282
2283
    /**
2284
     * @param Exercise $exercise
2285
     *
2286
     * @return bool
2287
     */
2288
    public function showFeedback($exercise)
2289
    {
2290
        return
2291
            in_array($this->type, $this->questionTypeWithFeedback) &&
2292
            $exercise->feedback_type != EXERCISE_FEEDBACK_TYPE_EXAM;
2293
    }
2294
2295
    /**
2296
     * @return string
2297
     */
2298
    public function returnFormatFeedback()
2299
    {
2300
        return '<br />'.Display::return_message($this->feedback, 'normal', false);
2301
    }
2302
2303
    /**
2304
     * Resizes a picture || Warning!: can only be called after uploadPicture,
2305
     * or if picture is already available in object.
2306
     *
2307
     * @param string $Dimension - Resizing happens proportional according to given dimension: height|width|any
2308
     * @param int    $Max       - Maximum size
2309
     *
2310
     * @return bool|null - true if success, false if failed
2311
     *
2312
     * @author Toon Keppens
2313
     */
2314
    private function resizePicture($Dimension, $Max)
2315
    {
2316
        // if the question has an ID
2317
        if (!$this->id) {
2318
            return false;
2319
        }
2320
2321
        $picturePath = $this->getHotSpotFolderInCourse().'/'.$this->getPictureFilename();
2322
2323
        // Get dimensions from current image.
2324
        $my_image = new Image($picturePath);
2325
2326
        $current_image_size = $my_image->get_image_size();
2327
        $current_width = $current_image_size['width'];
2328
        $current_height = $current_image_size['height'];
2329
2330
        if ($current_width < $Max && $current_height < $Max) {
2331
            return true;
2332
        } elseif ($current_height == '') {
2333
            return false;
2334
        }
2335
2336
        // Resize according to height.
2337
        if ($Dimension == "height") {
2338
            $resize_scale = $current_height / $Max;
2339
            $new_width = ceil($current_width / $resize_scale);
2340
        }
2341
2342
        // Resize according to width
2343
        if ($Dimension == "width") {
2344
            $new_width = $Max;
2345
        }
2346
2347
        // Resize according to height or width, both should not be larger than $Max after resizing.
2348
        if ($Dimension == "any") {
2349
            if ($current_height > $current_width || $current_height == $current_width) {
2350
                $resize_scale = $current_height / $Max;
2351
                $new_width = ceil($current_width / $resize_scale);
2352
            }
2353
            if ($current_height < $current_width) {
2354
                $new_width = $Max;
2355
            }
2356
        }
2357
2358
        $my_image->resize($new_width);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $new_width does not seem to be defined for all execution paths leading up to this point.
Loading history...
2359
        $result = $my_image->send_image($picturePath);
2360
2361
        if ($result) {
2362
            return true;
2363
        }
2364
2365
        return false;
2366
    }
2367
}
2368