Completed
Push — master ( 4fc9f8...d0e06e )
by Julito
12:04
created

Question::prepare_course_media_select()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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