Completed
Push — master ( cf4f4c...f3c478 )
by Julito
36:36
created

Question::search_engine_edit()   F

Complexity

Conditions 27
Paths 1123

Size

Total Lines 138
Code Lines 79

Duplication

Lines 0
Ratio 0 %

Importance

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

394
            /** @scrutinizer ignore-type */ $pictureId,
Loading history...
395
            $courseInfo['code'],
396
            false,
397
            $sessionId
398
        );
399
        $documentFilename = '';
400
        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...
401
            // document in document/images folder
402
            $documentFilename = pathinfo(
403
                $documentInfo['path'],
404
                PATHINFO_BASENAME
405
            );
406
        }
407
408
        return $documentFilename;
409
    }
410
411
    /**
412
     * @param int $courseId
413
     * @param int $sessionId
414
     *
415
     * @return false|CDocument
416
     */
417
    public function getPicture($courseId = 0, $sessionId = 0)
418
    {
419
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
420
        $sessionId = empty($sessionId) ? api_get_session_id() : (int) $sessionId;
421
422
        if (empty($courseId)) {
423
            return false;
424
        }
425
426
        $pictureId = $this->getPictureId();
427
        $courseInfo = $this->course;
428
        $documentInfo = DocumentManager::get_document_data_by_id(
429
            $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

429
            /** @scrutinizer ignore-type */ $pictureId,
Loading history...
430
            $courseInfo['code'],
431
            false,
432
            $sessionId
433
        );
434
435
        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...
436
            $em = Database::getManager();
437
438
            /** @var CDocument $document */
439
            $document = $em->getRepository('ChamiloCourseBundle:CDocument')->find($documentInfo['iid']);
440
441
            return $document;
442
        }
443
444
        return false;
445
    }
446
447
    /**
448
     * returns the array with the exercise ID list.
449
     *
450
     * @author Olivier Brouckaert
451
     *
452
     * @return array - list of exercise ID which the question is in
453
     */
454
    public function selectExerciseList()
455
    {
456
        return $this->exerciseList;
457
    }
458
459
    /**
460
     * returns the number of exercises which this question is in.
461
     *
462
     * @author Olivier Brouckaert
463
     *
464
     * @return int - number of exercises
465
     */
466
    public function selectNbrExercises()
467
    {
468
        return count($this->exerciseList);
469
    }
470
471
    /**
472
     * changes the question title.
473
     *
474
     * @param string $title - question title
475
     *
476
     * @author Olivier Brouckaert
477
     */
478
    public function updateTitle($title)
479
    {
480
        $this->question = $title;
481
    }
482
483
    /**
484
     * @param int $id
485
     */
486
    public function updateParentId($id)
487
    {
488
        $this->parent_id = (int) $id;
489
    }
490
491
    /**
492
     * changes the question description.
493
     *
494
     * @param string $description - question description
495
     *
496
     * @author Olivier Brouckaert
497
     */
498
    public function updateDescription($description)
499
    {
500
        $this->description = $description;
501
    }
502
503
    /**
504
     * changes the question weighting.
505
     *
506
     * @param int $weighting - question weighting
507
     *
508
     * @author Olivier Brouckaert
509
     */
510
    public function updateWeighting($weighting)
511
    {
512
        $this->weighting = $weighting;
513
    }
514
515
    /**
516
     * @param array $category
517
     *
518
     * @author Hubert Borderiou 12-10-2011
519
     */
520
    public function updateCategory($category)
521
    {
522
        $this->category = $category;
523
    }
524
525
    /**
526
     * @param int $value
527
     *
528
     * @author Hubert Borderiou 12-10-2011
529
     */
530
    public function updateScoreAlwaysPositive($value)
531
    {
532
        $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...
533
    }
534
535
    /**
536
     * @param int $value
537
     *
538
     * @author Hubert Borderiou 12-10-2011
539
     */
540
    public function updateUncheckedMayScore($value)
541
    {
542
        $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...
543
    }
544
545
    /**
546
     * in this version, a question can only have 1 category
547
     * if category is 0, then question has no category then delete the category entry.
548
     *
549
     * @param int $categoryId
550
     * @param int $courseId
551
     *
552
     * @return bool
553
     *
554
     * @author Hubert Borderiou 12-10-2011
555
     */
556
    public function saveCategory($categoryId, $courseId = 0)
557
    {
558
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
559
560
        if (empty($courseId)) {
561
            return false;
562
        }
563
564
        if ($categoryId <= 0) {
565
            $this->deleteCategory($courseId);
566
        } else {
567
            // update or add category for a question
568
            $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
569
            $categoryId = (int) $categoryId;
570
            $question_id = (int) $this->id;
571
            $sql = "SELECT count(*) AS nb FROM $table
572
                    WHERE
573
                        question_id = $question_id AND
574
                        c_id = ".$courseId;
575
            $res = Database::query($sql);
576
            $row = Database::fetch_array($res);
577
            if ($row['nb'] > 0) {
578
                $sql = "UPDATE $table
579
                        SET category_id = $categoryId
580
                        WHERE
581
                            question_id = $question_id AND
582
                            c_id = ".$courseId;
583
                Database::query($sql);
584
            } else {
585
                $sql = "INSERT INTO $table (c_id, question_id, category_id)
586
                        VALUES (".$courseId.", $question_id, $categoryId)";
587
                Database::query($sql);
588
            }
589
590
            return true;
591
        }
592
    }
593
594
    /**
595
     * @author hubert borderiou 12-10-2011
596
     *
597
     * @param int $courseId
598
     *                      delete any category entry for question id
599
     *                      delete the category for question
600
     *
601
     * @return bool
602
     */
603
    public function deleteCategory($courseId = 0)
604
    {
605
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
606
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
607
        $questionId = (int) $this->id;
608
        if (empty($courseId) || empty($questionId)) {
609
            return false;
610
        }
611
        $sql = "DELETE FROM $table
612
                WHERE
613
                    question_id = $questionId AND
614
                    c_id = ".$courseId;
615
        Database::query($sql);
616
617
        return true;
618
    }
619
620
    /**
621
     * changes the question position.
622
     *
623
     * @param int $position - question position
624
     *
625
     * @author Olivier Brouckaert
626
     */
627
    public function updatePosition($position)
628
    {
629
        $this->position = $position;
630
    }
631
632
    /**
633
     * changes the question level.
634
     *
635
     * @param int $level - question level
636
     *
637
     * @author Nicolas Raynaud
638
     */
639
    public function updateLevel($level)
640
    {
641
        $this->level = $level;
642
    }
643
644
    /**
645
     * changes the answer type. If the user changes the type from "unique answer" to "multiple answers"
646
     * (or conversely) answers are not deleted, otherwise yes.
647
     *
648
     * @param int $type - answer type
649
     *
650
     * @author Olivier Brouckaert
651
     */
652
    public function updateType($type)
653
    {
654
        $table = Database::get_course_table(TABLE_QUIZ_ANSWER);
655
        $course_id = $this->course['real_id'];
656
657
        if (empty($course_id)) {
658
            $course_id = api_get_course_int_id();
659
        }
660
        // if we really change the type
661
        if ($type != $this->type) {
662
            // if we don't change from "unique answer" to "multiple answers" (or conversely)
663
            if (!in_array($this->type, [UNIQUE_ANSWER, MULTIPLE_ANSWER]) ||
664
                !in_array($type, [UNIQUE_ANSWER, MULTIPLE_ANSWER])
665
            ) {
666
                // removes old answers
667
                $sql = "DELETE FROM $table
668
                        WHERE c_id = $course_id AND question_id = ".intval($this->id);
669
                Database::query($sql);
670
            }
671
672
            $this->type = $type;
673
        }
674
    }
675
676
    /**
677
     * Get default hot spot folder in documents.
678
     *
679
     * @param array $courseInfo
680
     *
681
     * @return CDocument
682
     */
683
    public function getHotSpotFolderInCourse($courseInfo = [])
684
    {
685
        $courseInfo = empty($courseInfo) ? $this->course : $courseInfo;
686
687
        if (empty($courseInfo) || empty($courseInfo['directory'])) {
688
            // Stop everything if course is not set.
689
            api_not_allowed();
690
        }
691
692
        $pictureAbsolutePath = api_get_path(SYS_COURSE_PATH).$courseInfo['directory'].'/document/images/';
693
        $picturePath = basename($pictureAbsolutePath);
694
695
        $folder = create_unexisting_directory(
696
            $courseInfo,
697
            api_get_user_id(),
698
            0,
699
            0,
700
            0,
701
            dirname($pictureAbsolutePath),
702
            '/'.$picturePath,
703
            $picturePath,
704
            '',
705
            false,
706
            false
707
        );
708
709
        return $folder;
710
    }
