Passed
Push — master ( 3b9d17...c5f69b )
by Julito
17:12
created

Question::updateQuestionOption()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 3
dl 0
loc 8
rs 10
c 0
b 0
f 0
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 $question_table_class = 'table table-striped';
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 => array('media_question.class.php' , 'MediaQuestion')
72
        ANNOTATION => ['Annotation.php', 'Annotation'],
73
        READING_COMPREHENSION => ['ReadingComprehension.php', 'ReadingComprehension'],
74
    ];
75
76
    /**
77
     * constructor of the class.
78
     *
79
     * @author Olivier Brouckaert
80
     */
81
    public function __construct()
82
    {
83
        $this->id = 0;
84
        $this->iid = 0;
85
        $this->question = '';
86
        $this->description = '';
87
        $this->weighting = 0;
88
        $this->position = 1;
89
        $this->picture = '';
90
        $this->level = 1;
91
        $this->category = 0;
92
        // This variable is used when loading an exercise like an scenario with
93
        // an special hotspot: final_overlap, final_missing, final_excess
94
        $this->extra = '';
95
        $this->exerciseList = [];
96
        $this->course = api_get_course_info();
97
        $this->category_list = [];
98
        $this->parent_id = 0;
99
        $this->mandatory = 0;
100
        // See BT#12611
101
        $this->questionTypeWithFeedback = [
102
            MATCHING,
103
            MATCHING_DRAGGABLE,
104
            DRAGGABLE,
105
            FILL_IN_BLANKS,
106
            FREE_ANSWER,
107
            ORAL_EXPRESSION,
108
            CALCULATED_ANSWER,
109
            ANNOTATION,
110
        ];
111
    }
112
113
    public function getId()
114
    {
115
        return $this->iid;
116
    }
117
118
    /**
119
     * @return int|null
120
     */
121
    public function getIsContent()
122
    {
123
        $isContent = null;
124
        if (isset($_REQUEST['isContent'])) {
125
            $isContent = (int) $_REQUEST['isContent'];
126
        }
127
128
        return $this->isContent = $isContent;
129
    }
130
131
    /**
132
     * Reads question information from the data base.
133
     *
134
     * @param int   $id              - question ID
135
     * @param array $course_info
136
     * @param bool  $getExerciseList
137
     *
138
     * @return Question
139
     *
140
     * @author Olivier Brouckaert
141
     */
142
    public static function read($id, $course_info = [], $getExerciseList = true)
143
    {
144
        $id = (int) $id;
145
        if (empty($course_info)) {
146
            $course_info = api_get_course_info();
147
        }
148
        $course_id = $course_info['real_id'];
149
150
        if (empty($course_id) || -1 == $course_id) {
151
            return false;
152
        }
153
154
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
155
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
156
157
        $sql = "SELECT *
158
                FROM $TBL_QUESTIONS
159
                WHERE iid = $id ";
160
        $result = Database::query($sql);
161
162
        // if the question has been found
163
        if ($object = Database::fetch_object($result)) {
164
            $objQuestion = self::getInstance($object->type);
165
            if (!empty($objQuestion)) {
166
                $objQuestion->id = (int) $id;
167
                $objQuestion->iid = (int) $object->iid;
168
                $objQuestion->question = $object->question;
169
                $objQuestion->description = $object->description;
170
                $objQuestion->weighting = $object->ponderation;
171
                $objQuestion->position = $object->position;
172
                $objQuestion->type = (int) $object->type;
173
                $objQuestion->picture = $object->picture;
174
                $objQuestion->level = (int) $object->level;
175
                $objQuestion->extra = $object->extra;
176
                $objQuestion->course = $course_info;
177
                $objQuestion->feedback = isset($object->feedback) ? $object->feedback : '';
178
                $objQuestion->category = TestCategory::getCategoryForQuestion($id, $course_id);
179
                $objQuestion->code = isset($object->code) ? $object->code : '';
180
                $categoryInfo = TestCategory::getCategoryInfoForQuestion($id, $course_id);
181
182
                if (!empty($categoryInfo)) {
183
                    if (isset($categoryInfo['category_id'])) {
184
                        $objQuestion->category = (int) $categoryInfo['category_id'];
185
                    }
186
187
                    if (api_get_configuration_value('allow_mandatory_question_in_category') &&
188
                        isset($categoryInfo['mandatory'])
189
                    ) {
190
                        $objQuestion->mandatory = (int) $categoryInfo['mandatory'];
191
                    }
192
                }
193
194
                if ($getExerciseList) {
195
                    $tblQuiz = Database::get_course_table(TABLE_QUIZ_TEST);
196
                    $sql = "SELECT DISTINCT q.quiz_id
197
                            FROM $TBL_EXERCISE_QUESTION q
198
                            INNER JOIN $tblQuiz e
199
                            ON e.iid = q.quiz_id
200
                            WHERE
201
                                q.question_id = $id AND
202
                                e.active >= 0";
203
204
                    $result = Database::query($sql);
205
206
                    // fills the array with the exercises which this question is in
207
                    if ($result) {
0 ignored issues
show
introduced by
$result is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
208
                        while ($obj = Database::fetch_object($result)) {
209
                            $objQuestion->exerciseList[] = $obj->quiz_id;
210
                        }
211
                    }
212
                }
213
214
                return $objQuestion;
215
            }
216
        }
217
218
        // question not found
219
        return false;
220
    }
221
222
    /**
223
     * returns the question title.
224
     *
225
     * @author Olivier Brouckaert
226
     *
227
     * @return string - question title
228
     */
229
    public function selectTitle()
230
    {
231
        if (!api_get_configuration_value('save_titles_as_html')) {
232
            return $this->question;
233
        }
234
235
        return Display::div($this->question, ['style' => 'display: inline-block;']);
236
    }
237
238
    /**
239
     * @param int $itemNumber
240
     *
241
     * @return string
242
     */
243
    public function getTitleToDisplay($itemNumber)
244
    {
245
        $showQuestionTitleHtml = api_get_configuration_value('save_titles_as_html');
246
        $title = '';
247
        if (api_get_configuration_value('show_question_id')) {
248
            $title .= '<h4>#'.$this->course['code'].'-'.$this->iid.'</h4>';
249
        }
250
251
        $title .= $showQuestionTitleHtml ? '' : '<strong>';
252
        $title .= $itemNumber.'. '.$this->selectTitle();
253
        $title .= $showQuestionTitleHtml ? '' : '</strong>';
254
255
        return Display::div(
256
            $title,
257
            ['class' => 'question_title']
258
        );
259
    }
260
261
    /**
262
     * returns the question description.
263
     *
264
     * @author Olivier Brouckaert
265
     *
266
     * @return string - question description
267
     */
268
    public function selectDescription()
269
    {
270
        return $this->description;
271
    }
272
273
    /**
274
     * returns the question weighting.
275
     *
276
     * @author Olivier Brouckaert
277
     *
278
     * @return int - question weighting
279
     */
280
    public function selectWeighting()
281
    {
282
        return $this->weighting;
283
    }
284
285
    /**
286
     * returns the answer type.
287
     *
288
     * @author Olivier Brouckaert
289
     *
290
     * @return int - answer type
291
     */
292
    public function selectType()
293
    {
294
        return $this->type;
295
    }
296
297
    /**
298
     * returns the level of the question.
299
     *
300
     * @author Nicolas Raynaud
301
     *
302
     * @return int - level of the question, 0 by default
303
     */
304
    public function getLevel()
305
    {
306
        return $this->level;
307
    }
308
309
    /**
310
     * changes the question title.
311
     *
312
     * @param string $title - question title
313
     *
314
     * @author Olivier Brouckaert
315
     */
316
    public function updateTitle($title)
317
    {
318
        $this->question = $title;
319
    }
320
321
    /**
322
     * changes the question description.
323
     *
324
     * @param string $description - question description
325
     *
326
     * @author Olivier Brouckaert
327
     */
328
    public function updateDescription($description)
329
    {
330
        $this->description = $description;
331
    }
332
333
    /**
334
     * changes the question weighting.
335
     *
336
     * @param int $weighting - question weighting
337
     *
338
     * @author Olivier Brouckaert
339
     */
