Completed
Push — master ( af0fd1...a35e0e )
by Julito
14:23
created

Question::updateType()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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