Question::duplicate()   B
last analyzed

Complexity

Conditions 8
Paths 22

Size

Total Lines 75
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 46
nc 22
nop 1
dl 0
loc 75
rs 7.9337
c 0
b 0
f 0

How to fix   Long Method   

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
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Framework\Container;
6
use Chamilo\CourseBundle\Entity\CQuizAnswer;
7
use Chamilo\CourseBundle\Entity\CQuizQuestion;
8
use Chamilo\CourseBundle\Entity\CQuizQuestionOption;
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
abstract class Question
21
{
22
    public $id;
23
    public $iid;
24
    public $question;
25
    public $description;
26
    public $weighting;
27
    public $position;
28
    public $type;
29
    public $level;
30
    public $picture;
31
    public $exerciseList; // array with the list of exercises which this question is in
32
    public $category_list;
33
    public $parent_id;
34
    public $category;
35
    public $mandatory;
36
    public $isContent;
37
    public $course;
38
    public $feedback;
39
    public $typePicture = 'new_question.png';
40
    public $explanationLangVar = '';
41
    public $questionTableClass = 'table table-striped question-answer-result__detail';
42
    public $questionTypeWithFeedback;
43
    public $extra;
44
    public $export = false;
45
    public $code;
46
    public static $questionTypes = [
47
        // — Single-choice
48
        UNIQUE_ANSWER                => ['unique_answer.class.php', 'UniqueAnswer'],
49
        UNIQUE_ANSWER_IMAGE          => ['UniqueAnswerImage.php', 'UniqueAnswerImage'],
50
        UNIQUE_ANSWER_NO_OPTION      => ['unique_answer_no_option.class.php', 'UniqueAnswerNoOption'],
51
52
        // — Multiple-choice (all variants together)
53
        MULTIPLE_ANSWER              => ['multiple_answer.class.php', 'MultipleAnswer'],
54
        GLOBAL_MULTIPLE_ANSWER       => ['global_multiple_answer.class.php', 'GlobalMultipleAnswer'],
55
        MULTIPLE_ANSWER_DROPDOWN     => ['MultipleAnswerDropdown.php', 'MultipleAnswerDropdown'],
56
        MULTIPLE_ANSWER_DROPDOWN_COMBINATION => ['MultipleAnswerDropdownCombination.php', 'MultipleAnswerDropdownCombination'],
57
        MULTIPLE_ANSWER_COMBINATION  => ['multiple_answer_combination.class.php', 'MultipleAnswerCombination'],
58
        MULTIPLE_ANSWER_TRUE_FALSE   => ['multiple_answer_true_false.class.php', 'MultipleAnswerTrueFalse'],
59
        MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE => [
60
            'multiple_answer_combination_true_false.class.php', 'MultipleAnswerCombinationTrueFalse'
61
        ],
62
        MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY => [
63
            'MultipleAnswerTrueFalseDegreeCertainty.php', 'MultipleAnswerTrueFalseDegreeCertainty'
64
        ],
65
66
        // — Matching / draggable
67
        MATCHING                     => ['matching.class.php', 'Matching'],
68
        MATCHING_COMBINATION         => ['MatchingCombination.php', 'MatchingCombination'],
69
        DRAGGABLE                    => ['Draggable.php', 'Draggable'],
70
        MATCHING_DRAGGABLE           => ['MatchingDraggable.php', 'MatchingDraggable'],
71
        MATCHING_DRAGGABLE_COMBINATION => ['MatchingDraggableCombination.php', 'MatchingDraggableCombination'],
72
73
        // — Fill-in-the-blanks / calculated
74
        FILL_IN_BLANKS               => ['fill_blanks.class.php', 'FillBlanks'],
75
        FILL_IN_BLANKS_COMBINATION   => ['FillBlanksCombination.php', 'FillBlanksCombination'],
76
        CALCULATED_ANSWER            => ['calculated_answer.class.php', 'CalculatedAnswer'],
77
78
        // — Open answers / expression
79
        FREE_ANSWER                  => ['freeanswer.class.php', 'FreeAnswer'],
80
        ORAL_EXPRESSION              => ['oral_expression.class.php', 'OralExpression'],
81
82
        // — Hotspot
83
        HOT_SPOT                     => ['hotspot.class.php', 'HotSpot'],
84
        HOT_SPOT_COMBINATION         => ['HotSpotCombination.php', 'HotSpotCombination'],
85
        HOT_SPOT_DELINEATION         => ['HotSpotDelineation.php', 'HotSpotDelineation'],
86
87
        // — Media / annotation
88
        MEDIA_QUESTION               => ['MediaQuestion.php', 'MediaQuestion'],
89
        ANNOTATION                   => ['Annotation.php', 'Annotation'],
90
91
        // — Special
92
        READING_COMPREHENSION        => ['ReadingComprehension.php', 'ReadingComprehension'],
93
        PAGE_BREAK                   => ['PageBreakQuestion.php', 'PageBreakQuestion'],
94
        UPLOAD_ANSWER => ['UploadAnswer.php', 'UploadAnswer'],
95
    ];
96
97
    /**
98
     * constructor of the class.
99
     *
100
     * @author Olivier Brouckaert
101
     */
102
    public function __construct()
103
    {
104
        $this->id = 0;
105
        $this->iid = 0;
106
        $this->question = '';
107
        $this->description = '';
108
        $this->weighting = 0;
109
        $this->position = 1;
110
        $this->picture = '';
111
        $this->level = 1;
112
        $this->category = 0;
113
        // This variable is used when loading an exercise like an scenario with
114
        // an special hotspot: final_overlap, final_missing, final_excess
115
        $this->extra = '';
116
        $this->exerciseList = [];
117
        $this->course = api_get_course_info();
118
        $this->category_list = [];
119
        $this->parent_id = 0;
120
        $this->mandatory = 0;
121
        // See BT#12611
122
        $this->questionTypeWithFeedback = [
123
            MATCHING,
124
            MATCHING_COMBINATION,
125
            MATCHING_DRAGGABLE,
126
            MATCHING_DRAGGABLE_COMBINATION,
127
            DRAGGABLE,
128
            FILL_IN_BLANKS,
129
            FILL_IN_BLANKS_COMBINATION,
130
            FREE_ANSWER,
131
            UPLOAD_ANSWER,
132
            ORAL_EXPRESSION,
133
            CALCULATED_ANSWER,
134
            ANNOTATION,
135
        ];
136
    }
137
138
    public function getId()
139
    {
140
        return $this->iid;
141
    }
142
143
    /**
144
     * @return int|null
145
     */
146
    public function getIsContent()
147
    {
148
        $isContent = null;
149
        if (isset($_REQUEST['isContent'])) {
150
            $isContent = (int) $_REQUEST['isContent'];
151
        }
152
153
        return $this->isContent = $isContent;
154
    }
155
156
    /**
157
     * Reads question information from the data base.
158
     *
159
     * @param int   $id              - question ID
160
     * @param array $course_info
161
     * @param bool  $getExerciseList
162
     *
163
     * @return Question
164
     *
165
     * @author Olivier Brouckaert
166
     */
167
    public static function read($id, $course_info = [], $getExerciseList = true)
168
    {
169
        $id = (int) $id;
170
        if (empty($course_info)) {
171
            $course_info = api_get_course_info();
172
        }
173
        $course_id = $course_info['real_id'];
174
175
        if (empty($course_id) || -1 == $course_id) {
176
            return false;
177
        }
178
179
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
180
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
181
182
        $sql = "SELECT *
183
                FROM $TBL_QUESTIONS
184
                WHERE iid = $id ";
185
        $result = Database::query($sql);
186
187
        // if the question has been found
188
        if ($object = Database::fetch_object($result)) {
189
            $objQuestion = self::getInstance($object->type);
190
            if (!empty($objQuestion)) {
191
                $objQuestion->id = $id;
192
                $objQuestion->iid = (int) $object->iid;
193
                $objQuestion->question = $object->question;
194
                $objQuestion->description = $object->description;
195
                $objQuestion->weighting = $object->ponderation;
196
                $objQuestion->position = $object->position;
197
                $objQuestion->type = (int) $object->type;
198
                $objQuestion->picture = $object->picture;
199
                $objQuestion->level = (int) $object->level;
200
                $objQuestion->extra = $object->extra;
201
                $objQuestion->course = $course_info;
202
                $objQuestion->feedback = isset($object->feedback) ? $object->feedback : '';
203
                $objQuestion->category = TestCategory::getCategoryForQuestion($id, $course_id);
204
                $objQuestion->code = isset($object->code) ? $object->code : '';
205
                $categoryInfo = TestCategory::getCategoryInfoForQuestion($id, $course_id);
206
207
                if (!empty($categoryInfo)) {
208
                    if (isset($categoryInfo['category_id'])) {
209
                        $objQuestion->category = (int) $categoryInfo['category_id'];
210
                    }
211
212
                    if (('true' === api_get_setting('exercise.allow_mandatory_question_in_category')) &&
213
                        isset($categoryInfo['mandatory'])
214
                    ) {
215
                        $objQuestion->mandatory = (int) $categoryInfo['mandatory'];
216
                    }
217
                }
218
219
                if ($getExerciseList) {
220
                    $tblQuiz   = Database::get_course_table(TABLE_QUIZ_TEST);
221
                    $sessionId = (int) api_get_session_id();
222
                    $sessionJoin = $sessionId > 0 ? "rl.session_id = $sessionId" : "rl.session_id IS NULL";
223
224
                    $sql = "SELECT DISTINCT q.quiz_id
225
            FROM $TBL_EXERCISE_QUESTION q
226
            INNER JOIN $tblQuiz e  ON e.iid = q.quiz_id
227
            INNER JOIN resource_node rn ON rn.id = e.resource_node_id
228
            INNER JOIN resource_link rl ON rl.resource_node_id = rn.id AND $sessionJoin
229
            WHERE q.question_id = $id
230
              AND rl.deleted_at IS NULL";
231
                    $result = Database::query($sql);
232
                    while ($obj = Database::fetch_object($result)) {
233
                        $objQuestion->exerciseList[] = (int) $obj->quiz_id;
234
                    }
235
                }
236
237
                $objQuestion->parent_id = isset($object->parent_media_id)
238
                    ? (int) $object->parent_media_id
239
                    : 0;
240
241
                return $objQuestion;
242
            }
243
        }
244
245
        // question not found
246
        return false;
247
    }
248
249
    /**
250
     * returns the question title.
251
     *
252
     * @author Olivier Brouckaert
253
     *
254
     * @return string - question title
255
     */
256
    public function selectTitle()
257
    {
258
        if ('true' !== api_get_setting('editor.save_titles_as_html')) {
259
            return $this->question;
260
        }
261
262
        return Display::div($this->question, ['style' => 'display: inline-block;']);
263
    }
264
265
    public function getTitleToDisplay(Exercise $exercise, int $itemNumber): string
266
    {
267
        $showQuestionTitleHtml = ('true' === api_get_setting('editor.save_titles_as_html'));
268
        $title = '';
269
        if ('true' === api_get_setting('exercise.show_question_id')) {
270
            $title .= '<h4>#'.$this->course['code'].'-'.$this->iid.'</h4>';
271
        }
272
273
        $title .= $showQuestionTitleHtml ? '' : '<strong>';
274
        if (1 !== $exercise->getHideQuestionNumber()) {
275
            $title .= $itemNumber.'. ';
276
        }
277
        $title .= $this->selectTitle();
278
        $title .= $showQuestionTitleHtml ? '' : '</strong>';
279
280
        return Display::div(
281
            $title,
282
            ['class' => 'question_title']
283
        );
284
    }
285
286
    /**
287
     * returns the question description.
288
     *
289
     * @author Olivier Brouckaert
290
     *
291
     * @return string - question description
292
     */
293
    public function selectDescription()
294
    {
295
        return $this->description;
296
    }
297
298
    /**
299
     * returns the question weighting.
300
     *
301
     * @author Olivier Brouckaert
302
     *
303
     * @return int - question weighting
304
     */
305
    public function selectWeighting()
306
    {
307
        return $this->weighting;
308
    }
309
310
    /**
311
     * returns the answer type.
312
     *
313
     * @author Olivier Brouckaert
314
     *
315
     * @return int - answer type
316
     */
317
    public function selectType()
318
    {
319
        return $this->type;
320
    }
321
322
    /**
323
     * returns the level of the question.
324
     *
325
     * @author Nicolas Raynaud
326
     *
327
     * @return int - level of the question, 0 by default
328
     */
329
    public function getLevel()
330
    {
331
        return $this->level;
332
    }
333
334
    /**
335
     * changes the question title.
336
     *
337
     * @param string $title - question title
338
     *
339
     * @author Olivier Brouckaert
340
     */
341
    public function updateTitle($title)
342
    {
343
        $this->question = $title;
344
    }
345
346
    /**
347
     * changes the question description.
348
     *
349
     * @param string $description - question description
350
     *
351
     * @author Olivier Brouckaert
352
     */
353
    public function updateDescription($description)
354
    {
355
        $this->description = $description;
356
    }
357
358
    /**
359
     * changes the question weighting.
360
     *
361
     * @param int $weighting - question weighting
362
     *
363
     * @author Olivier Brouckaert
364
     */
365
    public function updateWeighting($weighting)
366
    {
367
        $this->weighting = $weighting;
368
    }
369
370
    /**
371
     * @param array $category
372
     *
373
     * @author Hubert Borderiou 12-10-2011
374
     */
375
    public function updateCategory($category)
376
    {
377
        $this->category = $category;
378
    }
379
380
    public function setMandatory($value)
381
    {
382
        $this->mandatory = (int) $value;
383
    }
384
385
    /**
386
     * in this version, a question can only have 1 category
387
     * if category is 0, then question has no category then delete the category entry.
388
     *
389
     * @author Hubert Borderiou 12-10-2011
390
     */
391
    public function saveCategory(int $categoryId): bool
392
    {
393
        if ($categoryId <= 0) {
394
            $this->deleteCategory();
395
        } else {
396
            // update or add category for a question
397
            $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
398
            $categoryId = (int) $categoryId;
399
            $questionId = (int) $this->id;
400
            $sql = "SELECT count(*) AS nb FROM $table
401
                    WHERE
402
                        question_id = $questionId
403
                    ";
404
            $res = Database::query($sql);
405
            $row = Database::fetch_array($res);
406
            $allowMandatory = ('true' === api_get_setting('exercise.allow_mandatory_question_in_category'));
407
            if ($row['nb'] > 0) {
408
                $extraMandatoryCondition = '';
409
                if ($allowMandatory) {
410
                    $extraMandatoryCondition = ", mandatory = {$this->mandatory}";
411
                }
412
                $sql = "UPDATE $table
413
                        SET category_id = $categoryId
414
                        $extraMandatoryCondition
415
                        WHERE
416
                            question_id = $questionId
417
                        ";
418
                Database::query($sql);
419
            } else {
420
                $sql = "INSERT INTO $table (question_id, category_id)
421
                        VALUES ($questionId, $categoryId)
422
                        ";
423
                Database::query($sql);
424
                if ($allowMandatory) {
425
                    $id = Database::insert_id();
426
                    if ($id) {
427
                        $sql = "UPDATE $table SET mandatory = {$this->mandatory}
428
                                WHERE iid = $id";
429
                        Database::query($sql);
430
                    }
431
                }
432
            }
433
        }
434
435
        return true;
436
    }
437
438
    /**
439
     * @author hubert borderiou 12-10-2011
440
     *
441
     *                      delete any category entry for question id
442
     *                      delete the category for question
443
     */
444
    public function deleteCategory(): bool
445
    {
446
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
447
        $questionId = (int) $this->id;
448
        if (empty($questionId)) {
449
            return false;
450
        }
451
        $sql = "DELETE FROM $table
452
                WHERE
453
                    question_id = $questionId
454
                ";
455
        Database::query($sql);
456
457
        return true;
458
    }
459
460
    /**
461
     * changes the question position.
462
     *
463
     * @param int $position - question position
464
     *
465
     * @author Olivier Brouckaert
466
     */
467
    public function updatePosition($position)
468
    {
469
        $this->position = $position;
470
    }
471
472
    /**
473
     * changes the question level.
474
     *
475
     * @param int $level - question level
476
     *
477
     * @author Nicolas Raynaud
478
     */
479
    public function updateLevel($level)
480
    {
481
        $this->level = $level;
482
    }
483
484
    /**
485
     * changes the answer type. If the user changes the type from "unique answer" to "multiple answers"
486
     * (or conversely) answers are not deleted, otherwise yes.
487
     *
488
     * @param int $type - answer type
489
     *
490
     * @author Olivier Brouckaert
491
     */
492
    public function updateType($type)
493
    {
494
        $table = Database::get_course_table(TABLE_QUIZ_ANSWER);
495
        $course_id = $this->course['real_id'];
496
497
        if (empty($course_id)) {
498
            $course_id = api_get_course_int_id();
499
        }
500
        // if we really change the type
501
        if ($type != $this->type) {
502
            // if we don't change from "unique answer" to "multiple answers" (or conversely)
503
            if (!in_array($this->type, [UNIQUE_ANSWER, MULTIPLE_ANSWER]) ||
504
                !in_array($type, [UNIQUE_ANSWER, MULTIPLE_ANSWER])
505
            ) {
506
                // removes old answers
507
                $sql = "DELETE FROM $table
508
                        WHERE c_id = $course_id AND question_id = ".(int) ($this->id);
509
                Database::query($sql);
510
            }
511
512
            $this->type = $type;
513
        }
514
    }
515
516
    /**
517
     * Set title.
518
     *
519
     * @param string $title
520
     */
521
    public function setTitle($title)
522
    {
523
        $this->question = $title;
524
    }
525
526
    /**
527
     * Sets extra info.
528
     *
529
     * @param string $extra
530
     */
531
    public function setExtra($extra)
532
    {
533
        $this->extra = $extra;
534
    }
535
536
    /**
537
     * updates the question in the data base
538
     * if an exercise ID is provided, we add that exercise ID into the exercise list.
539
     *
540
     * @author Olivier Brouckaert
541
     *
542
     * @param Exercise $exercise
543
     */
544
    public function save($exercise)
545
    {
546
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
547
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
548
        $em = Database::getManager();
549
        $exerciseId = $exercise->iId;
550
551
        $id = $this->id;
552
        $type = $this->type;
553
        $c_id = $this->course['real_id'];
554
555
        $courseEntity = api_get_course_entity($c_id);
556
        $categoryId = $this->category;
557
558
        $questionCategoryRepo = Container::getQuestionCategoryRepository();
559
        $questionRepo = Container::getQuestionRepository();
560
561
        // question already exists
562
        if (!empty($id)) {
563
            /** @var CQuizQuestion $question */
564
            $question = $questionRepo->find($id);
565
            if ($question) {
0 ignored issues
show
introduced by
$question is of type Chamilo\CourseBundle\Entity\CQuizQuestion, thus it always evaluated to true.
Loading history...
566
                $question
567
                    ->setQuestion($this->question)
568
                    ->setDescription($this->description)
569
                    ->setPonderation($this->weighting)
570
                    ->setPosition($this->position)
571
                    ->setType($this->type)
572
                    ->setExtra($this->extra)
573
                    ->setLevel((int) $this->level)
574
                    ->setFeedback($this->feedback)
575
                    ->setParentMediaId($this->parent_id);
576
577
                if (!empty($categoryId)) {
578
                    $category = $questionCategoryRepo->find($categoryId);
579
                    $question->updateCategory($category);
580
                }
581
582
                $em->persist($question);
583
                $em->flush();
584
585
                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

585
                Event::/** @scrutinizer ignore-call */ 
586
                       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...
586
                    LOG_QUESTION_UPDATED,
587
                    LOG_QUESTION_ID,
588
                    $this->iid
589
                );
590
                if ('true' === api_get_setting('search_enabled')) {
591
                    $this->search_engine_edit($exerciseId);
592
                }
593
            }
594
        } else {
595
            // Creates a new question
596
            $sql = "SELECT max(position)
597
                    FROM $TBL_QUESTIONS as question,
598
                    $TBL_EXERCISE_QUESTION as test_question
599
                    WHERE
600
                        question.iid = test_question.question_id AND
601
                        test_question.quiz_id = ".$exerciseId;
602
            $result = Database::query($sql);
603
            $current_position = Database::result($result, 0, 0);
604
            $this->updatePosition($current_position + 1);
605
            $position = $this->position;
606
            //$exerciseEntity = $exerciseRepo->find($exerciseId);
607
608
            $question = (new CQuizQuestion())
609
                ->setQuestion($this->question)
610
                ->setDescription($this->description)
611
                ->setPonderation($this->weighting)
612
                ->setPosition($position)
613
                ->setType($this->type)
614
                ->setExtra($this->extra)
615
                ->setLevel((int) $this->level)
616
                ->setFeedback($this->feedback)
617
                ->setParentMediaId($this->parent_id)
618
                ->setParent($courseEntity)
619
                ->addCourseLink($courseEntity, api_get_session_entity(), api_get_group_entity());
620
621
            $em->persist($question);
622
            $em->flush();
623
624
            $this->id = $question->getIid();
625
626
            if ($this->id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->id 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...
627
                Event::addEvent(
628
                    LOG_QUESTION_CREATED,
629
                    LOG_QUESTION_ID,
630
                    $this->id
631
                );
632
633
                $questionRepo->addFileFromFileRequest($question, 'imageUpload');
634
635
                // If hotspot, create first answer
636
                if (in_array($type, [HOT_SPOT, HOT_SPOT_COMBINATION, HOT_SPOT_ORDER])) {
637
                    $quizAnswer = new CQuizAnswer();
638
                    $quizAnswer
639
                        ->setQuestion($question)
640
                        ->setPonderation(10)
641
                        ->setPosition(1)
642
                        ->setHotspotCoordinates('0;0|0|0')
643
                        ->setHotspotType('square');
644
645
                    $em->persist($quizAnswer);
646
                    $em->flush();
647
                }
648
649
                if (HOT_SPOT_DELINEATION == $type) {
650
                    $quizAnswer = new CQuizAnswer();
651
                    $quizAnswer
652
                        ->setQuestion($question)
653
                        ->setPonderation(10)
654
                        ->setPosition(1)
655
                        ->setHotspotCoordinates('0;0|0|0')
656
                        ->setHotspotType('delineation');
657
658
                    $em->persist($quizAnswer);
659
                    $em->flush();
660
                }
661
662
                if ('true' === api_get_setting('search_enabled')) {
663
                    $this->search_engine_edit($exerciseId, true);
664
                }
665
            }
666
        }
667
668
        // if the question is created in an exercise
669
        if (!empty($exerciseId)) {
670
            // adds the exercise into the exercise list of this question
671
            $this->addToList($exerciseId, true);
672
        }
673
    }