340
    public function updateWeighting($weighting)
341
    {
342
        $this->weighting = $weighting;
343
    }
344
345
    /**
346
     * @param array $category
347
     *
348
     * @author Hubert Borderiou 12-10-2011
349
     */
350
    public function updateCategory($category)
351
    {
352
        $this->category = $category;
353
    }
354
355
    public function setMandatory($value)
356
    {
357
        $this->mandatory = (int) $value;
358
    }
359
360
    /**
361
     * in this version, a question can only have 1 category
362
     * if category is 0, then question has no category then delete the category entry.
363
     *
364
     * @param int $categoryId
365
     * @param int $courseId
366
     *
367
     * @return bool
368
     *
369
     * @author Hubert Borderiou 12-10-2011
370
     */
371
    public function saveCategory($categoryId, $courseId = 0)
372
    {
373
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
374
375
        if (empty($courseId)) {
376
            return false;
377
        }
378
379
        if ($categoryId <= 0) {
380
            $this->deleteCategory($courseId);
381
        } else {
382
            // update or add category for a question
383
            $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
384
            $categoryId = (int) $categoryId;
385
            $question_id = (int) $this->id;
386
            $sql = "SELECT count(*) AS nb FROM $table
387
                    WHERE
388
                        question_id = $question_id AND
389
                        c_id = ".$courseId;
390
            $res = Database::query($sql);
391
            $row = Database::fetch_array($res);
392
            $allowMandatory = api_get_configuration_value('allow_mandatory_question_in_category');
393
            if ($row['nb'] > 0) {
394
                $extraMandatoryCondition = '';
395
                if ($allowMandatory) {
396
                    $extraMandatoryCondition = ", mandatory = {$this->mandatory}";
397
                }
398
                $sql = "UPDATE $table
399
                        SET category_id = $categoryId
400
                        $extraMandatoryCondition
401
                        WHERE
402
                            question_id = $question_id ";
403
                Database::query($sql);
404
            } else {
405
                $sql = "INSERT INTO $table (question_id, category_id)
406
                        VALUES ($question_id, $categoryId)";
407
                Database::query($sql);
408
                if ($allowMandatory) {
409
                    $id = Database::insert_id();
410
                    if ($id) {
411
                        $sql = "UPDATE $table SET mandatory = {$this->mandatory}
412
                                WHERE iid = $id";
413
                        Database::query($sql);
414
                    }
415
                }
416
            }
417
418
            return true;
419
        }
420
    }
421
422
    /**
423
     * @author hubert borderiou 12-10-2011
424
     *
425
     * @param int $courseId
426
     *                      delete any category entry for question id
427
     *                      delete the category for question
428
     *
429
     * @deprecated
430
     *
431
     * @return bool
432
     */
433
    public function deleteCategory($courseId = 0)
434
    {
435
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
436
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
437
        $questionId = (int) $this->id;
438
        if (empty($courseId) || empty($questionId)) {
439
            return false;
440
        }
441
        $sql = "DELETE FROM $table
442
                WHERE
443
                    question_id = $questionId";
444
        Database::query($sql);
445
446
        return true;
447
    }
448
449
    /**
450
     * changes the question position.
451
     *
452
     * @param int $position - question position
453
     *
454
     * @author Olivier Brouckaert
455
     */
456
    public function updatePosition($position)
457
    {
458
        $this->position = $position;
459
    }
460
461
    /**
462
     * changes the question level.
463
     *
464
     * @param int $level - question level
465
     *
466
     * @author Nicolas Raynaud
467
     */
468
    public function updateLevel($level)
469
    {
470
        $this->level = $level;
471
    }
472
473
    /**
474
     * changes the answer type. If the user changes the type from "unique answer" to "multiple answers"
475
     * (or conversely) answers are not deleted, otherwise yes.
476
     *
477
     * @param int $type - answer type
478
     *
479
     * @author Olivier Brouckaert
480
     */
481
    public function updateType($type)
482
    {
483
        $table = Database::get_course_table(TABLE_QUIZ_ANSWER);
484
        $course_id = $this->course['real_id'];
485
486
        if (empty($course_id)) {
487
            $course_id = api_get_course_int_id();
488
        }
489
        // if we really change the type
490
        if ($type != $this->type) {
491
            // if we don't change from "unique answer" to "multiple answers" (or conversely)
492
            if (!in_array($this->type, [UNIQUE_ANSWER, MULTIPLE_ANSWER]) ||
493
                !in_array($type, [UNIQUE_ANSWER, MULTIPLE_ANSWER])
494
            ) {
495
                // removes old answers
496
                $sql = "DELETE FROM $table
497
                        WHERE c_id = $course_id AND question_id = ".(int) ($this->id);
498
                Database::query($sql);
499
            }
500
501
            $this->type = $type;
502
        }
503
    }
504
505
    /**
506
     * Set title.
507
     *
508
     * @param string $title
509
     */
510
    public function setTitle($title)
511
    {
512
        $this->question = $title;
513
    }
514
515
    /**
516
     * Sets extra info.
517
     *
518
     * @param string $extra
519
     */
520
    public function setExtra($extra)
521
    {
522
        $this->extra = $extra;
523
    }
524
525
    /**
526
     * updates the question in the data base
527
     * if an exercise ID is provided, we add that exercise ID into the exercise list.
528
     *
529
     * @author Olivier Brouckaert
530
     *
531
     * @param Exercise $exercise
532
     */
533
    public function save($exercise)
534
    {
535
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
536
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
537
        $em = Database::getManager();
538
        $exerciseId = $exercise->iId;
539
540
        $id = $this->id;
541
        $type = $this->type;
542
        $c_id = $this->course['real_id'];
543
544
        $courseEntity = api_get_course_entity($c_id);
545
        $categoryId = $this->category;
546
547
        $questionCategoryRepo = Container::getQuestionCategoryRepository();
548
        $questionRepo = Container::getQuestionRepository();
549
550
        // question already exists
551
        if (!empty($id)) {
552
            /** @var CQuizQuestion $question */
553
            $question = $questionRepo->find($id);
554
            if ($question) {
0 ignored issues
show
introduced by
$question is of type Chamilo\CourseBundle\Entity\CQuizQuestion, thus it always evaluated to true.
Loading history...
555
                $question
556
                    ->setQuestion($this->question)
557
                    ->setDescription($this->description)
558
                    ->setPonderation($this->weighting)
559
                    ->setPosition($this->position)
560
                    ->setType($this->type)
561
                    ->setExtra($this->extra)
562
                    ->setLevel((int) $this->level)
563
                    ->setFeedback($this->feedback);
564
565
                if (!empty($categoryId)) {
566
                    $category = $questionCategoryRepo->find($categoryId);
567
                    $question->updateCategory($category);
568
                }
569
570
                $em->persist($question);
571
                $em->flush();
572
573
                Event::addEvent(
574
                    LOG_QUESTION_UPDATED,
575
                    LOG_QUESTION_ID,
576
                    $this->iid
577
                );
578
                if ('true' === api_get_setting('search_enabled')) {
579
                    $this->search_engine_edit($exerciseId);
580
                }
581
            }
582
        } else {
583
            // Creates a new question
584
            $sql = "SELECT max(position)
585
                    FROM $TBL_QUESTIONS as question,
586
                    $TBL_EXERCISE_QUESTION as test_question
587
                    WHERE
588
                        question.iid = test_question.question_id AND
589
                        test_question.quiz_id = ".$exerciseId;
590
            $result = Database::query($sql);
591
            $current_position = Database::result($result, 0, 0);
592
            $this->updatePosition($current_position + 1);
593
            $position = $this->position;
594
            //$exerciseEntity = $exerciseRepo->find($exerciseId);
595
596
            $question = new CQuizQuestion();
597
            $question
598
                ->setQuestion($this->question)
599
                ->setDescription($this->description)
600
                ->setPonderation($this->weighting)
601
                ->setPosition($position)
602
                ->setType($this->type)
603
                ->setExtra($this->extra)
604
                ->setLevel((int) $this->level)
605
                ->setFeedback($this->feedback)
606
                ->setParent($courseEntity)
607
                ->addCourseLink(
608
                    $courseEntity,
609
                    api_get_session_entity(),
610
                    api_get_group_entity()
611
                )
612
            ;
613
614
            $em->persist($question);
615
            $em->flush();
616
617
            $this->id = $question->getIid();
618
619
            if ($this->id) {
620
                Event::addEvent(
621
                    LOG_QUESTION_CREATED,
622
                    LOG_QUESTION_ID,
623
                    $this->id
624
                );
625
                $request = Container::getRequest();
626
                if ($request->files->has('imageUpload')) {
627
                    $file = $request->files->get('imageUpload');
628
                    $questionRepo->addFile($question, $file);
629
630
                    $em->flush();
631
                }
632
633
                // If hotspot, create first answer
634
                if (HOT_SPOT == $type || HOT_SPOT_ORDER == $type) {
635
                    $quizAnswer = new CQuizAnswer();
636
                    $quizAnswer
637
                        ->setQuestion($question)
638
                        ->setPonderation(10)
639
                        ->setPosition(1)
640
                        ->setHotspotCoordinates('0;0|0|0')
641
                        ->setHotspotType('square');
642
643
                    $em->persist($quizAnswer);
644
                    $em->flush();
645
                }
646
647
                if (HOT_SPOT_DELINEATION == $type) {
648
                    $quizAnswer = new CQuizAnswer();
649
                    $quizAnswer
650
                        ->setQuestion($question)
651
                        ->setPonderation(10)
652
                        ->setPosition(1)
653
                        ->setHotspotCoordinates('0;0|0|0')
654
                        ->setHotspotType('delineation');
655
656
                    $em->persist($quizAnswer);
657
                    $em->flush();
658
                }
659
660
                if ('true' === api_get_setting('search_enabled')) {
661
                    $this->search_engine_edit($exerciseId, true);
662
                }
663
            }
664
        }
665
666
        // if the question is created in an exercise
667
        if (!empty($exerciseId)) {
668
            // adds the exercise into the exercise list of this question
669
            $this->addToList($exerciseId, true);
670
        }
671
    }