711
712
    /**
713
     * adds a picture to the question.
714
     *
715
     * @param array $picture - picture to upload
716
     *
717
     * @return bool - true if uploaded, otherwise false
718
     */
719
    public function uploadPicture($picture)
720
    {
721
        $folder = $this->getHotSpotFolderInCourse();
722
723
        // if the question has got an ID
724
        if ($folder && $this->id) {
725
            $document = DocumentManager::upload_document(
726
                ['imageUpload' => $picture],
727
                '/images',
728
                '',
729
                '',
730
                false,
731
                'overwrite',
732
                false,
733
                true,
734
                'imageUpload',
735
                true,
736
                $folder->getId()
737
            );
738
739
            if ($document) {
740
                $this->picture = $document->getIid();
741
742
                return true;
743
            }
744
        }
745
746
        return false;
747
    }
748
749
    /**
750
     * return the name for image use in hotspot question
751
     * to be unique, name is quiz-[utc unix timestamp].jpg.
752
     *
753
     * @param string $prefix
754
     * @param string $extension
755
     *
756
     * @return string
757
     */
758
    public function generatePictureName($prefix = 'quiz-', $extension = 'jpg')
759
    {
760
        // image name is quiz-xxx.jpg in folder images/
761
        $utcTime = time();
762
763
        return $prefix.$utcTime.'.'.$extension;
764
    }
765
766
    /**
767
     *  Deletes a hot spot picture.
768
     *
769
     * @return bool - true
770
     */
771
    public function removePicture()
772
    {
773
        $picture = $this->getPicture();
774
775
        if ($picture) {
776
            $manager = Database::getManager();
777
            $manager->remove($picture);
778
            $manager->flush();
779
780
            return true;
781
        }
782
783
        return false;
784
    }
785
786
    /**
787
     * Exports a picture to another question.
788
     *
789
     * @author Olivier Brouckaert
790
     *
791
     * @param int   $questionId - ID of the target question
792
     * @param array $courseInfo destination course info
793
     *
794
     * @return bool - true if copied, otherwise false
795
     */
796
    public function exportPicture($questionId, $courseInfo)
797
    {
798
        if (empty($questionId) || empty($courseInfo)) {
799
            return false;
800
        }
801
802
        $course_id = $courseInfo['real_id'];
803
        $destination_path = $this->getHotSpotFolderInCourse($courseInfo);
804
805
        if (empty($destination_path)) {
806
            return false;
807
        }
808
809
        $source_path = $this->getHotSpotFolderInCourse();
810
811
        // if the question has got an ID and if the picture exists
812
        if (!$this->id || empty($this->picture)) {
813
            return false;
814
        }
815
816
        $sourcePictureName = $this->getPictureFilename($course_id);
817
        $picture = $this->generatePictureName();
818
        $result = false;
819
        if (file_exists($source_path.'/'.$sourcePictureName)) {
820
            // for backward compatibility
821
            $result = copy(
822
                $source_path.'/'.$sourcePictureName,
823
                $destination_path.'/'.$picture
824
            );
825
        } else {
826
            $imageInfo = DocumentManager::get_document_data_by_id(
827
                $this->picture,
828
                $courseInfo['code']
829
            );
830
            if (file_exists($imageInfo['absolute_path'])) {
831
                $result = @copy(
832
                    $imageInfo['absolute_path'],
833
                    $destination_path.'/'.$picture
834
                );
835
            }
836
        }
837
838
        // If copy was correct then add to the database
839
        if (!$result) {
840
            return false;
841
        }
842
843
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION);
844
        $sql = "UPDATE $table SET
845
                picture = '".Database::escape_string($picture)."'
846
                WHERE c_id = $course_id AND id='".intval($questionId)."'";
847
        Database::query($sql);
848
849
        $documentId = DocumentManager::addDocument(
850
            $courseInfo,
851
            '/images/'.$picture,
852
            'file',
853
            filesize($destination_path.'/'.$picture),
854
            $picture
855
        );
856
857
        if (!$documentId) {
858
            return false;
859
        }
860
861
        return true;
862
    }
863
864
    /**
865
     * Set title.
866
     *
867
     * @param string $title
868
     */
869
    public function setTitle($title)
870
    {
871
        $this->question = $title;
872
    }
873
874
    /**
875
     * Sets extra info.
876
     *
877
     * @param string $extra
878
     */
879
    public function setExtra($extra)
880
    {
881
        $this->extra = $extra;
882
    }
883
884
    /**
885
     * updates the question in the data base
886
     * if an exercise ID is provided, we add that exercise ID into the exercise list.
887
     *
888
     * @author Olivier Brouckaert
889
     *
890
     * @param Exercise $exercise
891
     */
892
    public function save($exercise)
893
    {
894
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
895
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
896
        $em = Database::getManager();
897
        $exerciseId = $exercise->id;
898
899
        $id = $this->id;
900
        $question = $this->question;
901
        $description = $this->description;
902
        $weighting = $this->weighting;
903
        $position = $this->position;
904
        $type = $this->type;
905
        $picture = $this->picture;
906
        $level = $this->level;
907
        $extra = $this->extra;
908
        $c_id = $this->course['real_id'];
909
        $categoryId = $this->category;
910
911
        $questionRepo = Container::getQuestionRepository();
912
        $exerciseRepo = Container::getExerciseRepository();
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
930
            Database::update(
931
                $TBL_QUESTIONS,
932
                $params,
933
                ['c_id = ? AND id = ?' => [$c_id, $id]]
934
            );
935
936
            Event::addEvent(
0 ignored issues
show
Bug introduced by
The method addEvent() does not exist on Event. ( Ignorable by Annotation )

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

936
            Event::/** @scrutinizer ignore-call */ 
937
                   addEvent(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
937
                LOG_QUESTION_UPDATED,
938
                LOG_QUESTION_ID,
939
                $this->iid
940
            );
941
            $this->saveCategory($categoryId);
942
943
            if (!empty($exerciseId)) {
944
                /*api_item_property_update(
945
                    $this->course,
946
                    TOOL_QUIZ,
947
                    $id,
948
                    'QuizQuestionUpdated',
949
                    api_get_user_id()
950
                );*/
951
            }
952
953
            /** @var CQuizQuestionCategory $questionCategory */
954
            /*$questionCategory = $repo->find($this->iid);
955
            $questionCategory->setTitle('');
956
            $repo->addResourceNode($questionCategory, api_get_user_entity(api_get_user_id()));
957
            $repo->addResourceToCourse($questionCategory->getResourceNode());*/
958
959
            if (api_get_setting('search_enabled') === 'true') {
960
                $this->search_engine_edit($exerciseId);
961
            }
962
        } else {
963
            // Creates a new question
964
            $sql = "SELECT max(position)
965
                    FROM $TBL_QUESTIONS as question,
966
                    $TBL_EXERCISE_QUESTION as test_question
967
                    WHERE
968
                        question.id = test_question.question_id AND
969
                        test_question.exercice_id = ".$exerciseId." AND
970
                        question.c_id = $c_id AND
971
                        test_question.c_id = $c_id ";
972
            $result = Database::query($sql);
973
            $current_position = Database::result($result, 0, 0);
974
            $this->updatePosition($current_position + 1);
975
            $position = $this->position;
976
977
            $params = [
978
                'c_id' => $c_id,
979
                'question' => $question,
980
                'description' => $description,
981
                'ponderation' => $weighting,
982
                'position' => $position,
983
                'type' => $type,
984
                'picture' => $picture,
985
                'extra' => $extra,
986
                'level' => $level,
987
            ];
988
989
            if ($exercise->questionFeedbackEnabled) {
990
                $params['feedback'] = $this->feedback;
991
            }
992
            $this->id = Database::insert($TBL_QUESTIONS, $params);
993
994
            if ($this->id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->id of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
995
                $sql = "UPDATE $TBL_QUESTIONS SET id = iid WHERE iid = {$this->id}";
996
                Database::query($sql);
997
998
                Event::addEvent(
999
                    LOG_QUESTION_CREATED,
1000
                    LOG_QUESTION_ID,
1001
                    $this->id
1002
                );
1003
1004
                $questionEntity = $questionRepo->find($this->id);
1005
                $exerciseEntity = $exerciseRepo->find($exerciseId);
1006
                $node = $questionRepo->addResourceNode($questionEntity, api_get_user_entity(api_get_user_id()), $exerciseEntity);
0 ignored issues
show
Bug introduced by
It seems like $questionEntity can also be of type null; however, parameter $resource of Chamilo\CoreBundle\Repos...tory::addResourceNode() does only seem to accept Chamilo\CoreBundle\Entit...source\AbstractResource, 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

1006
                $node = $questionRepo->addResourceNode(/** @scrutinizer ignore-type */ $questionEntity, api_get_user_entity(api_get_user_id()), $exerciseEntity);
Loading history...
1007
                $questionRepo->addResourceNodeToCourse(
1008
                    $node,
1009
                    ResourceLink::VISIBILITY_PUBLISHED,
1010
                    api_get_course_entity(),
1011
                    api_get_session_entity(),
1012
                    api_get_group_entity()
1013
                );
1014
1015
                // If hotspot, create first answer
1016
                if ($type == HOT_SPOT || $type == HOT_SPOT_ORDER) {
1017
                    $quizAnswer = new CQuizAnswer();
1018
                    $quizAnswer
1019
                        ->setCId($c_id)
1020
                        ->setQuestionId($this->id)
1021
                        ->setAnswer('')
1022
                        ->setPonderation(10)
1023
                        ->setPosition(1)
1024
                        ->setHotspotCoordinates('0;0|0|0')
1025
                        ->setHotspotType('square');
1026
1027
                    $em->persist($quizAnswer);
1028
                    $em->flush();
1029
1030
                    $id = $quizAnswer->getIid();
1031
1032
                    if ($id) {
1033
                        $quizAnswer
1034
                            ->setId($id)
1035
                            ->setIdAuto($id);
1036
1037
                        $em->merge($quizAnswer);
1038
                        $em->flush();
1039
                    }
1040
                }
1041
1042
                if ($type == HOT_SPOT_DELINEATION) {
1043
                    $quizAnswer = new CQuizAnswer();
1044
                    $quizAnswer
1045
                        ->setCId($c_id)
1046
                        ->setQuestionId($this->id)
1047
                        ->setAnswer('')
1048
                        ->setPonderation(10)
1049
                        ->setPosition(1)
1050
                        ->setHotspotCoordinates('0;0|0|0')
1051
                        ->setHotspotType('delineation');
1052
1053
                    $em->persist($quizAnswer);
1054
                    $em->flush();
1055
1056
                    $id = $quizAnswer->getIid();
1057
1058
                    if ($id) {
1059
                        $quizAnswer
1060
                            ->setId($id)
1061
                            ->setIdAuto($id);
1062
1063
                        $em->merge($quizAnswer);
1064
                        $em->flush();
1065
                    }
1066
                }
1067
1068
                if (api_get_setting('search_enabled') === 'true') {
1069
                    $this->search_engine_edit($exerciseId, true);
1070
                }
1071
            }
1072
        }
1073
1074
        // if the question is created in an exercise
1075
        if (!empty($exerciseId)) {
1076
            // adds the exercise into the exercise list of this question
1077
            $this->addToList($exerciseId, true);
1078
        }
1079
    }