674
675
    /**
676
     * @param int  $exerciseId
677
     * @param bool $addQs
678
     * @param bool $rmQs
679
     */
680
    public function search_engine_edit(
681
        $exerciseId,
682
        $addQs = false,
683
        $rmQs = false
684
    ) {
685
        // update search engine and its values table if enabled
686
        if (!empty($exerciseId) && 'true' == api_get_setting('search_enabled') &&
687
            extension_loaded('xapian')
688
        ) {
689
            $course_id = api_get_course_id();
690
            // get search_did
691
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
692
            if ($addQs || $rmQs) {
693
                //there's only one row per question on normal db and one document per question on search engine db
694
                $sql = 'SELECT * FROM %s
695
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_second_level=%s LIMIT 1';
696
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
697
            } else {
698
                $sql = 'SELECT * FROM %s
699
                    WHERE course_code=\'%s\' AND tool_id=\'%s\'
700
                    AND ref_id_high_level=%s AND ref_id_second_level=%s LIMIT 1';
701
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
702
            }
703
            $res = Database::query($sql);
704
705
            if (Database::num_rows($res) > 0 || $addQs) {
706
                $di = new ChamiloIndexer();
707
                if ($addQs) {
708
                    $question_exercises = [(int) $exerciseId];
709
                } else {
710
                    $question_exercises = [];
711
                }
712
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
713
                $di->connectDb(null, null, $lang);
714
715
                // retrieve others exercise ids
716
                $se_ref = Database::fetch_array($res);
717
                $se_doc = $di->get_document((int) $se_ref['search_did']);
718
                if (false !== $se_doc) {
719
                    if (false !== ($se_doc_data = $di->get_document_data($se_doc))) {
720
                        $se_doc_data = UnserializeApi::unserialize(
721
                            'not_allowed_classes',
722
                            $se_doc_data
723
                        );
724
                        if (isset($se_doc_data[SE_DATA]['type']) &&
725
                            SE_DOCTYPE_EXERCISE_QUESTION == $se_doc_data[SE_DATA]['type']
726
                        ) {
727
                            if (isset($se_doc_data[SE_DATA]['exercise_ids']) &&
728
                                is_array($se_doc_data[SE_DATA]['exercise_ids'])
729
                            ) {
730
                                foreach ($se_doc_data[SE_DATA]['exercise_ids'] as $old_value) {
731
                                    if (!in_array($old_value, $question_exercises)) {
732
                                        $question_exercises[] = $old_value;
733
                                    }
734
                                }
735
                            }
736
                        }
737
                    }
738
                }
739
                if ($rmQs) {
740
                    while (false !== ($key = array_search($exerciseId, $question_exercises))) {
741
                        unset($question_exercises[$key]);
742
                    }
743
                }
744
745
                // build the chunk to index
746
                $ic_slide = new IndexableChunk();
747
                $ic_slide->addValue('title', $this->question);
748
                $ic_slide->addCourseId($course_id);
749
                $ic_slide->addToolId(TOOL_QUIZ);
750
                $xapian_data = [
751
                    SE_COURSE_ID => $course_id,
752
                    SE_TOOL_ID => TOOL_QUIZ,
753
                    SE_DATA => [
754
                        'type' => SE_DOCTYPE_EXERCISE_QUESTION,
755
                        'exercise_ids' => $question_exercises,
756
                        'question_id' => (int) $this->id,
757
                    ],
758
                    SE_USER => (int) api_get_user_id(),
759
                ];
760
                $ic_slide->xapian_data = serialize($xapian_data);
761
                $ic_slide->addValue('content', $this->description);
762
763
                //TODO: index answers, see also form validation on question_admin.inc.php
764
765
                $di->remove_document($se_ref['search_did']);
766
                $di->addChunk($ic_slide);
767
768
                //index and return search engine document id
769
                if (!empty($question_exercises)) { // if empty there is nothing to index
770
                    $did = $di->index();
771
                    unset($di);
772
                }
773
                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...
774
                    // save it to db
775
                    if ($addQs || $rmQs) {
776
                        $sql = "DELETE FROM %s
777
                            WHERE course_code = '%s' AND tool_id = '%s' AND ref_id_second_level = '%s'";
778
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
779
                    } else {
780
                        $sql = "DELETE FROM %S
781
                            WHERE
782
                                course_code = '%s'
783
                                AND tool_id = '%s'
784
                                AND tool_id = '%s'
785
                                AND ref_id_high_level = '%s'
786
                                AND ref_id_second_level = '%s'";
787
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
788
                    }
789
                    Database::query($sql);
790
                    if ($rmQs) {
791
                        if (!empty($question_exercises)) {
792
                            $sql = "INSERT INTO %s (
793
                                    id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
794
                                )
795
                                VALUES (
796
                                    NULL, '%s', '%s', %s, %s, %s
797
                                )";
798
                            $sql = sprintf(
799
                                $sql,
800
                                $tbl_se_ref,
801
                                $course_id,
802
                                TOOL_QUIZ,
803
                                array_shift($question_exercises),
804
                                $this->id,
805
                                $did
806
                            );
807
                            Database::query($sql);
808
                        }
809
                    } else {
810
                        $sql = "INSERT INTO %s (
811
                                id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
812
                            )
813
                            VALUES (
814
                                NULL , '%s', '%s', %s, %s, %s
815
                            )";
816
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id, $did);
817
                        Database::query($sql);
818
                    }
