Question::search_engine_edit()   F
last analyzed

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

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