1080
1081
    /**
1082
     * @param int  $exerciseId
1083
     * @param bool $addQs
1084
     * @param bool $rmQs
1085
     */
1086
    public function search_engine_edit(
1087
        $exerciseId,
1088
        $addQs = false,
1089
        $rmQs = false
1090
    ) {
1091
        // update search engine and its values table if enabled
1092
        if (!empty($exerciseId) && api_get_setting('search_enabled') == 'true' &&
1093
            extension_loaded('xapian')
1094
        ) {
1095
            $course_id = api_get_course_id();
1096
            // get search_did
1097
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
1098
            if ($addQs || $rmQs) {
1099
                //there's only one row per question on normal db and one document per question on search engine db
1100
                $sql = 'SELECT * FROM %s
1101
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_second_level=%s LIMIT 1';
1102
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
1103
            } else {
1104
                $sql = 'SELECT * FROM %s
1105
                    WHERE course_code=\'%s\' AND tool_id=\'%s\'
1106
                    AND ref_id_high_level=%s AND ref_id_second_level=%s LIMIT 1';
1107
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
1108
            }
1109
            $res = Database::query($sql);
1110
1111
            if (Database::num_rows($res) > 0 || $addQs) {
1112
                $di = new ChamiloIndexer();
1113
                if ($addQs) {
1114
                    $question_exercises = [(int) $exerciseId];
1115
                } else {
1116
                    $question_exercises = [];
1117
                }
1118
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
1119
                $di->connectDb(null, null, $lang);
1120
1121
                // retrieve others exercise ids
1122
                $se_ref = Database::fetch_array($res);
1123
                $se_doc = $di->get_document((int) $se_ref['search_did']);
1124
                if ($se_doc !== false) {
1125
                    if (($se_doc_data = $di->get_document_data($se_doc)) !== false) {
1126
                        $se_doc_data = UnserializeApi::unserialize(
1127
                            'not_allowed_classes',
1128
                            $se_doc_data
1129
                        );
1130
                        if (isset($se_doc_data[SE_DATA]['type']) &&
1131
                            $se_doc_data[SE_DATA]['type'] == SE_DOCTYPE_EXERCISE_QUESTION
1132
                        ) {
1133
                            if (isset($se_doc_data[SE_DATA]['exercise_ids']) &&
1134
                                is_array($se_doc_data[SE_DATA]['exercise_ids'])
1135
                            ) {
1136
                                foreach ($se_doc_data[SE_DATA]['exercise_ids'] as $old_value) {
1137
                                    if (!in_array($old_value, $question_exercises)) {
1138
                                        $question_exercises[] = $old_value;
1139
                                    }
1140
                                }
1141
                            }
1142
                        }
1143
                    }
1144
                }
1145
                if ($rmQs) {
1146
                    while (($key = array_search($exerciseId, $question_exercises)) !== false) {
1147
                        unset($question_exercises[$key]);
1148
                    }
1149
                }
1150
1151
                // build the chunk to index
1152
                $ic_slide = new IndexableChunk();
1153
                $ic_slide->addValue("title", $this->question);
1154
                $ic_slide->addCourseId($course_id);
1155
                $ic_slide->addToolId(TOOL_QUIZ);
1156
                $xapian_data = [
1157
                    SE_COURSE_ID => $course_id,
1158
                    SE_TOOL_ID => TOOL_QUIZ,
1159
                    SE_DATA => [
1160
                        'type' => SE_DOCTYPE_EXERCISE_QUESTION,
1161
                        'exercise_ids' => $question_exercises,
1162
                        'question_id' => (int) $this->id,
1163
                    ],
1164
                    SE_USER => (int) api_get_user_id(),
1165
                ];
1166
                $ic_slide->xapian_data = serialize($xapian_data);
1167
                $ic_slide->addValue("content", $this->description);
1168
1169
                //TODO: index answers, see also form validation on question_admin.inc.php
1170
1171
                $di->remove_document($se_ref['search_did']);
1172
                $di->addChunk($ic_slide);
1173
1174
                //index and return search engine document id
1175
                if (!empty($question_exercises)) { // if empty there is nothing to index
1176
                    $did = $di->index();
1177
                    unset($di);
1178
                }
1179
                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...
1180
                    // save it to db
1181
                    if ($addQs || $rmQs) {
1182
                        $sql = "DELETE FROM %s
1183
                            WHERE course_code = '%s' AND tool_id = '%s' AND ref_id_second_level = '%s'";
1184
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
1185
                    } else {
1186
                        $sql = "DELETE FROM %S
1187
                            WHERE
1188
                                course_code = '%s'
1189
                                AND tool_id = '%s'
1190
                                AND tool_id = '%s'
1191
                                AND ref_id_high_level = '%s'
1192
                                AND ref_id_second_level = '%s'";
1193
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
1194
                    }
1195
                    Database::query($sql);
1196
                    if ($rmQs) {
1197
                        if (!empty($question_exercises)) {
1198
                            $sql = "INSERT INTO %s (
1199
                                    id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
1200
                                )
1201
                                VALUES (
1202
                                    NULL, '%s', '%s', %s, %s, %s
1203
                                )";
1204
                            $sql = sprintf(
1205
                                $sql,
1206
                                $tbl_se_ref,
1207
                                $course_id,
1208
                                TOOL_QUIZ,
1209
                                array_shift($question_exercises),
1210
                                $this->id,
1211
                                $did
1212
                            );
1213
                            Database::query($sql);
1214
                        }
1215
                    } else {
1216
                        $sql = "INSERT INTO %s (
1217
                                id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
1218
                            )
1219
                            VALUES (
1220
                                NULL , '%s', '%s', %s, %s, %s
1221
                            )";
1222
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id, $did);
1223
                        Database::query($sql);
1224
                    }