819
                }
820
            }
821
        }
822
    }
823
824
    /**
825
     * adds an exercise into the exercise list.
826
     *
827
     * @author Olivier Brouckaert
828
     *
829
     * @param int  $exerciseId - exercise ID
830
     * @param bool $fromSave   - from $this->save() or not
831
     */
832
    public function addToList($exerciseId, $fromSave = false)
833
    {
834
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
835
        $id = (int) $this->id;
836
        $exerciseId = (int) $exerciseId;
837
838
        // checks if the exercise ID is not in the list
839
        if (!empty($exerciseId) && !in_array($exerciseId, $this->exerciseList)) {
840
            $this->exerciseList[] = $exerciseId;
841
            $courseId = isset($this->course['real_id']) ? $this->course['real_id'] : 0;
842
            $newExercise = new Exercise($courseId);
843
            $newExercise->read($exerciseId, false);
844
            $count = $newExercise->getQuestionCount();
845
            $count++;
846
            $sql = "INSERT INTO $exerciseRelQuestionTable (question_id, quiz_id, question_order)
847
                    VALUES (".$id.', '.$exerciseId.", '$count')";
848
            Database::query($sql);
849
850
            // we do not want to reindex if we had just saved adnd indexed the question
851
            if (!$fromSave) {
852
                $this->search_engine_edit($exerciseId, true);
853
            }
854
        }
855
    }