672
673
    /**
674
     * @param int  $exerciseId
675
     * @param bool $addQs
676
     * @param bool $rmQs
677
     */
678
    public function search_engine_edit(
679
        $exerciseId,
680
        $addQs = false,
681
        $rmQs = false
682
    ) {
683
        // update search engine and its values table if enabled
684
        if (!empty($exerciseId) && 'true' == api_get_setting('search_enabled') &&
685
            extension_loaded('xapian')
686
        ) {
687
            $course_id = api_get_course_id();
688
            // get search_did
689
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
690
            if ($addQs || $rmQs) {
691
                //there's only one row per question on normal db and one document per question on search engine db
692
                $sql = 'SELECT * FROM %s
693
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_second_level=%s LIMIT 1';
694
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
695
            } else {
696
                $sql = 'SELECT * FROM %s
697
                    WHERE course_code=\'%s\' AND tool_id=\'%s\'
698
                    AND ref_id_high_level=%s AND ref_id_second_level=%s LIMIT 1';
699
                $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
700
            }
701
            $res = Database::query($sql);
702
703
            if (Database::num_rows($res) > 0 || $addQs) {
704
                $di = new ChamiloIndexer();
705
                if ($addQs) {
706
                    $question_exercises = [(int) $exerciseId];
707
                } else {
708
                    $question_exercises = [];
709
                }
710
                isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
711
                $di->connectDb(null, null, $lang);
712
713
                // retrieve others exercise ids
714
                $se_ref = Database::fetch_array($res);
715
                $se_doc = $di->get_document((int) $se_ref['search_did']);
716
                if (false !== $se_doc) {
717
                    if (false !== ($se_doc_data = $di->get_document_data($se_doc))) {
718
                        $se_doc_data = UnserializeApi::unserialize(
719
                            'not_allowed_classes',
720
                            $se_doc_data
721
                        );
722
                        if (isset($se_doc_data[SE_DATA]['type']) &&
723
                            SE_DOCTYPE_EXERCISE_QUESTION == $se_doc_data[SE_DATA]['type']
724
                        ) {
725
                            if (isset($se_doc_data[SE_DATA]['exercise_ids']) &&
726
                                is_array($se_doc_data[SE_DATA]['exercise_ids'])
727
                            ) {
728
                                foreach ($se_doc_data[SE_DATA]['exercise_ids'] as $old_value) {
729
                                    if (!in_array($old_value, $question_exercises)) {
730
                                        $question_exercises[] = $old_value;
731
                                    }
732
                                }
733
                            }
734
                        }
735
                    }
736
                }
737
                if ($rmQs) {
738
                    while (false !== ($key = array_search($exerciseId, $question_exercises))) {
739
                        unset($question_exercises[$key]);
740
                    }
741
                }
742
743
                // build the chunk to index
744
                $ic_slide = new IndexableChunk();
745
                $ic_slide->addValue('title', $this->question);
746
                $ic_slide->addCourseId($course_id);
747
                $ic_slide->addToolId(TOOL_QUIZ);
748
                $xapian_data = [
749
                    SE_COURSE_ID => $course_id,
750
                    SE_TOOL_ID => TOOL_QUIZ,
751
                    SE_DATA => [
752
                        'type' => SE_DOCTYPE_EXERCISE_QUESTION,
753
                        'exercise_ids' => $question_exercises,
754
                        'question_id' => (int) $this->id,
755
                    ],
756
                    SE_USER => (int) api_get_user_id(),
757
                ];
758
                $ic_slide->xapian_data = serialize($xapian_data);
759
                $ic_slide->addValue('content', $this->description);
760
761
                //TODO: index answers, see also form validation on question_admin.inc.php
762
763
                $di->remove_document($se_ref['search_did']);
764
                $di->addChunk($ic_slide);
765
766
                //index and return search engine document id
767
                if (!empty($question_exercises)) { // if empty there is nothing to index
768
                    $did = $di->index();
769
                    unset($di);
770
                }
771
                if ($did || $rmQs) {
772
                    // save it to db
773
                    if ($addQs || $rmQs) {
774
                        $sql = "DELETE FROM %s
775
                            WHERE course_code = '%s' AND tool_id = '%s' AND ref_id_second_level = '%s'";
776
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $this->id);
777
                    } else {
778
                        $sql = "DELETE FROM %S
779
                            WHERE
780
                                course_code = '%s'
781
                                AND tool_id = '%s'
782
                                AND tool_id = '%s'
783
                                AND ref_id_high_level = '%s'
784
                                AND ref_id_second_level = '%s'";
785
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id);
786
                    }
787
                    Database::query($sql);
788
                    if ($rmQs) {
789
                        if (!empty($question_exercises)) {
790
                            $sql = "INSERT INTO %s (
791
                                    id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
792
                                )
793
                                VALUES (
794
                                    NULL, '%s', '%s', %s, %s, %s
795
                                )";
796
                            $sql = sprintf(
797
                                $sql,
798
                                $tbl_se_ref,
799
                                $course_id,
800
                                TOOL_QUIZ,
801
                                array_shift($question_exercises),
802
                                $this->id,
803
                                $did
804
                            );
805
                            Database::query($sql);
806
                        }
807
                    } else {
808
                        $sql = "INSERT INTO %s (
809
                                id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did
810
                            )
811
                            VALUES (
812
                                NULL , '%s', '%s', %s, %s, %s
813
                            )";
814
                        $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_QUIZ, $exerciseId, $this->id, $did);
815
                        Database::query($sql);
816
                    }
817
                }
818
            }
819
        }
820
    }
821
822
    /**
823
     * adds an exercise into the exercise list.
824
     *
825
     * @author Olivier Brouckaert
826
     *
827
     * @param int  $exerciseId - exercise ID
828
     * @param bool $fromSave   - from $this->save() or not
829
     */
830
    public function addToList($exerciseId, $fromSave = false)