1225
                }
1226
            }
1227
        }
1228
    }
1229
1230
    /**
1231
     * adds an exercise into the exercise list.
1232
     *
1233
     * @author Olivier Brouckaert
1234
     *
1235
     * @param int  $exerciseId - exercise ID
1236
     * @param bool $fromSave   - from $this->save() or not
1237
     */
1238
    public function addToList($exerciseId, $fromSave = false)
1239
    {
1240
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1241
        $id = (int) $this->id;
1242
        $exerciseId = (int) $exerciseId;
1243
1244
        // checks if the exercise ID is not in the list
1245
        if (!empty($exerciseId) && !in_array($exerciseId, $this->exerciseList)) {
1246
            $this->exerciseList[] = $exerciseId;
1247
            $courseId = isset($this->course['real_id']) ? $this->course['real_id'] : 0;
1248
            $newExercise = new Exercise($courseId);
1249
            $newExercise->read($exerciseId, false);
1250
            $count = $newExercise->getQuestionCount();
1251
            $count++;
1252
            $sql = "INSERT INTO $exerciseRelQuestionTable (c_id, question_id, exercice_id, question_order)
1253
                    VALUES ({$this->course['real_id']}, ".$id.", ".$exerciseId.", '$count')";
1254
            Database::query($sql);
1255
1256
            // we do not want to reindex if we had just saved adnd indexed the question
1257
            if (!$fromSave) {
1258
                $this->search_engine_edit($exerciseId, true);
1259
            }
1260
        }
1261
    }
1262
1263
    /**
1264
     * removes an exercise from the exercise list.
1265
     *
1266
     * @author Olivier Brouckaert
1267
     *
1268
     * @param int $exerciseId - exercise ID
1269
     * @param int $courseId
1270
     *
1271
     * @return bool - true if removed, otherwise false
1272
     */
1273
    public function removeFromList($exerciseId, $courseId = 0)
1274
    {
1275
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1276
        $id = (int) $this->id;
1277
        $exerciseId = (int) $exerciseId;
1278
1279
        // searches the position of the exercise ID in the list
1280
        $pos = array_search($exerciseId, $this->exerciseList);
1281
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
1282
1283
        // exercise not found
1284
        if ($pos === false) {
1285
            return false;
1286
        } else {
1287
            // deletes the position in the array containing the wanted exercise ID
1288
            unset($this->exerciseList[$pos]);
1289
            //update order of other elements
1290
            $sql = "SELECT question_order
1291
                    FROM $table
1292
                    WHERE
1293
                        c_id = $courseId AND 
1294
                        question_id = $id AND 
1295
                        exercice_id = $exerciseId";
1296
            $res = Database::query($sql);
1297
            if (Database::num_rows($res) > 0) {
1298
                $row = Database::fetch_array($res);
1299
                if (!empty($row['question_order'])) {
1300
                    $sql = "UPDATE $table
1301
                            SET question_order = question_order-1
1302
                            WHERE
1303
                                c_id = $courseId AND 
1304
                                exercice_id = $exerciseId AND 
1305
                                question_order > ".$row['question_order'];
1306
                    Database::query($sql);
1307
                }
1308
            }
1309
1310
            $sql = "DELETE FROM $table
1311
                    WHERE
1312
                        c_id = $courseId AND 
1313
                        question_id = $id AND 
1314
                        exercice_id = $exerciseId";
1315
            Database::query($sql);
1316
1317
            return true;
1318
        }
1319
    }
1320
1321
    /**
1322
     * Deletes a question from the database
1323
     * the parameter tells if the question is removed from all exercises (value = 0),
1324
     * or just from one exercise (value = exercise ID).
1325
     *
1326
     * @author Olivier Brouckaert
1327
     *
1328
     * @param int $deleteFromEx - exercise ID if the question is only removed from one exercise
1329
     *
1330
     * @return bool
1331
     */
1332
    public function delete($deleteFromEx = 0)
1333
    {
1334
        if (empty($this->course)) {
1335
            return false;
1336
        }
1337
1338
        $courseId = $this->course['real_id'];
1339
1340
        if (empty($courseId)) {
1341
            return false;
1342
        }
1343
1344
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1345
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
1346
        $TBL_REPONSES = Database::get_course_table(TABLE_QUIZ_ANSWER);
1347
        $TBL_QUIZ_QUESTION_REL_CATEGORY = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
1348
1349
        $id = (int) $this->id;
1350
1351
        // if the question must be removed from all exercises
1352
        if (!$deleteFromEx) {
1353
            //update the question_order of each question to avoid inconsistencies
1354
            $sql = "SELECT exercice_id, question_order 
1355
                    FROM $TBL_EXERCISE_QUESTION
1356
                    WHERE c_id = $courseId AND question_id = ".$id;
1357
1358
            $res = Database::query($sql);
1359
            if (Database::num_rows($res) > 0) {
1360
                while ($row = Database::fetch_array($res)) {
1361
                    if (!empty($row['question_order'])) {
1362
                        $sql = "UPDATE $TBL_EXERCISE_QUESTION
1363
                                SET question_order = question_order-1
1364
                                WHERE
1365
                                    c_id = $courseId AND 
1366
                                    exercice_id = ".intval($row['exercice_id'])." AND 
1367
                                    question_order > ".$row['question_order'];
1368
                        Database::query($sql);
1369
                    }
1370
                }
1371
            }
1372
1373
            $sql = "DELETE FROM $TBL_EXERCISE_QUESTION
1374
                    WHERE c_id = $courseId AND question_id = ".$id;
1375
            Database::query($sql);
1376
1377
            $sql = "DELETE FROM $TBL_QUESTIONS
1378
                    WHERE c_id = $courseId AND id = ".$id;
1379
            Database::query($sql);
1380
1381
            $sql = "DELETE FROM $TBL_REPONSES
1382
                    WHERE c_id = $courseId AND question_id = ".$id;
1383
            Database::query($sql);
1384
1385
            // remove the category of this question in the question_rel_category table
1386
            $sql = "DELETE FROM $TBL_QUIZ_QUESTION_REL_CATEGORY
1387
                    WHERE 
1388
                        c_id = $courseId AND 
1389
                        question_id = ".$id;
1390
            Database::query($sql);
1391
1392
            api_item_property_update(
1393
                $this->course,
1394
                TOOL_QUIZ,
1395
                $id,
1396
                'QuizQuestionDeleted',
1397
                api_get_user_id()
1398
            );
1399
            $this->removePicture();
1400
        } else {
1401
            // just removes the exercise from the list
1402
            $this->removeFromList($deleteFromEx, $courseId);
1403
            if (api_get_setting('search_enabled') == 'true' && extension_loaded('xapian')) {
1404
                // disassociate question with this exercise
1405
                $this->search_engine_edit($deleteFromEx, false, true);
1406
            }
1407
1408
            api_item_property_update(
1409
                $this->course,
1410
                TOOL_QUIZ,
1411
                $id,
1412
                'QuizQuestionDeleted',
1413
                api_get_user_id()
1414
            );
1415
        }
1416
1417
        return true;
1418
    }
1419
1420
    /**
1421
     * Duplicates the question.
1422
     *
1423
     * @author Olivier Brouckaert
1424
     *
1425
     * @param array $courseInfo Course info of the destination course
1426
     *
1427
     * @return false|string ID of the new question
1428
     */
1429
    public function duplicate($courseInfo = [])