856
857
    /**
858
     * removes an exercise from the exercise list.
859
     *
860
     * @author Olivier Brouckaert
861
     *
862
     * @param int $exerciseId - exercise ID
863
     * @param int $courseId
864
     *
865
     * @return bool - true if removed, otherwise false
866
     */
867
    public function removeFromList($exerciseId, $courseId = 0)
868
    {
869
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
870
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
871
        $id = (int) $this->id;
872
        $exerciseId = (int) $exerciseId;
873
874
        // searches the position of the exercise ID in the list
875
        $pos = array_search($exerciseId, $this->exerciseList);
876
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
877
878
        // exercise not found
879
        if (false === $pos) {
880
            return false;
881
        } else {
882
            // deletes the position in the array containing the wanted exercise ID
883
            unset($this->exerciseList[$pos]);
884
            //update order of other elements
885
            $sql = "SELECT question_order
886
                    FROM $table
887
                    WHERE
888
                        question_id = $id AND
889
                        quiz_id = $exerciseId";
890
            $res = Database::query($sql);
891
            if (Database::num_rows($res) > 0) {
892
                $row = Database::fetch_array($res);
893
                if (!empty($row['question_order'])) {
894
                    $sql = "UPDATE $table
895
                            SET question_order = question_order-1
896
                            WHERE
897
                                quiz_id = $exerciseId AND
898
                                question_order > ".$row['question_order'];
899
                    Database::query($sql);
900
                }
901
            }
902
903
            $sql = "DELETE FROM $table
904
                    WHERE
905
                        question_id = $id AND
906
                        quiz_id = $exerciseId";
907
            Database::query($sql);
908
909
            $reset = "UPDATE $tableQuestion
910
                  SET parent_media_id = NULL
911
                  WHERE parent_media_id = $id";
912
            Database::query($reset);
913
914
            return true;
915
        }
916
    }
917
918
    /**
919
     * Deletes a question from the database
920
     * the parameter tells if the question is removed from all exercises (value = 0),
921
     * or just from one exercise (value = exercise ID).
922
     *
923
     * @author Olivier Brouckaert
924
     *
925
     * @param int $deleteFromEx - exercise ID if the question is only removed from one exercise
926
     *
927
     * @return bool
928
     */
929
    public function delete($deleteFromEx = 0)
930
    {
931
        if (empty($this->course)) {
932
            return false;
933
        }
934
935
        $courseId = $this->course['real_id'];
936
937
        if (empty($courseId)) {
938
            return false;
939
        }
940
941
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
942
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
943
        $TBL_REPONSES = Database::get_course_table(TABLE_QUIZ_ANSWER);
944
        $TBL_QUIZ_QUESTION_REL_CATEGORY = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
945
946
        $id = (int) $this->id;
947
948
        // if the question must be removed from all exercises
949
        if (!$deleteFromEx) {
950
            //update the question_order of each question to avoid inconsistencies
951
            $sql = "SELECT quiz_id, question_order
952
                    FROM $TBL_EXERCISE_QUESTION
953
                    WHERE question_id = ".$id;
954
955
            $res = Database::query($sql);
956
            if (Database::num_rows($res) > 0) {
957
                while ($row = Database::fetch_array($res)) {
958
                    if (!empty($row['question_order'])) {
959
                        $sql = "UPDATE $TBL_EXERCISE_QUESTION
960
                                SET question_order = question_order-1
961
                                WHERE
962
                                    quiz_id = ".(int) ($row['quiz_id']).' AND
963
                                    question_order > '.$row['question_order'];
964
                        Database::query($sql);
965
                    }
966
                }
967
            }
968
969
            $reset = "UPDATE $TBL_QUESTIONS
970
                  SET parent_media_id = NULL
971
                  WHERE parent_media_id = $id";
972
            Database::query($reset);
973
974
            $sql = "DELETE FROM $TBL_EXERCISE_QUESTION
975
                    WHERE question_id = ".$id;
976
            Database::query($sql);
977
978
            $sql = "DELETE FROM $TBL_QUESTIONS
979
                    WHERE iid = ".$id;
980
            Database::query($sql);
981
982
            $sql = "DELETE FROM $TBL_REPONSES
983
                    WHERE question_id = ".$id;
984
            Database::query($sql);
985
986
            // remove the category of this question in the question_rel_category table
987
            $sql = "DELETE FROM $TBL_QUIZ_QUESTION_REL_CATEGORY
988
                    WHERE
989
                        question_id = ".$id;
990
            Database::query($sql);
991
992
            // Add extra fields.
993
            $extraField = new ExtraFieldValue('question');
994
            $extraField->deleteValuesByItem($this->iid);
995
996
            /*api_item_property_update(
997
                $this->course,
998
                TOOL_QUIZ,
999
                $id,
1000
                'QuizQuestionDeleted',
1001
                api_get_user_id()
1002
            );*/
1003
            Event::addEvent(
1004
                LOG_QUESTION_DELETED,
1005
                LOG_QUESTION_ID,
1006
                $this->iid
1007
            );
1008
            //$this->removePicture();
1009
        } else {
1010
            // just removes the exercise from the list
1011
            $this->removeFromList($deleteFromEx, $courseId);
1012
            if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
1013
                // disassociate question with this exercise
1014
                $this->search_engine_edit($deleteFromEx, false, true);
1015
            }
1016
            /*
1017
            api_item_property_update(
1018
                $this->course,
1019
                TOOL_QUIZ,
1020
                $id,
1021
                'QuizQuestionDeleted',
1022
                api_get_user_id()
1023
            );*/
1024
            Event::addEvent(
1025
                LOG_QUESTION_REMOVED_FROM_QUIZ,
1026
                LOG_QUESTION_ID,
1027
                $this->iid
1028
            );
1029
        }
1030
1031
        return true;
1032
    }
1033
1034
    /**
1035
     * Duplicates the question.
1036
     *
1037
     * @author Olivier Brouckaert
1038
     *
1039
     * @param array $courseInfo Course info of the destination course
1040
     *
1041
     * @return false|string ID of the new question
1042
     */
1043
    public function duplicate($courseInfo = [])