831
    {
832
        $exerciseRelQuestionTable = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
833
        $id = (int) $this->id;
834
        $exerciseId = (int) $exerciseId;
835
836
        // checks if the exercise ID is not in the list
837
        if (!empty($exerciseId) && !in_array($exerciseId, $this->exerciseList)) {
838
            $this->exerciseList[] = $exerciseId;
839
            $courseId = isset($this->course['real_id']) ? $this->course['real_id'] : 0;
840
            $newExercise = new Exercise($courseId);
841
            $newExercise->read($exerciseId, false);
842
            $count = $newExercise->getQuestionCount();
843
            $count++;
844
            $sql = "INSERT INTO $exerciseRelQuestionTable (question_id, quiz_id, question_order)
845
                    VALUES (".$id.', '.$exerciseId.", '$count')";
846
            Database::query($sql);
847
848
            // we do not want to reindex if we had just saved adnd indexed the question
849
            if (!$fromSave) {
850
                $this->search_engine_edit($exerciseId, true);
851
            }
852
        }
853
    }
854
855
    /**
856
     * removes an exercise from the exercise list.
857
     *
858
     * @author Olivier Brouckaert
859
     *
860
     * @param int $exerciseId - exercise ID
861
     * @param int $courseId
862
     *
863
     * @return bool - true if removed, otherwise false
864
     */
865
    public function removeFromList($exerciseId, $courseId = 0)
866
    {
867
        $table = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
868
        $id = (int) $this->id;
869
        $exerciseId = (int) $exerciseId;
870
871
        // searches the position of the exercise ID in the list
872
        $pos = array_search($exerciseId, $this->exerciseList);
873
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
874
875
        // exercise not found
876
        if (false === $pos) {
877
            return false;
878
        } else {
879
            // deletes the position in the array containing the wanted exercise ID
880
            unset($this->exerciseList[$pos]);
881
            //update order of other elements
882
            $sql = "SELECT question_order
883
                    FROM $table
884
                    WHERE
885
                        question_id = $id AND
886
                        quiz_id = $exerciseId";
887
            $res = Database::query($sql);
888
            if (Database::num_rows($res) > 0) {
889
                $row = Database::fetch_array($res);
890
                if (!empty($row['question_order'])) {
891
                    $sql = "UPDATE $table
892
                            SET question_order = question_order-1
893
                            WHERE
894
                                quiz_id = $exerciseId AND
895
                                question_order > ".$row['question_order'];
896
                    Database::query($sql);
897
                }
898
            }
899
900
            $sql = "DELETE FROM $table
901
                    WHERE
902
                        question_id = $id AND
903
                        quiz_id = $exerciseId";
904
            Database::query($sql);
905
906
            return true;
907
        }
908
    }
909
910
    /**
911
     * Deletes a question from the database
912
     * the parameter tells if the question is removed from all exercises (value = 0),
913
     * or just from one exercise (value = exercise ID).
914
     *
915
     * @author Olivier Brouckaert
916
     *
917
     * @param int $deleteFromEx - exercise ID if the question is only removed from one exercise
918
     *
919
     * @return bool
920
     */
921
    public function delete($deleteFromEx = 0)
922
    {
923
        if (empty($this->course)) {
924
            return false;
925
        }
926
927
        $courseId = $this->course['real_id'];
928
929
        if (empty($courseId)) {
930
            return false;
931
        }
932
933
        $TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
934
        $TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
935
        $TBL_REPONSES = Database::get_course_table(TABLE_QUIZ_ANSWER);
936
        $TBL_QUIZ_QUESTION_REL_CATEGORY = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
937
938
        $id = (int) $this->id;
939
940
        // if the question must be removed from all exercises
941
        if (!$deleteFromEx) {
942
            //update the question_order of each question to avoid inconsistencies
943
            $sql = "SELECT quiz_id, question_order
944
                    FROM $TBL_EXERCISE_QUESTION
945
                    WHERE question_id = ".$id;
946
947
            $res = Database::query($sql);
948
            if (Database::num_rows($res) > 0) {
949
                while ($row = Database::fetch_array($res)) {
950
                    if (!empty($row['question_order'])) {
951
                        $sql = "UPDATE $TBL_EXERCISE_QUESTION
952
                                SET question_order = question_order-1
953
                                WHERE
954
                                    quiz_id = ".(int) ($row['quiz_id']).' AND
955
                                    question_order > '.$row['question_order'];
956
                        Database::query($sql);
957
                    }
958
                }
959
            }
960
961
            $sql = "DELETE FROM $TBL_EXERCISE_QUESTION
962
                    WHERE question_id = ".$id;
963
            Database::query($sql);
964
965
            $sql = "DELETE FROM $TBL_QUESTIONS
966
                    WHERE iid = ".$id;
967
            Database::query($sql);
968
969
            $sql = "DELETE FROM $TBL_REPONSES
970
                    WHERE question_id = ".$id;
971
            Database::query($sql);
972
973
            // remove the category of this question in the question_rel_category table
974
            $sql = "DELETE FROM $TBL_QUIZ_QUESTION_REL_CATEGORY
975
                    WHERE
976
                        question_id = ".$id;
977
            Database::query($sql);
978
979
            // Add extra fields.
980
            $extraField = new ExtraFieldValue('question');
981
            $extraField->deleteValuesByItem($this->iid);
982
983
            /*api_item_property_update(
984
                $this->course,
985
                TOOL_QUIZ,
986
                $id,
987
                'QuizQuestionDeleted',
988
                api_get_user_id()
989
            );*/
990
            Event::addEvent(
991
                LOG_QUESTION_DELETED,
992
                LOG_QUESTION_ID,
993
                $this->iid
994
            );
995
        //$this->removePicture();
996
        } else {
997
            // just removes the exercise from the list
998
            $this->removeFromList($deleteFromEx, $courseId);
999
            if ('true' == api_get_setting('search_enabled') && extension_loaded('xapian')) {
1000
                // disassociate question with this exercise
1001
                $this->search_engine_edit($deleteFromEx, false, true);
1002
            }
1003
            /*
1004
            api_item_property_update(
1005
                $this->course,
1006
                TOOL_QUIZ,
1007
                $id,
1008
                'QuizQuestionDeleted',
1009
                api_get_user_id()
1010
            );*/
1011
            Event::addEvent(
1012
                LOG_QUESTION_REMOVED_FROM_QUIZ,
1013
                LOG_QUESTION_ID,
1014
                $this->iid
1015
            );
1016
        }
1017
1018
        return true;
1019
    }
1020
1021
    /**
1022
     * Duplicates the question.
1023
     *
1024
     * @author Olivier Brouckaert
1025
     *
1026
     * @param array $courseInfo Course info of the destination course
1027
     *
1028
     * @return false|string ID of the new question
1029
     */
1030
    public function duplicate($courseInfo = [])
1031
    {
1032
        $courseInfo = empty($courseInfo) ? $this->course : $courseInfo;
1033
1034
        if (empty($courseInfo)) {
1035
            return false;
1036
        }
1037
        $TBL_QUESTION_OPTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1038
1039
        $questionText = $this->question;
1040
        $description = $this->description;
1041
1042
        // Using the same method used in the course copy to transform URLs
1043
        if ($this->course['id'] != $courseInfo['id']) {
1044
            $description = DocumentManager::replaceUrlWithNewCourseCode(
1045
                $description,
1046
                $this->course['code'],
1047
                $courseInfo['id']
1048
            );
1049
            $questionText = DocumentManager::replaceUrlWithNewCourseCode(
1050
                $questionText,
1051
                $this->course['code'],
1052
                $courseInfo['id']
1053
            );
1054
        }
1055
1056
        $course_id = $courseInfo['real_id'];
1057
1058
        // Read the source options
1059
        $options = self::readQuestionOption($this->id, $this->course['real_id']);
1060
1061
        $em = Database::getManager();
1062
        $courseEntity = api_get_course_entity($course_id);
1063
1064
        $question = new CQuizQuestion();
1065
        $question
1066
            ->setCId($course_id)
1067
            ->setQuestion($questionText)
1068
            ->setDescription($description)
1069
            ->setPonderation($this->weighting)
1070
            ->setPosition($this->position)
1071
            ->setType($this->type)
1072
            ->setExtra($this->extra)
1073
            ->setLevel($this->level)
1074
            ->setFeedback($this->feedback)
1075
            ->setParent($courseEntity)
1076
            ->addCourseLink(
1077
                $courseEntity
1078
            )
1079
        ;
1080
1081
        $em->persist($question);
1082
        $em->flush();
1083
        $newQuestionId = $question->getIid();
1084
1085
        if ($newQuestionId) {
1086
            // Add extra fields.
1087
            $extraField = new ExtraFieldValue('question');
1088
            $extraField->copy($this->iid, $newQuestionId);
1089
1090
            if (!empty($options)) {
1091
                // Saving the quiz_options
1092
                foreach ($options as $item) {
1093
                    $item['question_id'] = $newQuestionId;
1094
                    $item['c_id'] = $course_id;
1095
                    unset($item['iid']);
1096
                    unset($item['iid']);
1097
                    Database::insert($TBL_QUESTION_OPTIONS, $item);
1098
                }
1099
            }
1100
1101
            // Duplicates the picture of the hotspot
1102
            // @todo implement copy of hotspot question
1103
            if (HOT_SPOT == $this->type) {
1104
                throw new Exception('implement copy of hotspot question');
1105
            }
1106
        }
1107
1108
        return $newQuestionId;
1109
    }