1430
    {
1431
        $courseInfo = empty($courseInfo) ? $this->course : $courseInfo;
1432
1433
        if (empty($courseInfo)) {
1434
            return false;
1435
        }
1436
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
1437
        $TBL_QUESTION_OPTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1438
1439
        $question = $this->question;
1440
        $description = $this->description;
1441
        $weighting = $this->weighting;
1442
        $position = $this->position;
1443
        $type = $this->type;
1444
        $level = (int) $this->level;
1445
        $extra = $this->extra;
1446
1447
        // Using the same method used in the course copy to transform URLs
1448
        if ($this->course['id'] != $courseInfo['id']) {
1449
            $description = DocumentManager::replaceUrlWithNewCourseCode(
1450
                $description,
1451
                $this->course['code'],
1452
                $courseInfo['id']
1453
            );
1454
            $question = DocumentManager::replaceUrlWithNewCourseCode(
1455
                $question,
1456
                $this->course['code'],
1457
                $courseInfo['id']
1458
            );
1459
        }
1460
1461
        $course_id = $courseInfo['real_id'];
1462
1463
        // Read the source options
1464
        $options = self::readQuestionOption($this->id, $this->course['real_id']);
1465
1466
        // Inserting in the new course db / or the same course db
1467
        $params = [
1468
            'c_id' => $course_id,
1469
            'question' => $question,
1470
            'description' => $description,
1471
            'ponderation' => $weighting,
1472
            'position' => $position,
1473
            'type' => $type,
1474
            'level' => $level,
1475
            'extra' => $extra,
1476
        ];
1477
        $newQuestionId = Database::insert($questionTable, $params);
1478
1479
        if ($newQuestionId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $newQuestionId of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1480
            $sql = "UPDATE $questionTable 
1481
                    SET id = iid
1482
                    WHERE iid = $newQuestionId";
1483
            Database::query($sql);
1484
1485
            if (!empty($options)) {
1486
                // Saving the quiz_options
1487
                foreach ($options as $item) {
1488
                    $item['question_id'] = $newQuestionId;
1489
                    $item['c_id'] = $course_id;
1490
                    unset($item['id']);
1491
                    unset($item['iid']);
1492
                    $id = Database::insert($TBL_QUESTION_OPTIONS, $item);
1493
                    if ($id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1494
                        $sql = "UPDATE $TBL_QUESTION_OPTIONS 
1495
                                SET id = iid
1496
                                WHERE iid = $id";
1497
                        Database::query($sql);
1498
                    }
1499
                }
1500
            }
1501
1502
            // Duplicates the picture of the hotspot
1503
            $this->exportPicture($newQuestionId, $courseInfo);
1504
        }
1505
1506
        return $newQuestionId;
1507
    }
1508
1509
    /**
1510
     * @return string
1511
     */
1512
    public function get_question_type_name()
1513
    {
1514
        $key = self::$questionTypes[$this->type];
1515
1516
        return get_lang($key[1]);
1517
    }
1518
1519
    /**
1520
     * @param string $type
1521
     */
1522
    public static function get_question_type($type)
1523
    {
1524
        if ($type == ORAL_EXPRESSION && api_get_setting('enable_record_audio') !== 'true') {
1525
            return null;
1526
        }
1527
1528
        return self::$questionTypes[$type];
1529
    }
1530
1531
    /**
1532
     * @return array
1533
     */
1534
    public static function getQuestionTypeList()
1535
    {
1536
        if (api_get_setting('enable_record_audio') !== 'true') {
1537
            self::$questionTypes[ORAL_EXPRESSION] = null;
1538
            unset(self::$questionTypes[ORAL_EXPRESSION]);
1539
        }
1540
        if (api_get_setting('enable_quiz_scenario') !== 'true') {
1541
            self::$questionTypes[HOT_SPOT_DELINEATION] = null;
1542
            unset(self::$questionTypes[HOT_SPOT_DELINEATION]);
1543
        }
1544
1545
        return self::$questionTypes;
1546
    }
1547
1548
    /**
1549
     * Returns an instance of the class corresponding to the type.
1550
     *
1551
     * @param int $type the type of the question
1552
     *
1553
     * @return $this instance of a Question subclass (or of Questionc class by default)
1554
     */
1555
    public static function getInstance($type)
1556
    {
1557
        if (!is_null($type)) {
1558
            list($fileName, $className) = self::get_question_type($type);
1559
            if (!empty($fileName)) {
1560
                if (class_exists($className)) {
1561
                    return new $className();
1562
                } else {
1563
                    echo 'Can\'t instanciate class '.$className.' of type '.$type;
1564
                }
1565
            }
1566
        }
1567
1568
        return null;
1569
    }
1570
1571
    /**
1572
     * Creates the form to create / edit a question
1573
     * A subclass can redefine this function to add fields...
1574
     *
1575
     * @param FormValidator $form
1576
     * @param Exercise      $exercise
1577
     */
1578
    public function createForm(&$form, $exercise)
1579
    {
1580
        echo '<style>
1581
                .media { display:none;}
1582
            </style>';
1583
1584
        // question name
1585
        if (api_get_configuration_value('save_titles_as_html')) {
1586
            $editorConfig = ['ToolbarSet' => 'TitleAsHtml'];
1587
            $form->addHtmlEditor(
1588
                'questionName',
1589
                get_lang('Question'),
1590
                false,
1591
                false,
1592
                $editorConfig,
1593
                true
1594
            );
1595
        } else {
1596
            $form->addElement('text', 'questionName', get_lang('Question'));
1597
        }
1598
1599
        $form->addRule('questionName', get_lang('Please type the question'), 'required');
1600
1601
        // default content
1602
        $isContent = isset($_REQUEST['isContent']) ? (int) $_REQUEST['isContent'] : null;
1603
1604
        // Question type
1605
        $answerType = isset($_REQUEST['answerType']) ? (int) $_REQUEST['answerType'] : null;
1606
        $form->addElement('hidden', 'answerType', $answerType);
1607
1608
        // html editor
1609
        $editorConfig = [
1610
            'ToolbarSet' => 'TestQuestionDescription',
1611
            'Height' => '150',
1612
        ];
1613
1614
        if (!api_is_allowed_to_edit(null, true)) {
1615
            $editorConfig['UserStatus'] = 'student';
1616
        }
1617
1618
        $form->addButtonAdvancedSettings('advanced_params');
1619
        $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
1620
        $form->addHtmlEditor(
1621
            'questionDescription',
1622
            get_lang('Enrich question'),
1623
            false,
1624
            false,
1625
            $editorConfig
1626
        );
1627
1628
        if ($this->type != MEDIA_QUESTION) {
1629
            // Advanced parameters
1630
            $select_level = self::get_default_levels();
1631
            $form->addElement(
1632
                'select',
1633
                'questionLevel',
1634
                get_lang('Difficulty'),
1635
                $select_level
1636
            );
1637
1638
            // Categories
1639
            $tabCat = TestCategory::getCategoriesForSelect();
1640
1641
            $form->addElement(
1642
                'select',
1643
                'questionCategory',
1644
                get_lang('Category'),
1645
                $tabCat
1646
            );
1647
1648
            global $text;
1649
1650
            switch ($this->type) {
1651
                case UNIQUE_ANSWER:
1652
                    $buttonGroup = [];
1653
                    $buttonGroup[] = $form->addButtonSave(
1654
                        $text,
1655
                        'submitQuestion',
1656
                        true
1657
                    );
1658
                    $buttonGroup[] = $form->addButton(
1659
                        'convertAnswer',
1660
                        get_lang('Convert to multiple answer'),
1661
                        'dot-circle-o',
1662
                        'default',
1663
                        null,
1664
                        null,
1665
                        null,
1666
                        true
1667
                    );
1668
                    $form->addGroup($buttonGroup);
1669
                    break;
1670
                case MULTIPLE_ANSWER:
1671
                    $buttonGroup = [];
1672
                    $buttonGroup[] = $form->addButtonSave(
1673
                        $text,
1674
                        'submitQuestion',
1675
                        true
1676
                    );
1677
                    $buttonGroup[] = $form->addButton(
1678
                        'convertAnswer',
1679
                        get_lang('Convert to unique answer'),
1680
                        'check-square-o',
1681
                        'default',
1682
                        null,
1683
                        null,
1684
                        null,
1685
                        true
1686
                    );
1687
                    $form->addGroup($buttonGroup);
1688
                    break;
1689
            }
1690
            //Medias
1691
            //$course_medias = self::prepare_course_media_select(api_get_course_int_id());
1692
            //$form->addElement('select', 'parent_id', get_lang('Attach to media'), $course_medias);
1693
        }
1694
1695
        $form->addElement('html', '</div>');
1696
1697
        if (!isset($_GET['fromExercise'])) {
1698
            switch ($answerType) {
1699
                case 1:
1700
                    $this->question = get_lang('Select the good reasoning');
1701
                    break;
1702
                case 2:
1703
                    $this->question = get_lang('The marasmus is a consequence of');
1704
                    break;
1705
                case 3:
1706
                    $this->question = get_lang('Calculate the Body Mass Index');
1707
                    break;
1708
                case 4:
1709
                    $this->question = get_lang('Order the operations');
1710
                    break;
1711
                case 5:
1712
                    $this->question = get_lang('List what you consider the 10 top qualities of a good project manager?');
1713
                    break;
1714
                case 9:
1715
                    $this->question = get_lang('The marasmus is a consequence of');
1716
                    break;
1717
            }
1718
        }
1719
1720
        if (!is_null($exercise)) {
1721
            if ($exercise->questionFeedbackEnabled && $this->showFeedback($exercise)) {
1722
                $form->addTextarea('feedback', get_lang('Feedback if not correct'));
1723
            }
1724
        }
1725
1726
        // default values
1727
        $defaults = [];
1728
        $defaults['questionName'] = $this->question;
1729
        $defaults['questionDescription'] = $this->description;
1730
        $defaults['questionLevel'] = $this->level;
1731
        $defaults['questionCategory'] = $this->category;
1732
        $defaults['feedback'] = $this->feedback;
1733
1734
        // Came from he question pool
1735
        if (isset($_GET['fromExercise'])) {
1736
            $form->setDefaults($defaults);
1737
        }
1738
1739
        if (!isset($_GET['newQuestion']) || $isContent) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $isContent of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1740
            $form->setDefaults($defaults);
1741
        }