1044
    {
1045
        $courseInfo = empty($courseInfo) ? $this->course : $courseInfo;
1046
1047
        if (empty($courseInfo)) {
1048
            return false;
1049
        }
1050
        $TBL_QUESTION_OPTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1051
1052
        $questionText = $this->question;
1053
        $description = $this->description;
1054
1055
        // Using the same method used in the course copy to transform URLs
1056
        if ($this->course['id'] != $courseInfo['id']) {
1057
            $description = DocumentManager::replaceUrlWithNewCourseCode(
1058
                $description,
1059
                $this->course['code'],
1060
                $courseInfo['id']
1061
            );
1062
            $questionText = DocumentManager::replaceUrlWithNewCourseCode(
1063
                $questionText,
1064
                $this->course['code'],
1065
                $courseInfo['id']
1066
            );
1067
        }
1068
1069
        $course_id = $courseInfo['real_id'];
1070
1071
        // Read the source options
1072
        $options = self::readQuestionOption($this->id, $this->course['real_id']);
1073
1074
        $em = Database::getManager();
1075
        $courseEntity = api_get_course_entity($course_id);
1076
1077
        $question = (new CQuizQuestion())
1078
            ->setQuestion($questionText)
1079
            ->setDescription($description)
1080
            ->setPonderation($this->weighting)
1081
            ->setPosition($this->position)
1082
            ->setType($this->type)
1083
            ->setExtra($this->extra)
1084
            ->setLevel($this->level)
1085
            ->setFeedback($this->feedback)
1086
            ->setParent($courseEntity)
1087
            ->addCourseLink($courseEntity)
1088
        ;
1089
1090
        $em->persist($question);
1091
        $em->flush();
1092
        $newQuestionId = $question->getIid();
1093
1094
        if ($newQuestionId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $newQuestionId 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...
1095
            // Add extra fields.
1096
            $extraField = new ExtraFieldValue('question');
1097
            $extraField->copy($this->iid, $newQuestionId);
1098
1099
            if (!empty($options)) {
1100
                // Saving the quiz_options
1101
                foreach ($options as $item) {
1102
                    $item['question_id'] = $newQuestionId;
1103
                    $item['c_id'] = $course_id;
1104
                    unset($item['iid']);
1105
                    unset($item['iid']);
1106
                    Database::insert($TBL_QUESTION_OPTIONS, $item);
1107
                }
1108
            }
1109
1110
            // Duplicates the picture of the hotspot
1111
            // @todo implement copy of hotspot question
1112
            if (HOT_SPOT == $this->type) {
1113
                throw new Exception('implement copy of hotspot question');
1114
            }
1115
        }
1116
1117
        return $newQuestionId;
1118
    }
1119
1120
    /**
1121
     * @return string
1122
     */
1123
    public function get_question_type_name()
1124
    {
1125
        $key = self::$questionTypes[$this->type];
1126
1127
        return get_lang($key[1]);
1128
    }
1129
1130
    /**
1131
     * @param string $type
1132
     */
1133
    public static function get_question_type($type)
1134
    {
1135
        return self::$questionTypes[$type];
1136
    }
1137
1138
    /**
1139
     * @return array
1140
     */
1141
    public static function getQuestionTypeList(): array
1142
    {
1143
        $list = self::$questionTypes;
1144
1145
        if ('true' !== api_get_setting('enable_quiz_scenario')) {
1146
            unset($list[HOT_SPOT_DELINEATION]);
1147
        }
1148
1149
        ksort($list, SORT_NUMERIC);
1150
1151
        return $list;
1152
    }
1153
1154
    /**
1155
     * Returns an instance of the class corresponding to the type.
1156
     *
1157
     * @param int $type the type of the question
1158
     *
1159
     * @return $this instance of a Question subclass (or of Questionc class by default)
1160
     */
1161
    public static function getInstance($type)
1162
    {
1163
        if (null !== $type) {
1164
            [$fileName, $className] = self::get_question_type($type);
1165
            if (!empty($fileName)) {
1166
                if (class_exists($className)) {
1167
                    return new $className();
1168
                } else {
1169
                    echo 'Can\'t instanciate class '.$className.' of type '.$type;
1170
                }
1171
            }
1172
        }
1173
1174
        return null;
1175
    }
1176
1177
    /**
1178
     * Creates the form to create / edit a question
1179
     * A subclass can redefine this function to add fields...
1180
     *
1181
     * @param FormValidator $form
1182
     * @param Exercise      $exercise
1183
     */
1184
    public function createForm(&$form, $exercise)
1185
    {
1186
        $zoomOptions = api_get_setting('exercise.quiz_image_zoom', true);
1187
        if (isset($zoomOptions['options'])) {
1188
            $finderFolder = api_get_path(WEB_PATH).'vendor/studio-42/elfinder/';
1189
            echo '<!-- elFinder CSS (REQUIRED) -->';
1190
            echo '<link rel="stylesheet" type="text/css" media="screen" href="'.$finderFolder.'css/elfinder.full.css">';
1191
            echo '<link rel="stylesheet" type="text/css" media="screen" href="'.$finderFolder.'css/theme.css">';
1192
1193
            echo '<!-- elFinder JS (REQUIRED) -->';
1194
            echo '<script src="'.$finderFolder.'js/elfinder.full.js"></script>';
1195
1196
            echo '<!-- elFinder translation (OPTIONAL) -->';
1197
            $language = 'en';
1198
            $platformLanguage = api_get_language_isocode();
1199
            $iso = api_get_language_isocode($platformLanguage);
1200
            $filePart = "vendor/studio-42/elfinder/js/i18n/elfinder.$iso.js";
1201
            $file = api_get_path(SYS_PATH).$filePart;
1202
            $includeFile = '';
1203
            if (file_exists($file)) {
1204
                $includeFile = '<script src="'.api_get_path(WEB_PATH).$filePart.'"></script>';
1205
                $language = $iso;
1206
            }
1207
            echo $includeFile;
1208
            echo '<script>
1209
            $(function() {
1210
                $(".create_img_link").click(function(e){
1211
                    e.preventDefault();
1212
                    e.stopPropagation();
1213
                    var imageZoom = $("input[name=\'imageZoom\']").val();
1214
                    var imageWidth = $("input[name=\'imageWidth\']").val();
1215
                    CKEDITOR.instances.questionDescription.insertHtml(\'<img id="zoom_picture" class="zoom_picture" src="\'+imageZoom+\'" data-zoom-image="\'+imageZoom+\'" width="\'+imageWidth+\'px" />\');
1216
                });
1217
1218
                $("input[name=\'imageZoom\']").on("click", function(){
1219
                    var elf = $("#elfinder").elfinder({
1220
                        url : "'.api_get_path(WEB_LIBRARY_PATH).'elfinder/connectorAction.php?'.api_get_cidreq().'",
1221
                        getFileCallback: function(file) {
1222
                            var filePath = file; //file contains the relative url.
1223
                            var imgPath = "<img src = \'"+filePath+"\'/>";
1224
                            $("input[name=\'imageZoom\']").val(filePath.url);
1225
                            $("#elfinder").remove(); //close the window after image is selected
1226
                        },
1227
                        startPathHash: "l2_Lw", // Sets the course driver as default
1228
                        resizable: false,
1229
                        lang: "'.$language.'"
1230
                    }).elfinder("instance");
1231
                });
1232
            });
1233
            </script>';
1234
            echo '<div id="elfinder"></div>';
1235
        }
1236
1237
        // question name
1238
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
1239
            $editorConfig = ['ToolbarSet' => 'TitleAsHtml'];
1240
            $form->addHtmlEditor(
1241
                'questionName',
1242
                get_lang('Question'),
1243
                false,
1244
                false,
1245
                $editorConfig
1246
            );
1247
        } else {
1248
            $form->addText('questionName', get_lang('Question'));
1249
        }
1250
1251
        $form->addRule('questionName', get_lang('Please type the question'), 'required');
1252
1253
        // default content
1254
        $isContent = isset($_REQUEST['isContent']) ? (int) $_REQUEST['isContent'] : null;
1255
1256
        // Question type
1257
        $answerType = isset($_REQUEST['answerType']) ? (int) $_REQUEST['answerType'] : null;
1258
        $form->addHidden('answerType', $answerType);
1259
1260
        // html editor
1261
        $editorConfig = [
1262
            'ToolbarSet' => 'TestQuestionDescription',
1263
            'Height' => '150',
1264
        ];
1265
1266
        if (!api_is_allowed_to_edit(null, true)) {
1267
            $editorConfig['UserStatus'] = 'student';
1268
        }
1269
1270
        $form->addButtonAdvancedSettings('advanced_params');
1271
        $form->addHtml('<div id="advanced_params_options" style="display:none">');
1272
1273
        if (isset($zoomOptions['options'])) {
1274
            $form->addElement('text', 'imageZoom', get_lang('Image URL'));
1275
            $form->addElement('text', 'imageWidth', get_lang('px width'));
1276
            $form->addButton('btn_create_img', get_lang('Add to editor'), 'plus', 'info', 'small', 'create_img_link');
1277
        }
1278
1279
        $form->addHtmlEditor(
1280
            'questionDescription',
1281
            get_lang('Enrich question'),
1282
            false,
1283
            false,
1284
            $editorConfig
1285
        );
1286
1287
        if (MEDIA_QUESTION != $this->type) {
1288
            // Advanced parameters.
1289
            $form->addSelect(
1290
                'questionLevel',
1291
                get_lang('Difficulty'),
1292
                self::get_default_levels()
1293
            );
1294
1295
            // Categories.
1296
            $form->addSelect(
1297
                'questionCategory',
1298
                get_lang('Category'),
1299
                TestCategory::getCategoriesIdAndName()
1300
            );
1301
1302
            $courseMedias = self::prepare_course_media_select($exercise->iId);
1303
            $form->addSelect(
1304
                'parent_id',
1305
                get_lang('Attach to media'),
1306
                $courseMedias
1307
            );
1308
1309
            if (EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM == $exercise->getQuestionSelectionType() &&
1310
                ('true' === api_get_setting('exercise.allow_mandatory_question_in_category'))
1311
            ) {
1312
                $form->addCheckBox('mandatory', get_lang('Mandatory?'));
1313
            }
1314
1315
            //global $text;
1316
            $text = get_lang('Save the question');
1317
            switch ($this->type) {
1318
                case UNIQUE_ANSWER:
1319
                    $buttonGroup = [];
1320
                    $buttonGroup[] = $form->addButtonSave(
1321
                        $text,
1322
                        'submitQuestion',
1323
                        true
1324
                    );
1325
                    $buttonGroup[] = $form->addButton(
1326
                        'convertAnswer',
1327
                        get_lang('Convert to multiple answer'),
1328
                        'dot-circle-o',
1329
                        'default',
1330
                        null,
1331
                        null,
1332
                        null,
1333
                        true
1334
                    );
1335
                    $form->addGroup($buttonGroup);
1336
1337
                    break;
1338
                case MULTIPLE_ANSWER:
1339
                    $buttonGroup = [];
1340
                    $buttonGroup[] = $form->addButtonSave(
1341
                        $text,
1342
                        'submitQuestion',
1343
                        true
1344
                    );
1345
                    $buttonGroup[] = $form->addButton(
1346
                        'convertAnswer',
1347
                        get_lang('Convert to unique answer'),
1348
                        'check-square-o',
1349
                        'default',
1350
                        null,
1351
                        null,
1352
                        null,
1353
                        true
1354
                    );
1355
                    $form->addGroup($buttonGroup);
1356
1357
                    break;
1358
            }
1359
        }
1360
1361
        $form->addElement('html', '</div>');
1362
1363
        if (!isset($_GET['fromExercise'])) {
1364
            switch ($answerType) {
1365
                case 1:
1366
                    $this->question = get_lang('Select the good reasoning');
1367
1368
                    break;
1369
                case 2:
1370
                    $this->question = get_lang('The marasmus is a consequence of');
1371
1372
                    break;
1373
                case 3:
1374
                    $this->question = get_lang('Calculate the Body Mass Index');
1375
1376
                    break;
1377
                case 4:
1378
                    $this->question = get_lang('Order the operations');
1379
1380
                    break;
1381
                case 5:
1382
                    $this->question = get_lang('List what you consider the 10 top qualities of a good project manager?');
1383
1384
                    break;
1385
                case 9:
1386
                    $this->question = get_lang('The marasmus is a consequence of');
1387
1388
                    break;
1389
            }
1390
        }
1391
1392
        if (null !== $exercise) {
1393
            if ($exercise->questionFeedbackEnabled && $this->showFeedback($exercise)) {
1394
                $form->addTextarea('feedback', get_lang('Feedback if not correct'));
1395
            }
1396
        }
1397
1398
        $extraField = new ExtraField('question');
1399
        $extraField->addElements($form, $this->iid);
1400
1401
        // default values
1402
        $defaults = [
1403
            'questionName'        => $this->question,
1404
            'questionDescription' => $this->description,
1405
            'questionLevel'       => $this->level,
1406
            'questionCategory'    => $this->category,
1407
            'feedback'            => $this->feedback,
1408
            'mandatory'           => $this->mandatory,
1409
            'parent_id'           => $this->parent_id,
1410
        ];
1411
1412
        // Came from he question pool
1413
        if (isset($_GET['fromExercise'])) {
1414
            $form->setDefaults($defaults);
1415
        }
1416
1417
        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...
1418
            $form->setDefaults($defaults);
1419
        }