1110
1111
    /**
1112
     * @return string
1113
     */
1114
    public function get_question_type_name()
1115
    {
1116
        $key = self::$questionTypes[$this->type];
1117
1118
        return get_lang($key[1]);
1119
    }
1120
1121
    /**
1122
     * @param string $type
1123
     */
1124
    public static function get_question_type($type)
1125
    {
1126
        if (ORAL_EXPRESSION == $type && 'true' !== api_get_setting('enable_record_audio')) {
1127
            return null;
1128
        }
1129
1130
        return self::$questionTypes[$type];
1131
    }
1132
1133
    /**
1134
     * @return array
1135
     */
1136
    public static function getQuestionTypeList()
1137
    {
1138
        if ('true' !== api_get_setting('enable_record_audio')) {
1139
            self::$questionTypes[ORAL_EXPRESSION] = null;
1140
            unset(self::$questionTypes[ORAL_EXPRESSION]);
1141
        }
1142
        if ('true' !== api_get_setting('enable_quiz_scenario')) {
1143
            self::$questionTypes[HOT_SPOT_DELINEATION] = null;
1144
            unset(self::$questionTypes[HOT_SPOT_DELINEATION]);
1145
        }
1146
1147
        return self::$questionTypes;
1148
    }
1149
1150
    /**
1151
     * Returns an instance of the class corresponding to the type.
1152
     *
1153
     * @param int $type the type of the question
1154
     *
1155
     * @return $this instance of a Question subclass (or of Questionc class by default)
1156
     */
1157
    public static function getInstance($type)
1158
    {
1159
        if (null !== $type) {
1160
            [$fileName, $className] = self::get_question_type($type);
1161
            if (!empty($fileName)) {
1162
                if (class_exists($className)) {
1163
                    return new $className();
1164
                } else {
1165
                    echo 'Can\'t instanciate class '.$className.' of type '.$type;
1166
                }
1167
            }
1168
        }
1169
1170
        return null;
1171
    }
1172
1173
    /**
1174
     * Creates the form to create / edit a question
1175
     * A subclass can redefine this function to add fields...
1176
     *
1177
     * @param FormValidator $form
1178
     * @param Exercise      $exercise
1179
     */
1180
    public function createForm(&$form, $exercise)
1181
    {
1182
        echo '<style>
1183
                .media { display:none;}
1184
            </style>';
1185
1186
        $zoomOptions = api_get_configuration_value('quiz_image_zoom');
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 type="text/javascript" src="'.$finderFolder.'js/elfinder.full.js"></script>';
1195
1196
            echo '<!-- elFinder translation (OPTIONAL) -->';
1197
            $language = 'en';
1198
            $platformLanguage = api_get_interface_language();
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 type="text/javascript" src="'.api_get_path(WEB_PATH).$filePart.'"></script>';
1205
                $language = $iso;
1206
            }
1207
            echo $includeFile;
1208
1209
            echo '<script type="text/javascript" charset="utf-8">
1210
            $(function() {
1211
                $(".create_img_link").click(function(e){
1212
                    e.preventDefault();
1213
                    e.stopPropagation();
1214
                    var imageZoom = $("input[name=\'imageZoom\']").val();
1215
                    var imageWidth = $("input[name=\'imageWidth\']").val();
1216
                    CKEDITOR.instances.questionDescription.insertHtml(\'<img id="zoom_picture" class="zoom_picture" src="\'+imageZoom+\'" data-zoom-image="\'+imageZoom+\'" width="\'+imageWidth+\'px" />\');
1217
                });
1218
1219
                $("input[name=\'imageZoom\']").on("click", function(){
1220
                    var elf = $("#elfinder").elfinder({
1221
                        url : "'.api_get_path(WEB_LIBRARY_PATH).'elfinder/connectorAction.php?'.api_get_cidreq().'",
1222
                        getFileCallback: function(file) {
1223
                            var filePath = file; //file contains the relative url.
1224
                            var imgPath = "<img src = \'"+filePath+"\'/>";
1225
                            $("input[name=\'imageZoom\']").val(filePath.url);
1226
                            $("#elfinder").remove(); //close the window after image is selected
1227
                        },
1228
                        startPathHash: "l2_Lw", // Sets the course driver as default
1229
                        resizable: false,
1230
                        lang: "'.$language.'"
1231
                    }).elfinder("instance");
1232
                });
1233
            });
1234
            </script>';
1235
            echo '<div id="elfinder"></div>';
1236
        }
1237
1238
        // question name
1239
        if (api_get_configuration_value('save_titles_as_html')) {
1240
            $editorConfig = ['ToolbarSet' => 'TitleAsHtml'];
1241
            $form->addHtmlEditor(
1242
                'questionName',
1243
                get_lang('Question'),
1244
                false,
1245
                false,
1246
                $editorConfig,
1247
                true
1248
            );
1249
        } else {
1250
            $form->addElement('text', 'questionName', get_lang('Question'));
1251
        }
1252
1253
        $form->addRule('questionName', get_lang('Please type the question'), 'required');
1254
1255
        // default content
1256
        $isContent = isset($_REQUEST['isContent']) ? (int) $_REQUEST['isContent'] : null;
1257
1258
        // Question type
1259
        $answerType = isset($_REQUEST['answerType']) ? (int) $_REQUEST['answerType'] : null;
1260
        $form->addElement('hidden', 'answerType', $answerType);
1261
1262
        // html editor
1263
        $editorConfig = [
1264
            'ToolbarSet' => 'TestQuestionDescription',
1265
            'Height' => '150',
1266
        ];
1267
1268
        if (!api_is_allowed_to_edit(null, true)) {
1269
            $editorConfig['UserStatus'] = 'student';
1270
        }
1271
1272
        $form->addButtonAdvancedSettings('advanced_params');
1273
        $form->addHtml('<div id="advanced_params_options" style="display:none">');
1274
1275
        if (isset($zoomOptions['options'])) {
1276
            $form->addElement('text', 'imageZoom', get_lang('ImageURL'));
1277
            $form->addElement('text', 'imageWidth', get_lang('PixelWidth'));
1278
1279
            $form->addButton('btn_create_img', get_lang('AddToEditor'), 'plus', 'info', 'small', 'create_img_link');
1280
        }
1281
1282
        $form->addHtmlEditor(
1283
            'questionDescription',
1284
            get_lang('Enrich question'),
1285
            false,
1286
            false,
1287
            $editorConfig
1288
        );