1742
1743
        /*if (!empty($_REQUEST['myid'])) {
1744
            $form->setDefaults($defaults);
1745
        } else {
1746
            if ($isContent == 1) {
1747
                $form->setDefaults($defaults);
1748
            }
1749
        }*/
1750
    }
1751
1752
    /**
1753
     * function which process the creation of questions.
1754
     *
1755
     * @param FormValidator $form
1756
     * @param Exercise      $exercise
1757
     */
1758
    public function processCreation($form, $exercise)
1759
    {
1760
        $this->updateTitle($form->getSubmitValue('questionName'));
1761
        $this->updateDescription($form->getSubmitValue('questionDescription'));
1762
        $this->updateLevel($form->getSubmitValue('questionLevel'));
1763
        $this->updateCategory($form->getSubmitValue('questionCategory'));
1764
        $this->setFeedback($form->getSubmitValue('feedback'));
1765
1766
        //Save normal question if NOT media
1767
        if ($this->type != MEDIA_QUESTION) {
1768
            $this->save($exercise);
1769
            // modify the exercise
1770
            $exercise->addToList($this->id);
1771
            $exercise->update_question_positions();
1772
        }
1773
    }
1774
1775
    /**
1776
     * abstract function which creates the form to create / edit the answers of the question.
1777
     *
1778
     * @param FormValidator $form
1779
     */
1780
    abstract public function createAnswersForm($form);
1781
1782
    /**
1783
     * abstract function which process the creation of answers.
1784
     *
1785
     * @param FormValidator $form
1786
     * @param Exercise      $exercise
1787
     */
1788
    abstract public function processAnswersCreation($form, $exercise);
1789
1790
    /**
1791
     * Displays the menu of question types.
1792
     *
1793
     * @param Exercise $objExercise
1794
     */
1795
    public static function displayTypeMenu($objExercise)
1796
    {
1797
        $feedbackType = $objExercise->getFeedbackType();
1798
        $exerciseId = $objExercise->id;
1799
1800
        // 1. by default we show all the question types
1801
        $questionTypeList = self::getQuestionTypeList();
1802
1803
        if (!isset($feedbackType)) {
1804
            $feedbackType = 0;
1805
        }
1806
1807
        switch ($feedbackType) {
1808
            case EXERCISE_FEEDBACK_TYPE_DIRECT:
1809
                $questionTypeList = [
1810
                    UNIQUE_ANSWER => self::$questionTypes[UNIQUE_ANSWER],
1811
                    HOT_SPOT_DELINEATION => self::$questionTypes[HOT_SPOT_DELINEATION],
1812
                ];
1813
                break;
1814
            case EXERCISE_FEEDBACK_TYPE_POPUP:
1815
                $questionTypeList = [
1816
                    UNIQUE_ANSWER => self::$questionTypes[UNIQUE_ANSWER],
1817
                    MULTIPLE_ANSWER => self::$questionTypes[MULTIPLE_ANSWER],
1818
                    DRAGGABLE => self::$questionTypes[DRAGGABLE],
1819
                    HOT_SPOT_DELINEATION => self::$questionTypes[HOT_SPOT_DELINEATION],
1820
                    CALCULATED_ANSWER => self::$questionTypes[CALCULATED_ANSWER],
1821
                ];
1822
                break;
1823
            default:
1824
                unset($questionTypeList[HOT_SPOT_DELINEATION]);
1825
                break;
1826
        }
1827
1828
        echo '<div class="card">';
1829
        echo '<div class="card-body">';
1830
        echo '<ul class="question_menu">';
1831
        foreach ($questionTypeList as $i => $type) {
1832
            /** @var Question $type */
1833
            $type = new $type[1]();
1834
            $img = $type->getTypePicture();
1835
            $explanation = get_lang($type->getExplanation());
1836
            echo '<li>';
1837
            echo '<div class="icon-image">';
1838
            $icon = '<a href="admin.php?'.api_get_cidreq().'&newQuestion=yes&answerType='.$i.'">'.
1839
                Display::return_icon($img, $explanation, null, ICON_SIZE_BIG).'</a>';
1840
1841
            if ($objExercise->force_edit_exercise_in_lp === false) {
1842
                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...
1843
                    $img = pathinfo($img);
1844
                    $img = $img['filename'].'_na.'.$img['extension'];
1845
                    $icon = Display::return_icon($img, $explanation, null, ICON_SIZE_BIG);
1846
                }
1847
            }
1848
            echo $icon;
1849
            echo '</div>';
1850
            echo '</li>';
1851
        }
1852
1853
        echo '<li>';
1854
        echo '<div class="icon_image_content">';
1855
        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...
1856
            echo Display::return_icon(
1857
                'database_na.png',
1858
                get_lang('Recycle existing questions'),
1859
                null,
1860
                ICON_SIZE_BIG
1861
            );
1862
        } else {
1863
            if (in_array($feedbackType, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1864
                echo $url = "<a href=\"question_pool.php?".api_get_cidreq()."&type=1&fromExercise=$exerciseId\">";
1865
            } else {
1866
                echo $url = '<a href="question_pool.php?'.api_get_cidreq().'&fromExercise='.$exerciseId.'">';
1867
            }
1868
            echo Display::return_icon(
1869
                'database.png',
1870
                get_lang('Recycle existing questions'),
1871
                null,
1872
                ICON_SIZE_BIG
1873
            );
1874
        }
1875
        echo '</a>';
1876
        echo '</div></li>';
1877
        echo '</ul>';
1878
        echo '</div>';
1879
        echo '</div>';
1880
    }
1881
1882
    /**
1883
     * @param int    $question_id
1884
     * @param string $name
1885
     * @param int    $course_id
1886
     * @param int    $position
1887
     *
1888
     * @return false|string
1889
     */
1890
    public static function saveQuestionOption($question_id, $name, $course_id, $position = 0)
1891
    {
1892
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1893
        $params['question_id'] = (int) $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...
1894
        $params['name'] = $name;
1895
        $params['position'] = $position;
1896
        $params['c_id'] = $course_id;
1897
        $result = self::readQuestionOption($question_id, $course_id);
1898
        $last_id = Database::insert($table, $params);
1899
        if ($last_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $last_id of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1900
            $sql = "UPDATE $table SET id = iid WHERE iid = $last_id";
1901
            Database::query($sql);
1902
        }
1903
1904
        return $last_id;
1905
    }
1906
1907
    /**
1908
     * @param int $question_id
1909
     * @param int $course_id
1910
     */
1911
    public static function deleteAllQuestionOptions($question_id, $course_id)
1912
    {
1913
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1914
        Database::delete(
1915
            $table,
1916
            [
1917
                'c_id = ? AND question_id = ?' => [
1918
                    $course_id,
1919
                    $question_id,
1920
                ],
1921
            ]
1922
        );
1923
    }