1420
1421
        /*if (!empty($_REQUEST['myid'])) {
1422
            $form->setDefaults($defaults);
1423
        } else {
1424
            if ($isContent == 1) {
1425
                $form->setDefaults($defaults);
1426
            }
1427
        }*/
1428
    }
1429
1430
    /**
1431
     * Function which process the creation of questions.
1432
     */
1433
    public function processCreation(FormValidator $form, Exercise $exercise)
1434
    {
1435
        $this->parent_id = (int) $form->getSubmitValue('parent_id');
1436
        $this->updateTitle($form->getSubmitValue('questionName'));
1437
        $this->updateDescription($form->getSubmitValue('questionDescription'));
1438
        $this->updateLevel($form->getSubmitValue('questionLevel'));
1439
        $this->updateCategory($form->getSubmitValue('questionCategory'));
1440
        $this->setMandatory($form->getSubmitValue('mandatory'));
1441
        $this->setFeedback($form->getSubmitValue('feedback'));
1442
1443
        //Save normal question if NOT media
1444
        if (MEDIA_QUESTION != $this->type) {
1445
            $this->save($exercise);
1446
            // modify the exercise
1447
            $exercise->addToList($this->id);
1448
            $exercise->update_question_positions();
1449
1450
            $params = $form->exportValues();
1451
            $params['item_id'] = $this->id;
1452
1453
            $extraFieldValues = new ExtraFieldValue('question');
1454
            $extraFieldValues->saveFieldValues($params);
1455
        }
1456
    }
1457
1458
    /**
1459
     * Creates the form to create / edit the answers of the question.
1460
     */
1461
    abstract public function createAnswersForm(FormValidator $form);
1462
1463
    /**
1464
     * Process the creation of answers.
1465
     *
1466
     * @param FormValidator $form
1467
     * @param Exercise      $exercise
1468
     */
1469
    abstract public function processAnswersCreation($form, $exercise);
1470
1471
    /**
1472
     * Displays the menu of question types.
1473
     *
1474
     * @param Exercise $objExercise
1475
     */
1476
    public static function displayTypeMenu(Exercise $objExercise)
1477
    {
1478
        if (empty($objExercise)) {
1479
            return '';
1480
        }
1481
1482
        $feedbackType = $objExercise->getFeedbackType();
1483
        $exerciseId   = $objExercise->id;
1484
1485
        $questionTypeList = self::getQuestionTypeList();
1486
1487
        if (!isset($feedbackType)) {
1488
            $feedbackType = 0;
1489
        }
1490
1491
        switch ($feedbackType) {
1492
            case EXERCISE_FEEDBACK_TYPE_DIRECT:
1493
                $questionTypeList = [
1494
                    UNIQUE_ANSWER        => self::$questionTypes[UNIQUE_ANSWER],
1495
                    HOT_SPOT_DELINEATION => self::$questionTypes[HOT_SPOT_DELINEATION],
1496
                ];
1497
1498
                break;
1499
            case EXERCISE_FEEDBACK_TYPE_POPUP:
1500
                $questionTypeList = [
1501
                    UNIQUE_ANSWER        => self::$questionTypes[UNIQUE_ANSWER],
1502
                    MULTIPLE_ANSWER      => self::$questionTypes[MULTIPLE_ANSWER],
1503
                    DRAGGABLE            => self::$questionTypes[DRAGGABLE],
1504
                    HOT_SPOT_DELINEATION => self::$questionTypes[HOT_SPOT_DELINEATION],
1505
                    CALCULATED_ANSWER    => self::$questionTypes[CALCULATED_ANSWER],
1506
                ];
1507
1508
                break;
1509
            default:
1510
                unset($questionTypeList[HOT_SPOT_DELINEATION]);
1511
1512
                break;
1513
        }
1514
1515
        echo '<div class="card">';
1516
        echo '  <div class="card-body">';
1517
        echo '    <ul class="qtype-menu flex flex-wrap gap-x-2 gap-y-2 items-center justify-start w-full">';
1518
        foreach ($questionTypeList as $i => $type) {
1519
            /** @var Question $type */
1520
            $type = new $type[1]();
1521
            $img  = $type->getTypePicture();
1522
            $expl = $type->getExplanation();
1523
1524
            echo '      <li class="flex items-center justify-center">';
1525
1526
            $icon = Display::url(
1527
                Display::return_icon($img, $expl, null, ICON_SIZE_BIG),
1528
                'admin.php?' . api_get_cidreq() . '&' . http_build_query([
1529
                    'newQuestion' => 'yes',
1530
                    'answerType'  => $i,
1531
                    'exerciseId'  => $exerciseId,
1532
                ]),
1533
                ['title' => $expl, 'class' => 'block']
1534
            );
1535
1536
            if (false === $objExercise->force_edit_exercise_in_lp && $objExercise->exercise_was_added_in_lp) {
1537
                $img  = pathinfo($img);
1538
                $img  = $img['filename'].'_na.'.$img['extension'];
1539
                $icon = Display::return_icon($img, $expl, null, ICON_SIZE_BIG);
1540
            }
1541
            echo $icon;
1542
            echo '      </li>';
1543
        }
1544
1545
        echo '      <li class="flex items-center justify-center">';
1546
        if ($objExercise->exercise_was_added_in_lp) {
1547
            echo Display::getMdiIcon('database', 'ch-tool-icon-disabled', null, ICON_SIZE_BIG, get_lang('Recycle existing questions'));
1548
        } else {
1549
            $href = in_array($feedbackType, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])
1550
                ? 'question_pool.php?' . api_get_cidreq() . "&type=1&fromExercise={$exerciseId}"
1551
                : 'question_pool.php?' . api_get_cidreq() . "&fromExercise={$exerciseId}";
1552
1553
            echo Display::url(
1554
                Display::getMdiIcon('database', 'ch-tool-icon', null, ICON_SIZE_BIG, get_lang('Recycle existing questions')),
1555
                $href,
1556
                ['class' => 'block', 'title' => get_lang('Recycle existing questions')]
1557
            );
1558
        }
1559
        echo '      </li>';
1560
1561
        echo '    </ul>';
1562
        echo '  </div>';
1563
        echo '</div>';
1564
    }