1289
1290
        if (MEDIA_QUESTION != $this->type) {
1291
            // Advanced parameters
1292
            $form->addElement(
1293
                'select',
1294
                'questionLevel',
1295
                get_lang('Difficulty'),
1296
                self::get_default_levels()
1297
            );
1298
1299
            // Categories
1300
1301
            $form->addElement(
1302
                'select',
1303
                'questionCategory',
1304
                get_lang('Category'),
1305
                TestCategory::getCategoriesIdAndName()
1306
            );
1307
            if (EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM == $exercise->getQuestionSelectionType() &&
1308
                api_get_configuration_value('allow_mandatory_question_in_category')
1309
            ) {
1310
                $form->addCheckBox(
1311
                    'mandatory',
1312
                    get_lang('IsMandatory')
1313
                );
1314
            }
1315
1316
            global $text;
1317
1318
            switch ($this->type) {
1319
                case UNIQUE_ANSWER:
1320
                    $buttonGroup = [];
1321
                    $buttonGroup[] = $form->addButtonSave(
1322
                        $text,
1323
                        'submitQuestion',
1324
                        true
1325
                    );
1326
                    $buttonGroup[] = $form->addButton(
1327
                        'convertAnswer',
1328
                        get_lang('Convert to multiple answer'),
1329
                        'dot-circle-o',
1330
                        'default',
1331
                        null,
1332
                        null,
1333
                        null,
1334
                        true
1335
                    );
1336
                    $form->addGroup($buttonGroup);
1337
1338
                    break;
1339
                case MULTIPLE_ANSWER:
1340
                    $buttonGroup = [];
1341
                    $buttonGroup[] = $form->addButtonSave(
1342
                        $text,
1343
                        'submitQuestion',
1344
                        true
1345
                    );
1346
                    $buttonGroup[] = $form->addButton(
1347
                        'convertAnswer',
1348
                        get_lang('Convert to unique answer'),
1349
                        'check-square-o',
1350
                        'default',
1351
                        null,
1352
                        null,
1353
                        null,
1354
                        true
1355
                    );
1356
                    $form->addGroup($buttonGroup);
1357
1358
                    break;
1359
            }
1360
            //Medias
1361
            //$course_medias = self::prepare_course_media_select(api_get_course_int_id());
1362
            //$form->addElement('select', 'parent_id', get_lang('Attach to media'), $course_medias);
1363
        }
1364
1365
        $form->addElement('html', '</div>');
1366
1367
        if (!isset($_GET['fromExercise'])) {
1368
            switch ($answerType) {
1369
                case 1:
1370
                    $this->question = get_lang('Select the good reasoning');
1371
1372
                    break;
1373
                case 2:
1374
                    $this->question = get_lang('The marasmus is a consequence of');
1375
1376
                    break;
1377
                case 3:
1378
                    $this->question = get_lang('Calculate the Body Mass Index');
1379
1380
                    break;
1381
                case 4:
1382
                    $this->question = get_lang('Order the operations');
1383
1384
                    break;
1385
                case 5:
1386
                    $this->question = get_lang('List what you consider the 10 top qualities of a good project manager?');
1387
1388
                    break;
1389
                case 9:
1390
                    $this->question = get_lang('The marasmus is a consequence of');
1391
1392
                    break;
1393
            }
1394
        }
1395
1396
        if (null !== $exercise) {
1397
            if ($exercise->questionFeedbackEnabled && $this->showFeedback($exercise)) {
1398
                $form->addTextarea('feedback', get_lang('Feedback if not correct'));
1399
            }
1400
        }
1401
1402
        $extraField = new ExtraField('question');
1403
        $extraField->addElements($form, $this->iid);
1404
1405
        // default values
1406
        $defaults = [];
1407
        $defaults['questionName'] = $this->question;
1408
        $defaults['questionDescription'] = $this->description;
1409
        $defaults['questionLevel'] = $this->level;
1410
        $defaults['questionCategory'] = $this->category;
1411
        $defaults['feedback'] = $this->feedback;
1412
        $defaults['mandatory'] = $this->mandatory;
1413
1414
        // Came from he question pool
1415
        if (isset($_GET['fromExercise'])) {
1416
            $form->setDefaults($defaults);
1417
        }
1418
1419
        if (!isset($_GET['newQuestion']) || $isContent) {
1420
            $form->setDefaults($defaults);
1421
        }
1422
1423
        /*if (!empty($_REQUEST['myid'])) {
1424
            $form->setDefaults($defaults);
1425
        } else {
1426
            if ($isContent == 1) {
1427
                $form->setDefaults($defaults);
1428
            }
1429
        }*/
1430
    }
1431
1432
    /**
1433
     * Function which process the creation of questions.
1434
     */
1435
    public function processCreation(FormValidator $form, Exercise $exercise)
1436
    {
1437
        $this->updateTitle($form->getSubmitValue('questionName'));
1438
        $this->updateDescription($form->getSubmitValue('questionDescription'));
1439
        $this->updateLevel($form->getSubmitValue('questionLevel'));
1440
        $this->updateCategory($form->getSubmitValue('questionCategory'));
1441
        $this->setMandatory($form->getSubmitValue('mandatory'));
1442
        $this->setFeedback($form->getSubmitValue('feedback'));
1443
1444
        //Save normal question if NOT media
1445
        if (MEDIA_QUESTION != $this->type) {
1446
            $this->save($exercise);
1447
            // modify the exercise
1448
            $exercise->addToList($this->id);
1449
            $exercise->update_question_positions();
1450
1451
            $params = $form->exportValues();
1452
            $params['item_id'] = $this->id;
1453
1454
            $extraFieldValues = new ExtraFieldValue('question');
1455
            $extraFieldValues->saveFieldValues($params);
1456
        }
1457
    }
1458
1459
    /**
1460
     * abstract function which creates the form to create / edit the answers of the question.
1461
     */
1462
    abstract public function createAnswersForm(FormValidator $form);
1463
1464
    /**
1465
     * abstract function which process the creation of answers.
1466
     *
1467
     * @param FormValidator $form
1468
     * @param Exercise      $exercise
1469
     */
1470
    abstract public function processAnswersCreation($form, $exercise);
1471
1472
    /**
1473
     * Displays the menu of question types.
1474
     *
1475
     * @param Exercise $objExercise
1476
     */
1477
    public static function displayTypeMenu($objExercise)