1924
1925
    /**
1926
     * @param int   $id
1927
     * @param array $params
1928
     * @param int   $course_id
1929
     *
1930
     * @return bool|int
1931
     */
1932
    public static function updateQuestionOption($id, $params, $course_id)
1933
    {
1934
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1935
        $result = Database::update(
1936
            $table,
1937
            $params,
1938
            ['c_id = ? AND id = ?' => [$course_id, $id]]
1939
        );
1940
1941
        return $result;
1942
    }
1943
1944
    /**
1945
     * @param int $question_id
1946
     * @param int $course_id
1947
     *
1948
     * @return array
1949
     */
1950
    public static function readQuestionOption($question_id, $course_id)
1951
    {
1952
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1953
        $result = Database::select(
1954
            '*',
1955
            $table,
1956
            [
1957
                'where' => [
1958
                    'c_id = ? AND question_id = ?' => [
1959
                        $course_id,
1960
                        $question_id,
1961
                    ],
1962
                ],
1963
                'order' => 'id ASC',
1964
            ]
1965
        );
1966
1967
        return $result;
1968
    }
1969
1970
    /**
1971
     * Shows question title an description.
1972
     *
1973
     * @param Exercise $exercise
1974
     * @param int      $counter
1975
     * @param array    $score
1976
     *
1977
     * @return string HTML string with the header of the question (before the answers table)
1978
     */
1979
    public function return_header(Exercise $exercise, $counter = null, $score = [])
1980
    {
1981
        $counterLabel = '';
1982
        if (!empty($counter)) {
1983
            $counterLabel = (int) $counter;
1984
        }
1985
1986
        $scoreLabel = get_lang('Wrong');
1987
1988
        if (in_array($exercise->results_disabled, [
1989
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
1990
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
1991
        ])
1992
        ) {
1993
            $scoreLabel = get_lang('Wrong answer. The correct one was:');
1994
        }
1995
1996
        $class = 'error';
1997
        if (isset($score['pass']) && $score['pass'] == true) {
1998
            $scoreLabel = get_lang('Correct');
1999
2000
            if (in_array($exercise->results_disabled, [
2001
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
2002
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
2003
            ])
2004
            ) {
2005
                $scoreLabel = get_lang('Correct answer');
2006
            }
2007
            $class = 'success';
2008
        }
2009
2010
        switch ($this->type) {
2011
            case FREE_ANSWER:
2012
            case ORAL_EXPRESSION:
2013
            case ANNOTATION:
2014
                $score['revised'] = isset($score['revised']) ? $score['revised'] : false;
2015
                if ($score['revised'] == true) {
2016
                    $scoreLabel = get_lang('Revised');
2017
                    $class = '';
2018
                } else {
2019
                    $scoreLabel = get_lang('Not reviewed');
2020
                    $class = 'warning';
2021
                    if (isset($score['weight'])) {
2022
                        $weight = float_format($score['weight'], 1);
2023
                        $score['result'] = ' ? / '.$weight;
2024
                    }
2025
                    $model = ExerciseLib::getCourseScoreModel();
2026
                    if (!empty($model)) {
2027
                        $score['result'] = ' ? ';
2028
                    }
2029
2030
                    $hide = api_get_configuration_value('hide_free_question_score');
2031
                    if ($hide === true) {
2032
                        $score['result'] = '-';
2033
                    }
2034
                }
2035
                break;
2036
            case UNIQUE_ANSWER:
2037
                if (in_array($exercise->results_disabled, [
2038
                    RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
2039
                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
2040
                ])
2041
                ) {
2042
                    if (isset($score['user_answered'])) {
2043
                        if ($score['user_answered'] === false) {
2044
                            $scoreLabel = get_lang('Unanswered');
2045
                            $class = 'info';
2046
                        }
2047
                    }
2048
                }
2049
                break;
2050
        }
2051
2052
        // display question category, if any
2053
        $header = '';
2054
        if ($exercise->display_category_name) {
2055
            $header = TestCategory::returnCategoryAndTitle($this->id);
2056
        }
2057
        $show_media = '';
2058
        if ($show_media) {
2059
            $header .= $this->show_media_content();
2060
        }
2061
2062
        $scoreCurrent = [
2063
            'used' => isset($score['score']) ? $score['score'] : '',
2064
            'missing' => isset($score['weight']) ? $score['weight'] : '',
2065
        ];
2066
        $header .= Display::page_subheader2($counterLabel.'. '.$this->question);
2067
2068
        // dont display score for certainty degree questions
2069
        if ($this->type != MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
2070
            if (isset($score['result'])) {
2071
                if (in_array($exercise->results_disabled, [
2072
                    RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
2073
                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
2074
                ])
2075
                ) {
2076
                    $score['result'] = null;
2077
                }
2078
                $header .= $exercise->getQuestionRibbon($class, $scoreLabel, $score['result'], $scoreCurrent);
2079
            }
2080
        }
2081
2082
        if ($this->type != READING_COMPREHENSION) {
2083
            // Do not show the description (the text to read) if the question is of type READING_COMPREHENSION
2084
            $header .= Display::div(
2085
                $this->description,
2086
                ['class' => 'question_description']
2087
            );
2088
        } else {
2089
            if ($score['pass'] == true) {
2090
                $message = Display::div(
2091
                    sprintf(
2092
                        get_lang('Congratulations, you have reached and correctly understood, at a speed of %s words per minute, a text of a total %s words.'),
2093
                        ReadingComprehension::$speeds[$this->level],
2094
                        $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

2094
                        $this->/** @scrutinizer ignore-call */ 
2095
                               getWordsCount()
Loading history...
2095
                    )
2096
                );
2097
            } else {
2098
                $message = Display::div(
2099
                    sprintf(
2100
                        get_lang('Sorry, it seems like a speed of %s words/minute was too fast for this text of %s words.'),
2101
                        ReadingComprehension::$speeds[$this->level],
2102
                        $this->getWordsCount()
2103
                    )
2104
                );
2105
            }
2106
            $header .= $message.'<br />';
2107
        }
2108
2109
        if (isset($score['pass']) && $score['pass'] === false) {
2110
            if ($this->showFeedback($exercise)) {
2111
                $header .= $this->returnFormatFeedback();
2112
            }
2113
        }
2114
2115
        return $header;
2116
    }
2117
2118
    /**
2119
     * @deprecated
2120
     * Create a question from a set of parameters.
2121
     *
2122
     * @param   int     Quiz ID
2123
     * @param   string  Question name
2124
     * @param   int     Maximum result for the question
2125
     * @param   int     Type of question (see constants at beginning of question.class.php)
2126
     * @param   int     Question level/category
2127
     * @param string $quiz_id
2128
     */
2129
    public function create_question(
2130
        $quiz_id,
2131
        $question_name,
2132
        $question_description = '',
2133
        $max_score = 0,
2134
        $type = 1,
2135
        $level = 1
2136
    ) {
2137
        $course_id = api_get_course_int_id();
2138
        $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2139
        $tbl_quiz_rel_question = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
2140
2141
        $quiz_id = intval($quiz_id);
2142
        $max_score = (float) $max_score;
2143
        $type = intval($type);
2144
        $level = intval($level);
2145
2146
        // Get the max position
2147
        $sql = "SELECT max(position) as max_position
2148
                FROM $tbl_quiz_question q 
2149
                INNER JOIN $tbl_quiz_rel_question r
2150
                ON
2151
                    q.id = r.question_id AND
2152
                    exercice_id = $quiz_id AND
2153
                    q.c_id = $course_id AND
2154
                    r.c_id = $course_id";
2155
        $rs_max = Database::query($sql);
2156
        $row_max = Database::fetch_object($rs_max);
2157
        $max_position = $row_max->max_position + 1;
2158
2159
        $params = [
2160
            'c_id' => $course_id,
2161
            'question' => $question_name,
2162
            'description' => $question_description,
2163
            'ponderation' => $max_score,
2164
            'position' => $max_position,
2165
            'type' => $type,
2166
            'level' => $level,
2167
        ];
2168
        $question_id = Database::insert($tbl_quiz_question, $params);
2169
2170
        if ($question_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $question_id of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2171
            $sql = "UPDATE $tbl_quiz_question  
2172
                    SET id = iid WHERE iid = $question_id";
2173
            Database::query($sql);
2174
2175
            // Get the max question_order
2176
            $sql = "SELECT max(question_order) as max_order
2177
                    FROM $tbl_quiz_rel_question
2178
                    WHERE c_id = $course_id AND exercice_id = $quiz_id ";
2179
            $rs_max_order = Database::query($sql);
2180
            $row_max_order = Database::fetch_object($rs_max_order);
2181
            $max_order = $row_max_order->max_order + 1;
2182
            // Attach questions to quiz
2183
            $sql = "INSERT INTO $tbl_quiz_rel_question (c_id, question_id, exercice_id, question_order)
2184
                    VALUES($course_id, $question_id, $quiz_id, $max_order)";
2185
            Database::query($sql);
2186
        }
2187
2188
        return $question_id;
2189
    }