1565
1566
    /**
1567
     * @param string $name
1568
     * @param int    $position
1569
     *
1570
     * @return CQuizQuestion|null
1571
     */
1572
    public static function saveQuestionOption(CQuizQuestion $question, $name, $position = 0)
1573
    {
1574
        $option = new CQuizQuestionOption();
1575
        $option
1576
            ->setQuestion($question)
1577
            ->setTitle($name)
1578
            ->setPosition($position)
1579
        ;
1580
        $em = Database::getManager();
1581
        $em->persist($option);
1582
        $em->flush();
1583
    }
1584
1585
    /**
1586
     * @param int $question_id
1587
     * @param int $course_id
1588
     */
1589
    public static function deleteAllQuestionOptions($question_id, $course_id)
1590
    {
1591
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1592
        Database::delete(
1593
            $table,
1594
            [
1595
                'c_id = ? AND question_id = ?' => [
1596
                    $course_id,
1597
                    $question_id,
1598
                ],
1599
            ]
1600
        );
1601
    }
1602
1603
    /**
1604
     * @param int $question_id
1605
     *
1606
     * @return array
1607
     */
1608
    public static function readQuestionOption($question_id)
1609
    {
1610
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1611
1612
        return Database::select(
1613
            '*',
1614
            $table,
1615
            [
1616
                'where' => [
1617
                    'question_id = ?' => [
1618
                        $question_id,
1619
                    ],
1620
                ],
1621
                'order' => 'iid ASC',
1622
            ]
1623
        );
1624
    }
1625
1626
    /**
1627
     * Shows question title an description.
1628
     *
1629
     * @param int   $counter
1630
     * @param array $score
1631
     *
1632
     * @return string HTML string with the header of the question (before the answers table)
1633
     */
1634
    public function return_header(Exercise $exercise, $counter = null, $score = [])
1635
    {
1636
        $counterLabel = '';
1637
        if (!empty($counter)) {
1638
            $counterLabel = (int) $counter;
1639
        }
1640
1641
        $scoreLabel = get_lang('Wrong');
1642
        if (in_array($exercise->results_disabled, [
1643
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
1644
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
1645
        ])
1646
        ) {
1647
            $scoreLabel = get_lang('Wrong answer. The correct one was:');
1648
        }
1649
1650
        $class = 'error';
1651
        if (isset($score['pass']) && true == $score['pass']) {
1652
            $scoreLabel = get_lang('Correct');
1653
1654
            if (in_array($exercise->results_disabled, [
1655
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
1656
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
1657
            ])
1658
            ) {
1659
                $scoreLabel = get_lang('Correct answer');
1660
            }
1661
            $class = 'success';
1662
        }
1663
1664
        switch ($this->type) {
1665
            case FREE_ANSWER:
1666
            case UPLOAD_ANSWER:
1667
            case ORAL_EXPRESSION:
1668
            case ANNOTATION:
1669
                $score['revised'] = isset($score['revised']) ? $score['revised'] : false;
1670
                if (true == $score['revised']) {
1671
                    $scoreLabel = get_lang('Reviewed');
1672
                    $class = '';
1673
                } else {
1674
                    $scoreLabel = get_lang('Not reviewed');
1675
                    $class = 'warning';
1676
                    if (isset($score['weight'])) {
1677
                        $weight = float_format($score['weight'], 1);
1678
                        $score['result'] = ' ? / '.$weight;
1679
                    }
1680
                    $model = ExerciseLib::getCourseScoreModel();
1681
                    if (!empty($model)) {
1682
                        $score['result'] = ' ? ';
1683
                    }
1684
1685
                    $hide = ('true' === api_get_setting('exercise.hide_free_question_score'));
1686
                    if (true === $hide) {
1687
                        $score['result'] = '-';
1688
                    }
1689
                }
1690
1691
                break;
1692
            case UNIQUE_ANSWER:
1693
                if (in_array($exercise->results_disabled, [
1694
                    RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
1695
                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
1696
                ])
1697
                ) {
1698
                    if (isset($score['user_answered'])) {
1699
                        if (false === $score['user_answered']) {
1700
                            $scoreLabel = get_lang('Unanswered');
1701
                            $class = 'info';
1702
                        }
1703
                    }
1704
                }
1705
1706
                break;
1707
        }
1708
1709
        // display question category, if any
1710
        $header = '';
1711
        if ($exercise->display_category_name) {
1712
            $header = TestCategory::returnCategoryAndTitle($this->id);
1713
        }
1714
        $show_media = '';
1715
        if ($show_media) {
1716
            $header .= $this->show_media_content();
1717
        }
1718
1719
        $scoreCurrent = [
1720
            'used' => isset($score['score']) ? $score['score'] : '',
1721
            'missing' => isset($score['weight']) ? $score['weight'] : '',
1722
        ];
1723
        $header .= Display::page_subheader2($counterLabel.'. '.$this->question);
1724
1725
        $showRibbon = true;
1726
        // dont display score for certainty degree questions
1727
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $this->type) {
1728
            $showRibbon = false;
1729
            $ribbonResult = ('true' === api_get_setting('exercise.show_exercise_question_certainty_ribbon_result'));
1730
            if (true === $ribbonResult) {
1731
                $showRibbon = true;
1732
            }
1733
        }
1734
1735
        if ($showRibbon && isset($score['result'])) {
1736
            if (in_array($exercise->results_disabled, [
1737
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
1738
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
1739
            ])
1740
            ) {
1741
                $score['result'] = null;
1742
            }
1743
            $header .= $exercise->getQuestionRibbon($class, $scoreLabel, $score['result'], $scoreCurrent);
1744
        }
1745
1746
        if (READING_COMPREHENSION != $this->type) {
1747
            // Do not show the description (the text to read) if the question is of type READING_COMPREHENSION
1748
            $header .= Display::div(
1749
                $this->description,
1750
                ['class' => 'question-answer-result__header-description']
1751
            );
1752
        } else {
1753
            /** @var ReadingComprehension $this */
1754
            if (true === $score['pass']) {
1755
                $message = Display::div(
1756
                    sprintf(
1757
                        get_lang(
1758
                            'Congratulations, you have reached and correctly understood, at a speed of %s words per minute, a text of a total %s words.'
1759
                        ),
1760
                        ReadingComprehension::$speeds[$this->level],
1761
                        $this->getWordsCount()
1762
                    )
1763
                );
1764
            } else {
1765
                $message = Display::div(
1766
                    sprintf(
1767
                        get_lang(
1768
                            'Sorry, it seems like a speed of %s words/minute was too fast for this text of %s words.'
1769
                        ),
1770
                        ReadingComprehension::$speeds[$this->level],
1771
                        $this->getWordsCount()
1772
                    )
1773
                );
1774
            }
1775
            $header .= $message.'<br />';
1776
        }
1777
1778
        if ($exercise->hideComment && in_array($this->type, [HOT_SPOT, HOT_SPOT_COMBINATION])) {
1779
            $header .= Display::return_message(get_lang('Results only available online'));
1780
1781
            return $header;
1782
        }
1783
1784
        if (isset($score['pass']) && false === $score['pass']) {
1785
            if ($this->showFeedback($exercise)) {
1786
                $header .= $this->returnFormatFeedback();
1787
            }
1788
        }
1789
1790
        return Display::div(
1791
            $header,
1792
            ['class' => 'question-answer-result__header']
1793
        );
1794
    }
1795
1796
    /**
1797
     * @deprecated
1798
     * Create a question from a set of parameters
1799
     *
1800
     * @param int    $question_name        Quiz ID
1801
     * @param string $question_description Question name
1802
     * @param int    $max_score            Maximum result for the question
1803
     * @param int    $type                 Type of question (see constants at beginning of question.class.php)
1804
     * @param int    $level                Question level/category
1805
     * @param string $quiz_id
1806
     */
1807
    public function create_question(
1808
        $quiz_id,
1809
        $question_name,
1810
        $question_description = '',
1811
        $max_score = 0,
1812
        $type = 1,
1813
        $level = 1
1814
    ) {
1815
        $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
1816
        $tbl_quiz_rel_question = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1817
1818
        $quiz_id = (int) $quiz_id;
1819
        $max_score = (float) $max_score;
1820
        $type = (int) $type;
1821
        $level = (int) $level;
1822
1823
        // Get the max position
1824
        $sql = "SELECT max(position) as max_position
1825
                FROM $tbl_quiz_question q
1826
                INNER JOIN $tbl_quiz_rel_question r
1827
                ON
1828
                    q.iid = r.question_id AND
1829
                    quiz_id = $quiz_id";
1830
        $rs_max = Database::query($sql);
1831
        $row_max = Database::fetch_object($rs_max);
1832
        $max_position = $row_max->max_position + 1;
1833
1834
        $params = [
1835
            'question' => $question_name,
1836
            'description' => $question_description,
1837
            'ponderation' => $max_score,
1838
            'position' => $max_position,
1839
            'type' => $type,
1840
            'level' => $level,
1841
            'mandatory' => 0,
1842
        ];
1843
        $question_id = Database::insert($tbl_quiz_question, $params);
1844
1845
        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...
1846
            // Get the max question_order
1847
            $sql = "SELECT max(question_order) as max_order
1848
                    FROM $tbl_quiz_rel_question
1849
                    WHERE quiz_id = $quiz_id ";
1850
            $rs_max_order = Database::query($sql);
1851
            $row_max_order = Database::fetch_object($rs_max_order);
1852
            $max_order = $row_max_order->max_order + 1;
1853
            // Attach questions to quiz
1854
            $sql = "INSERT INTO $tbl_quiz_rel_question (question_id, quiz_id, question_order)
1855
                    VALUES($question_id, $quiz_id, $max_order)";
1856
            Database::query($sql);
1857
        }