1478
    {
1479
        if (empty($objExercise)) {
1480
            return '';
1481
        }
1482
1483
        $feedbackType = $objExercise->getFeedbackType();
1484
        $exerciseId = $objExercise->id;
1485
1486
        // 1. by default we show all the question types
1487
        $questionTypeList = self::getQuestionTypeList();
1488
1489
        if (!isset($feedbackType)) {
1490
            $feedbackType = 0;
1491
        }
1492
1493
        switch ($feedbackType) {
1494
            case EXERCISE_FEEDBACK_TYPE_DIRECT:
1495
                $questionTypeList = [
1496
                    UNIQUE_ANSWER => self::$questionTypes[UNIQUE_ANSWER],
1497
                    HOT_SPOT_DELINEATION => self::$questionTypes[HOT_SPOT_DELINEATION],
1498
                ];
1499
1500
                break;
1501
            case EXERCISE_FEEDBACK_TYPE_POPUP:
1502
                $questionTypeList = [
1503
                    UNIQUE_ANSWER => self::$questionTypes[UNIQUE_ANSWER],
1504
                    MULTIPLE_ANSWER => self::$questionTypes[MULTIPLE_ANSWER],
1505
                    DRAGGABLE => self::$questionTypes[DRAGGABLE],
1506
                    HOT_SPOT_DELINEATION => self::$questionTypes[HOT_SPOT_DELINEATION],
1507
                    CALCULATED_ANSWER => self::$questionTypes[CALCULATED_ANSWER],
1508
                ];
1509
1510
                break;
1511
            default:
1512
                unset($questionTypeList[HOT_SPOT_DELINEATION]);
1513
1514
                break;
1515
        }
1516
1517
        echo '<div class="card">';
1518
        echo '<div class="card-body">';
1519
        echo '<ul class="question_menu">';
1520
        foreach ($questionTypeList as $i => $type) {
1521
            /** @var Question $type */
1522
            $type = new $type[1]();
1523
            $img = $type->getTypePicture();
1524
            $explanation = $type->getExplanation();
1525
            echo '<li>';
1526
            echo '<div class="icon-image">';
1527
            $icon = '<a href="admin.php?'.api_get_cidreq().'&newQuestion=yes&answerType='.$i.'&exerciseId='.$exerciseId.'">'.
1528
                Display::return_icon($img, $explanation, null, ICON_SIZE_BIG).'</a>';
1529
1530
            if (false === $objExercise->force_edit_exercise_in_lp) {
1531
                if (true == $objExercise->exercise_was_added_in_lp) {
1532
                    $img = pathinfo($img);
1533
                    $img = $img['filename'].'_na.'.$img['extension'];
1534
                    $icon = Display::return_icon($img, $explanation, null, ICON_SIZE_BIG);
1535
                }
1536
            }
1537
            echo $icon;
1538
            echo '</div>';
1539
            echo '</li>';
1540
        }
1541
1542
        echo '<li>';
1543
        echo '<div class="icon_image_content">';
1544
        if (true == $objExercise->exercise_was_added_in_lp) {
1545
            echo Display::return_icon(
1546
                'database_na.png',
1547
                get_lang('Recycle existing questions'),
1548
                null,
1549
                ICON_SIZE_BIG
1550
            );
1551
        } else {
1552
            if (in_array($feedbackType, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
1553
                echo $url = '<a href="question_pool.php?'.api_get_cidreq()."&type=1&fromExercise=$exerciseId\">";
1554
            } else {
1555
                echo $url = '<a href="question_pool.php?'.api_get_cidreq().'&fromExercise='.$exerciseId.'">';
1556
            }
1557
            echo Display::return_icon(
1558
                'database.png',
1559
                get_lang('Recycle existing questions'),
1560
                null,
1561
                ICON_SIZE_BIG
1562
            );
1563
        }
1564
        echo '</a>';
1565
        echo '</div></li>';
1566
        echo '</ul>';
1567
        echo '</div>';
1568
        echo '</div>';
1569
    }
1570
1571
    /**
1572
     * @param CQuizQuestion $question
1573
     * @param string        $name
1574
     * @param int           $position
1575
     *
1576
     * @return null|CQuizQuestion
1577
     */
1578
    public static function saveQuestionOption(CQuizQuestion $question, $name, $position = 0)
1579
    {
1580
        $option = new CQuizQuestionOption();
1581
        $option
1582
            ->setQuestion($question)
1583
            ->setName($name)
1584
            ->setPosition($position)
1585
        ;
1586
        $em = Database::getManager();
1587
        $em->persist($option);
1588
        $em->flush();
1589
    }
1590
1591
    /**
1592
     * @param int $question_id
1593
     * @param int $course_id
1594
     */
1595
    public static function deleteAllQuestionOptions($question_id, $course_id)
1596
    {
1597
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1598
        Database::delete(
1599
            $table,
1600
            [
1601
                'c_id = ? AND question_id = ?' => [
1602
                    $course_id,
1603
                    $question_id,
1604
                ],
1605
            ]
1606
        );
1607
    }
1608
1609
    /**
1610
     * @param int $question_id
1611
     *
1612
     * @return array
1613
     */
1614
    public static function readQuestionOption($question_id)
1615
    {
1616
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1617
1618
        return Database::select(
1619
            '*',
1620
            $table,
1621
            [
1622
                'where' => [
1623
                    'question_id = ?' => [
1624
                        $question_id,
1625
                    ],
1626
                ],
1627
                'order' => 'iid ASC',
1628
            ]
1629
        );
1630
    }
1631
1632
    /**
1633
     * Shows question title an description.
1634
     *
1635
     * @param int   $counter
1636
     * @param array $score
1637
     *
1638
     * @return string HTML string with the header of the question (before the answers table)
1639
     */
1640
    public function return_header(Exercise $exercise, $counter = null, $score = [])
1641
    {
1642
        $counterLabel = '';
1643
        if (!empty($counter)) {
1644
            $counterLabel = (int) $counter;
1645
        }
1646
1647
        $scoreLabel = get_lang('Wrong');
1648
        if (in_array($exercise->results_disabled, [
1649
            RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
1650
            RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
1651
        ])
1652
        ) {
1653
            $scoreLabel = get_lang('Wrong answer. The correct one was:');
1654
        }
1655
1656
        $class = 'error';
1657
        if (isset($score['pass']) && true == $score['pass']) {
1658
            $scoreLabel = get_lang('Correct');
1659
1660
            if (in_array($exercise->results_disabled, [
1661
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
1662
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
1663
            ])
1664
            ) {
1665
                $scoreLabel = get_lang('Correct answer');
1666
            }
1667
            $class = 'success';
1668
        }
1669
1670
        switch ($this->type) {
1671
            case FREE_ANSWER:
1672
            case ORAL_EXPRESSION:
1673
            case ANNOTATION:
1674
                $score['revised'] = isset($score['revised']) ? $score['revised'] : false;
1675
                if (true == $score['revised']) {
1676
                    $scoreLabel = get_lang('Revised');
1677
                    $class = '';
1678
                } else {
1679
                    $scoreLabel = get_lang('Not reviewed');
1680
                    $class = 'warning';
1681
                    if (isset($score['weight'])) {
1682
                        $weight = float_format($score['weight'], 1);
1683
                        $score['result'] = ' ? / '.$weight;
1684
                    }
1685
                    $model = ExerciseLib::getCourseScoreModel();
1686
                    if (!empty($model)) {
1687
                        $score['result'] = ' ? ';
1688
                    }
1689
1690
                    $hide = api_get_configuration_value('hide_free_question_score');
1691
                    if (true === $hide) {
1692
                        $score['result'] = '-';
1693
                    }
1694
                }
1695
1696
                break;
1697
            case UNIQUE_ANSWER:
1698
                if (in_array($exercise->results_disabled, [
1699
                    RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
1700
                    RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
1701
                ])
1702
                ) {
1703
                    if (isset($score['user_answered'])) {
1704
                        if (false === $score['user_answered']) {
1705
                            $scoreLabel = get_lang('Unanswered');
1706
                            $class = 'info';
1707
                        }
1708
                    }
1709
                }
1710
1711
                break;
1712
        }
1713
1714
        // display question category, if any
1715
        $header = '';
1716
        if ($exercise->display_category_name) {
1717
            $header = TestCategory::returnCategoryAndTitle($this->id);
1718
        }
1719
        $show_media = '';
1720
        if ($show_media) {
1721
            $header .= $this->show_media_content();
1722
        }
1723
1724
        $scoreCurrent = [
1725
            'used' => isset($score['score']) ? $score['score'] : '',
1726
            'missing' => isset($score['weight']) ? $score['weight'] : '',
1727
        ];
1728
        $header .= Display::page_subheader2($counterLabel.'. '.$this->question);
1729
1730
        $showRibbon = true;
1731
        // dont display score for certainty degree questions
1732
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $this->type) {
1733
            $showRibbon = false;
1734
            $ribbonResult = api_get_configuration_value('show_exercise_question_certainty_ribbon_result');
1735
            if (true === $ribbonResult) {
1736
                $showRibbon = true;
1737
            }
1738
        }
1739
1740
        if ($showRibbon && isset($score['result'])) {
1741
            if (in_array($exercise->results_disabled, [
1742
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
1743
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
1744
            ])
1745
            ) {
1746
                $score['result'] = null;
1747
            }
1748
            $header .= $exercise->getQuestionRibbon($class, $scoreLabel, $score['result'], $scoreCurrent);
1749
        }
1750
1751
        if (READING_COMPREHENSION != $this->type) {
1752
            // Do not show the description (the text to read) if the question is of type READING_COMPREHENSION
1753
            $header .= Display::div(
1754
                $this->description,
1755
                ['class' => 'question_description']
1756
            );
1757
        } else {
1758
            if (true == $score['pass']) {
1759
                $message = Display::div(
1760
                    sprintf(
1761
                        get_lang('Congratulations, you have reached and correctly understood, at a speed of %s words per minute, a text of a total %s words.'),
1762
                        ReadingComprehension::$speeds[$this->level],
1763
                        $this->getWordsCount()
1764
                    )
1765
                );
1766
            } else {
1767
                $message = Display::div(
1768
                    sprintf(
1769
                        get_lang('Sorry, it seems like a speed of %s words/minute was too fast for this text of %s words.'),
1770
                        ReadingComprehension::$speeds[$this->level],
1771
                        $this->getWordsCount()
1772
                    )
1773
                );
1774
            }
1775
            $header .= $message.'<br />';
1776
        }
1777
1778
        if ($exercise->hideComment && HOT_SPOT == $this->type) {
1779
            $header .= Display::return_message(get_lang('ResultsOnlyAvailableOnline'));
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 $header;
1791
    }
1792
1793
    /**
1794
     * @deprecated
1795
     * Create a question from a set of parameters
1796
     *
1797
     * @param int    $question_name        Quiz ID
1798
     * @param string $question_description Question name
1799
     * @param int    $max_score            Maximum result for the question
1800
     * @param int    $type                 Type of question (see constants at beginning of question.class.php)
1801
     * @param int    $level                Question level/category
1802
     * @param string $quiz_id
1803
     */
1804
    public function create_question(
1805
        $quiz_id,
1806
        $question_name,
1807
        $question_description = '',
1808
        $max_score = 0,
1809
        $type = 1,
1810
        $level = 1
1811
    ) {
1812
        $course_id = api_get_course_int_id();
1813
        $tbl_quiz_question = Database::get_course_table(TABLE_QUIZ_QUESTION);
1814
        $tbl_quiz_rel_question = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
1815
1816
        $quiz_id = (int) $quiz_id;
1817
        $max_score = (float) $max_score;
1818
        $type = (int) $type;
1819
        $level = (int) $level;
1820
1821
        // Get the max position
1822
        $sql = "SELECT max(position) as max_position
1823
                FROM $tbl_quiz_question q
1824
                INNER JOIN $tbl_quiz_rel_question r
1825
                ON
1826
                    q.id = r.question_id AND
1827
                    quiz_id = $quiz_id AND
1828
                    q.c_id = $course_id AND
1829
                    r.c_id = $course_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
            'c_id' => $course_id,
1836
            'question' => $question_name,
1837
            'description' => $question_description,
1838
            'ponderation' => $max_score,
1839
            'position' => $max_position,
1840
            'type' => $type,
1841
            'level' => $level,
1842
        ];
1843
        $question_id = Database::insert($tbl_quiz_question, $params);
1844
1845
        if ($question_id) {
1846
            // Get the max question_order
1847
            $sql = "SELECT max(question_order) as max_order
1848
                    FROM $tbl_quiz_rel_question
1849
                    WHERE c_id = $course_id AND 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 (c_id, question_id, quiz_id, question_order)
1855
                    VALUES($course_id, $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($course_id)
1949
    {
1950
        $medias = self::get_course_medias($course_id);
1951
        $media_list = [];
1952
        $media_list[0] = get_lang('Not linked to media');
1953
1954
        if (!empty($medias)) {
1955
            foreach ($medias as $media) {
1956
                $media_list[$media['id']] = empty($media['question']) ? get_lang('Untitled') : $media['question'];
1957
            }
1958
        }
1959
1960
        return $media_list;
1961
    }
1962
1963
    /**
1964
     * @return array
1965
     */
1966
    public static function get_default_levels()
1967
    {
1968
        return [
1969
            1 => 1,
1970
            2 => 2,
1971
            3 => 3,
1972
            4 => 4,
1973
            5 => 5,
1974
        ];
1975
    }
1976
1977
    /**
1978
     * @return string
1979
     */
1980
    public function show_media_content()
1981
    {
1982
        $html = '';
1983
        if (0 != $this->parent_id) {
1984
            $parent_question = self::read($this->parent_id);
1985
            $html = $parent_question->show_media_content();
1986
        } else {
1987
            $html .= Display::page_subheader($this->selectTitle());
1988
            $html .= $this->selectDescription();
1989
        }
1990
1991
        return $html;
1992
    }
1993
1994
    /**
1995
     * Swap between unique and multiple type answers.
1996
     *
1997
     * @return UniqueAnswer|MultipleAnswer
1998
     */
1999
    public function swapSimpleAnswerTypes()
2000
    {
2001
        $oppositeAnswers = [
2002
            UNIQUE_ANSWER => MULTIPLE_ANSWER,
2003
            MULTIPLE_ANSWER => UNIQUE_ANSWER,
2004
        ];
2005
        $this->type = $oppositeAnswers[$this->type];
2006
        Database::update(
2007
            Database::get_course_table(TABLE_QUIZ_QUESTION),
2008
            ['type' => $this->type],
2009
            ['c_id = ? AND id = ?' => [$this->course['real_id'], $this->id]]
2010
        );
2011
        $answerClasses = [
2012
            UNIQUE_ANSWER => 'UniqueAnswer',
2013
            MULTIPLE_ANSWER => 'MultipleAnswer',
2014
        ];
2015
        $swappedAnswer = new $answerClasses[$this->type]();
2016
        foreach ($this as $key => $value) {
2017
            $swappedAnswer->$key = $value;
2018
        }
2019
2020
        return $swappedAnswer;
2021
    }
2022
2023
    /**
2024
     * @param array $score
2025
     *
2026
     * @return bool
2027
     */
2028
    public function isQuestionWaitingReview($score)
2029
    {
2030
        $isReview = false;
2031
        if (!empty($score)) {
2032
            if (!empty($score['comments']) || $score['score'] > 0) {
2033
                $isReview = true;
2034
            }
2035
        }
2036
2037
        return $isReview;
2038
    }
2039
2040
    /**
2041
     * @param string $value
2042
     */
2043
    public function setFeedback($value)
2044
    {
2045
        $this->feedback = $value;
2046
    }
2047
2048
    /**
2049
     * @param Exercise $exercise
2050
     *
2051
     * @return bool
2052
     */
2053
    public function showFeedback($exercise)
2054
    {
2055
        if (false === $exercise->hideComment) {
2056
            return false;
2057
        }
2058
2059
        return
2060
            in_array($this->type, $this->questionTypeWithFeedback) &&
2061
            EXERCISE_FEEDBACK_TYPE_EXAM != $exercise->getFeedbackType();
2062
    }
2063
2064
    /**
2065
     * @return string
2066
     */
2067
    public function returnFormatFeedback()
2068
    {
2069
        return '<br />'.Display::return_message($this->feedback, 'normal', false);
2070
    }
2071
2072
    /**
2073
     * Check if this question exists in another exercise.
2074
     *
2075
     * @throws \Doctrine\ORM\Query\QueryException
2076
     *
2077
     * @return bool
2078
     */
2079
    public function existsInAnotherExercise()
2080
    {
2081
        $count = $this->getCountExercise();
2082
2083
        return $count > 1;
2084
    }
2085
2086
    /**
2087
     * @throws \Doctrine\ORM\Query\QueryException
2088
     *
2089
     * @return int
2090
     */
2091
    public function getCountExercise()
2092
    {
2093
        $em = Database::getManager();
2094
2095
        $count = $em
2096
            ->createQuery('
2097
                SELECT COUNT(qq.iid) FROM ChamiloCourseBundle:CQuizRelQuestion qq
2098
                WHERE qq.question = :id
2099
            ')
2100
            ->setParameters(['id' => (int) $this->id])
2101
            ->getSingleScalarResult();
2102
2103
        return (int) $count;
2104
    }
2105
2106
    /**
2107
     * Check if this question exists in another exercise.
2108
     *
2109
     * @throws \Doctrine\ORM\Query\QueryException
2110
     */
2111
    public function getExerciseListWhereQuestionExists()
2112
    {
2113
        $em = Database::getManager();
2114
2115
        return $em
2116
            ->createQuery('
2117
                SELECT e
2118
                FROM ChamiloCourseBundle:CQuizRelQuestion qq
2119
                JOIN ChamiloCourseBundle:CQuiz e
2120
                WHERE e.iid = qq.exerciceId AND qq.questionId = :id
2121
            ')
2122
            ->setParameters(['id' => (int) $this->id])
2123
            ->getResult();
2124
    }
2125
2126
    /**
2127
     * @return int
2128
     */
2129
    public function countAnswers()
2130
    {
2131
        $result = Database::select(
2132
            'COUNT(1) AS c',
2133
            Database::get_course_table(TABLE_QUIZ_ANSWER),
2134
            ['where' => ['question_id = ?' => [$this->id]]],
2135
            'first'
2136
        );
2137
2138
        return (int) $result['c'];
2139
    }
2140
}
2141