2190
2191
    /**
2192
     * @return string
2193
     */
2194
    public function getTypePicture()
2195
    {
2196
        return $this->typePicture;
2197
    }
2198
2199
    /**
2200
     * @return string
2201
     */
2202
    public function getExplanation()
2203
    {
2204
        return $this->explanationLangVar;
2205
    }
2206
2207
    /**
2208
     * Get course medias.
2209
     *
2210
     * @param int $course_id
2211
     *
2212
     * @return array
2213
     */
2214
    public static function get_course_medias(
2215
        $course_id,
2216
        $start = 0,
2217
        $limit = 100,
2218
        $sidx = "question",
2219
        $sord = "ASC",
2220
        $where_condition = []
2221
    ) {
2222
        $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2223
        $default_where = [
2224
            'c_id = ? AND parent_id = 0 AND type = ?' => [
2225
                $course_id,
2226
                MEDIA_QUESTION,
2227
            ],
2228
        ];
2229
        $result = Database::select(
2230
            '*',
2231
            $table_question,
2232
            [
2233
                'limit' => " $start, $limit",
2234
                'where' => $default_where,
2235
                'order' => "$sidx $sord",
2236
            ]
2237
        );
2238
2239
        return $result;
2240
    }
2241
2242
    /**
2243
     * Get count course medias.
2244
     *
2245
     * @param int course id
2246
     *
2247
     * @return int
2248
     */
2249
    public static function get_count_course_medias($course_id)
2250
    {
2251
        $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
2252
        $result = Database::select(
2253
            'count(*) as count',
2254
            $table_question,
2255
            [
2256
                'where' => [
2257
                    'c_id = ? AND parent_id = 0 AND type = ?' => [
2258
                        $course_id,
2259
                        MEDIA_QUESTION,
2260
                    ],
2261
                ],
2262
            ],
2263
            'first'
2264
        );
2265
2266
        if ($result && isset($result['count'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result 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...
2267
            return $result['count'];
2268
        }
2269
2270
        return 0;
2271
    }
2272
2273
    /**
2274
     * @param int $course_id
2275
     *
2276
     * @return array
2277
     */
2278
    public static function prepare_course_media_select($course_id)
2279
    {
2280
        $medias = self::get_course_medias($course_id);
2281
        $media_list = [];
2282
        $media_list[0] = get_lang('Not linked to media');
2283
2284
        if (!empty($medias)) {
2285
            foreach ($medias as $media) {
2286
                $media_list[$media['id']] = empty($media['question']) ? get_lang('Untitled') : $media['question'];
2287
            }
2288
        }
2289
2290
        return $media_list;
2291
    }
2292
2293
    /**
2294
     * @return array
2295
     */
2296
    public static function get_default_levels()
2297
    {
2298
        $levels = [
2299
            1 => 1,
2300
            2 => 2,
2301
            3 => 3,
2302
            4 => 4,
2303
            5 => 5,
2304
        ];
2305
2306
        return $levels;
2307
    }
2308
2309
    /**
2310
     * @return string
2311
     */
2312
    public function show_media_content()
2313
    {
2314
        $html = '';
2315
        if ($this->parent_id != 0) {
2316
            $parent_question = self::read($this->parent_id);
2317
            $html = $parent_question->show_media_content();
2318
        } else {
2319
            $html .= Display::page_subheader($this->selectTitle());
2320
            $html .= $this->selectDescription();
2321
        }
2322
2323
        return $html;
2324
    }
2325
2326
    /**
2327
     * Swap between unique and multiple type answers.
2328
     *
2329
     * @return UniqueAnswer|MultipleAnswer
2330
     */
2331
    public function swapSimpleAnswerTypes()
2332
    {
2333
        $oppositeAnswers = [
2334
            UNIQUE_ANSWER => MULTIPLE_ANSWER,
2335
            MULTIPLE_ANSWER => UNIQUE_ANSWER,
2336
        ];
2337
        $this->type = $oppositeAnswers[$this->type];
2338
        Database::update(
2339
            Database::get_course_table(TABLE_QUIZ_QUESTION),
2340
            ['type' => $this->type],
2341
            ['c_id = ? AND id = ?' => [$this->course['real_id'], $this->id]]
2342
        );
2343
        $answerClasses = [
2344
            UNIQUE_ANSWER => 'UniqueAnswer',
2345
            MULTIPLE_ANSWER => 'MultipleAnswer',
2346
        ];
2347
        $swappedAnswer = new $answerClasses[$this->type]();
2348
        foreach ($this as $key => $value) {
2349
            $swappedAnswer->$key = $value;
2350
        }
2351
2352
        return $swappedAnswer;
2353
    }
2354
2355
    /**
2356
     * @param array $score
2357
     *
2358
     * @return bool
2359
     */
2360
    public function isQuestionWaitingReview($score)
2361
    {
2362
        $isReview = false;
2363
        if (!empty($score)) {
2364
            if (!empty($score['comments']) || $score['score'] > 0) {
2365
                $isReview = true;
2366
            }
2367
        }
2368
2369
        return $isReview;
2370
    }
2371
2372
    /**
2373
     * @param string $value
2374
     */
2375
    public function setFeedback($value)
2376
    {
2377
        $this->feedback = $value;
2378
    }
2379
2380
    /**
2381
     * @param Exercise $exercise
2382
     *
2383
     * @return bool
2384
     */
2385
    public function showFeedback($exercise)
2386
    {
2387
        return
2388
            in_array($this->type, $this->questionTypeWithFeedback) &&
2389
            $exercise->getFeedbackType() != EXERCISE_FEEDBACK_TYPE_EXAM;
2390
    }
2391
2392
    /**
2393
     * @return string
2394
     */
2395
    public function returnFormatFeedback()
2396
    {
2397
        return '<br />'.Display::return_message($this->feedback, 'normal', false);
2398
    }
2399
2400
    /**
2401
     * Check if this question exists in another exercise.
2402
     *
2403
     * @throws \Doctrine\ORM\Query\QueryException
2404
     *
2405
     * @return bool
2406
     */
2407
    public function existsInAnotherExercise()
2408
    {
2409
        $count = $this->getCountExercise();
2410
2411
        return $count > 1;
2412
    }
2413
2414
    /**
2415
     * @throws \Doctrine\ORM\Query\QueryException
2416
     *
2417
     * @return int
2418
     */
2419
    public function getCountExercise()
2420
    {
2421
        $em = Database::getManager();
2422
2423
        $count = $em
2424
            ->createQuery('
2425
                SELECT COUNT(qq.iid) FROM ChamiloCourseBundle:CQuizRelQuestion qq
2426
                WHERE qq.questionId = :id
2427
            ')
2428
            ->setParameters(['id' => (int) $this->id])
2429
            ->getSingleScalarResult();
2430
2431
        return (int) $count;
2432
    }
2433
2434
    /**
2435
     * Check if this question exists in another exercise.
2436
     *
2437
     * @throws \Doctrine\ORM\Query\QueryException
2438
     *
2439
     * @return mixed
2440
     */
2441
    public function getExerciseListWhereQuestionExists()
2442
    {
2443
        $em = Database::getManager();
2444
2445
        $result = $em
2446
            ->createQuery('
2447
                SELECT e 
2448
                FROM ChamiloCourseBundle:CQuizRelQuestion qq
2449
                JOIN ChamiloCourseBundle:CQuiz e                
2450
                WHERE e.iid = qq.exerciceId AND qq.questionId = :id 
2451
            ')
2452
            ->setParameters(['id' => (int) $this->id])
2453
            ->getResult();
2454
2455
        return $result;
2456
    }
2457
2458
    public function getHotSpotData()
2459
    {
2460
    }
2461
}
2462