1858
1859
        return $question_id;
1860
    }
1861
1862
    /**
1863
     * @return string
1864
     */
1865
    public function getTypePicture()
1866
    {
1867
        return $this->typePicture;
1868
    }
1869
1870
    /**
1871
     * @return string
1872
     */
1873
    public function getExplanation()
1874
    {
1875
        return get_lang($this->explanationLangVar);
1876
    }
1877
1878
    /**
1879
     * Get course medias.
1880
     *
1881
     * @param int $course_id
1882
     *
1883
     * @return array
1884
     */
1885
    public static function get_course_medias(
1886
        $course_id,
1887
        $start = 0,
1888
        $limit = 100,
1889
        $sidx = 'question',
1890
        $sord = 'ASC',
1891
        $where_condition = []
1892
    ) {
1893
        $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
1894
        $default_where = [
1895
            'c_id = ? AND parent_id = 0 AND type = ?' => [
1896
                $course_id,
1897
                MEDIA_QUESTION,
1898
            ],
1899
        ];
1900
1901
        return Database::select(
1902
            '*',
1903
            $table_question,
1904
            [
1905
                'limit' => " $start, $limit",
1906
                'where' => $default_where,
1907
                'order' => "$sidx $sord",
1908
            ]
1909
        );
1910
    }
1911
1912
    /**
1913
     * Get count course medias.
1914
     *
1915
     * @param int $course_id course id
1916
     *
1917
     * @return int
1918
     */
1919
    public static function get_count_course_medias($course_id)
1920
    {
1921
        $table_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
1922
        $result = Database::select(
1923
            'count(*) as count',
1924
            $table_question,
1925
            [
1926
                'where' => [
1927
                    'c_id = ? AND parent_id = 0 AND type = ?' => [
1928
                        $course_id,
1929
                        MEDIA_QUESTION,
1930
                    ],
1931
                ],
1932
            ],
1933
            'first'
1934
        );
1935
1936
        if ($result && isset($result['count'])) {
1937
            return $result['count'];
1938
        }
1939
1940
        return 0;
1941
    }
1942
1943
    /**
1944
     * @param int $course_id
1945
     *
1946
     * @return array
1947
     */
1948
    public static function prepare_course_media_select(int $quizId): array
1949
    {
1950
        $tableQuestion     = Database::get_course_table(TABLE_QUIZ_QUESTION);
1951
        $tableRelQuestion  = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1952
1953
        $medias = Database::select(
1954
            '*',
1955
            "$tableQuestion q
1956
         JOIN $tableRelQuestion rq ON rq.question_id = q.iid",
1957
            [
1958
                'where' => [
1959
                    'rq.quiz_id = ? AND (q.parent_media_id IS NULL OR q.parent_media_id = 0) AND q.type = ?'
1960
                    => [$quizId, MEDIA_QUESTION],
1961
                ],
1962
                'order' => 'question ASC',
1963
            ]
1964
        );
1965
1966
        $mediaList = [
1967
            0 => get_lang('Not linked to media'),
1968
        ];
1969
1970
        foreach ($medias as $media) {
1971
            $mediaList[$media['question_id']] = empty($media['question'])
1972
                ? get_lang('Untitled')
1973
                : $media['question'];
1974
        }
1975
1976
        return $mediaList;
1977
    }
1978
1979
    /**
1980
     * @return array
1981
     */
1982
    public static function get_default_levels()
1983
    {
1984
        return [
1985
            1 => 1,
1986
            2 => 2,
1987
            3 => 3,
1988
            4 => 4,
1989
            5 => 5,
1990
        ];
1991
    }
1992
1993
    /**
1994
     * @return string
1995
     */
1996
    public function show_media_content()
1997
    {
1998
        $html = '';
1999
        if (0 != $this->parent_id) {
2000
            $parent_question = self::read($this->parent_id);
2001
            $html = $parent_question->show_media_content();
2002
        } else {
2003
            $html .= Display::page_subheader($this->selectTitle());
2004
            $html .= $this->selectDescription();
2005
        }
2006
2007
        return $html;
2008
    }
2009
2010
    /**
2011
     * Swap between unique and multiple type answers.
2012
     *
2013
     * @return UniqueAnswer|MultipleAnswer
2014
     */
2015
    public function swapSimpleAnswerTypes()
2016
    {
2017
        $oppositeAnswers = [
2018
            UNIQUE_ANSWER => MULTIPLE_ANSWER,
2019
            MULTIPLE_ANSWER => UNIQUE_ANSWER,
2020
        ];
2021
        $this->type = $oppositeAnswers[$this->type];
2022
        Database::update(
2023
            Database::get_course_table(TABLE_QUIZ_QUESTION),
2024
            ['type' => $this->type],
2025
            ['c_id = ? AND id = ?' => [$this->course['real_id'], $this->id]]
2026
        );
2027
        $answerClasses = [
2028
            UNIQUE_ANSWER => 'UniqueAnswer',
2029
            MULTIPLE_ANSWER => 'MultipleAnswer',
2030
            MULTIPLE_ANSWER_DROPDOWN => 'MultipleAnswerDropdown',
2031
            MULTIPLE_ANSWER_DROPDOWN_COMBINATION => 'MultipleAnswerDropdownCombination',
2032
        ];
2033
        $swappedAnswer = new $answerClasses[$this->type]();
2034
        foreach ($this as $key => $value) {
2035
            $swappedAnswer->$key = $value;
2036
        }
2037
2038
        return $swappedAnswer;
2039
    }
2040
2041
    /**
2042
     * @param array $score
2043
     *
2044
     * @return bool
2045
     */
2046
    public function isQuestionWaitingReview($score)
2047
    {
2048
        $isReview = false;
2049
        if (!empty($score)) {
2050
            if (!empty($score['comments']) || $score['score'] > 0) {
2051
                $isReview = true;
2052
            }
2053
        }
2054
2055
        return $isReview;
2056
    }
2057
2058
    /**
2059
     * @param string $value
2060
     */
2061
    public function setFeedback($value)
2062
    {
2063
        $this->feedback = $value;
2064
    }
2065
2066
    /**
2067
     * @param Exercise $exercise
2068
     *
2069
     * @return bool
2070
     */
2071
    public function showFeedback($exercise)
2072
    {
2073
        if (false === $exercise->hideComment) {
2074
            return false;
2075
        }
2076
2077
        return
2078
            in_array($this->type, $this->questionTypeWithFeedback) &&
2079
            EXERCISE_FEEDBACK_TYPE_EXAM != $exercise->getFeedbackType();
2080
    }
2081
2082
    /**
2083
     * @return string
2084
     */
2085
    public function returnFormatFeedback()
2086
    {
2087
        return '<br />'.Display::return_message($this->feedback, 'normal', false);
2088
    }
2089
2090
    /**
2091
     * Check if this question exists in another exercise.
2092
     *
2093
     * @throws \Doctrine\ORM\Query\QueryException
2094
     *
2095
     * @return bool
2096
     */
2097
    public function existsInAnotherExercise()
2098
    {
2099
        $count = $this->getCountExercise();
2100
2101
        return $count > 1;
2102
    }
2103
2104
    /**
2105
     * @throws \Doctrine\ORM\Query\QueryException
2106
     *
2107
     * @return int
2108
     */
2109
    public function getCountExercise()
2110
    {
2111
        $em = Database::getManager();
2112
2113
        $count = $em
2114
            ->createQuery('
2115
                SELECT COUNT(qq.iid) FROM ChamiloCourseBundle:CQuizRelQuestion qq
2116
                WHERE qq.question = :id
2117
            ')
2118
            ->setParameters(['id' => (int) $this->id])
2119
            ->getSingleScalarResult();
2120
2121
        return (int) $count;
2122
    }
2123
2124
    /**
2125
     * Check if this question exists in another exercise.
2126
     *
2127
     * @throws \Doctrine\ORM\Query\QueryException
2128
     */
2129
    public function getExerciseListWhereQuestionExists()
2130
    {
2131
        $em = Database::getManager();
2132
2133
        return $em
2134
            ->createQuery('
2135
                SELECT e
2136
                FROM ChamiloCourseBundle:CQuizRelQuestion qq
2137
                JOIN ChamiloCourseBundle:CQuiz e
2138
                WHERE e.iid = qq.exerciceId AND qq.questionId = :id
2139
            ')
2140
            ->setParameters(['id' => (int) $this->id])
2141
            ->getResult();
2142
    }
2143
2144
    /**
2145
     * @return int
2146
     */
2147
    public function countAnswers()
2148
    {
2149
        $result = Database::select(
2150
            'COUNT(1) AS c',
2151
            Database::get_course_table(TABLE_QUIZ_ANSWER),
2152
            ['where' => ['question_id = ?' => [$this->id]]],
2153
            'first'
2154
        );
2155
2156
        return (int) $result['c'];
2157
    }
2158
}
2159