Passed
Push — 1.11.x ( bce6cd...c146d9 )
by Angel Fernando Quiroz
12:25
created

main/exercise/TestCategory.php (2 issues)

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use ChamiloSession as Session;
6
7
/**
8
 * Class TestCategory.
9
 * Manage question categories inside an exercise.
10
 *
11
 * @author hubert.borderiou
12
 * @author Julio Montoya - several fixes
13
 *
14
 * @todo rename to ExerciseCategory
15
 */
16
class TestCategory
17
{
18
    public $iid;
19
    public $name;
20
    public $description;
21
22
    /**
23
     * Constructor of the class Category.
24
     */
25
    public function __construct()
26
    {
27
        $this->name = '';
28
        $this->description = '';
29
    }
30
31
    /**
32
     * return the TestCategory object with iid=$id.
33
     *
34
     * @param int $id
35
     * @param int $courseId
36
     *
37
     * @return TestCategory
38
     */
39
    public function getCategory($id, $courseId = 0)
40
    {
41
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
42
        $id = (int) $id;
43
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
44
        $sql = "SELECT * FROM $table
45
                WHERE iid = ".$id;
46
        $res = Database::query($sql);
47
48
        if (Database::num_rows($res)) {
49
            $row = Database::fetch_array($res);
50
51
            $this->iid = $row['iid'];
52
            $this->name = $row['title'];
53
            $this->description = $row['description'];
54
55
            return $this;
56
        }
57
58
        return false;
59
    }
60
61
    /**
62
     * Save TestCategory in the database if name doesn't exists.
63
     *
64
     * @param int $courseId
65
     *
66
     * @return bool
67
     */
68
    public function save($courseId = 0)
69
    {
70
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
71
        $courseInfo = api_get_course_info_by_id($courseId);
72
        if (empty($courseInfo)) {
73
            return false;
74
        }
75
76
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
77
78
        // check if name already exists
79
        $sql = "SELECT count(*) AS nb FROM $table
80
                WHERE title = '".Database::escape_string($this->name)."' AND c_id = $courseId";
81
        $result = Database::query($sql);
82
        $row = Database::fetch_array($result);
83
        // lets add in BDD if not the same name
84
        if ($row['nb'] <= 0) {
85
            $params = [
86
                'c_id' => $courseId,
87
                'title' => $this->name,
88
                'description' => $this->description,
89
            ];
90
            $newId = Database::insert($table, $params);
91
92
            if ($newId) {
93
                api_item_property_update(
94
                    $courseInfo,
95
                    TOOL_TEST_CATEGORY,
96
                    $newId,
97
                    'TestCategoryAdded',
98
                    api_get_user_id()
99
                );
100
            }
101
102
            return $newId;
103
        } else {
104
            return false;
105
        }
106
    }
107
108
    /**
109
     * Removes the category from the database
110
     * if there were question in this category, the link between question and category is removed.
111
     *
112
     * @param int $id
113
     *
114
     * @return bool
115
     */
116
    public function removeCategory($id)
117
    {
118
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
119
        $tbl_question_rel_cat = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
120
        $id = (int) $id;
121
        $course_id = api_get_course_int_id();
122
        $category = $this->getCategory($id);
123
124
        if ($category) {
125
            $sql = "DELETE FROM $table
126
                    WHERE iid = ".$id;
127
            Database::query($sql);
128
129
            // remove link between question and category
130
            $sql = "DELETE FROM $tbl_question_rel_cat
131
                    WHERE category_id = ".$id;
132
            Database::query($sql);
133
            // item_property update
134
            $courseInfo = api_get_course_info_by_id($course_id);
135
            api_item_property_update(
136
                $courseInfo,
137
                TOOL_TEST_CATEGORY,
138
                $this->iid,
139
                'TestCategoryDeleted',
140
                api_get_user_id()
141
            );
142
143
            return true;
144
        }
145
146
        return false;
147
    }
148
149
    /**
150
     * Modify category name or description of category with id=in_id.
151
     *
152
     * @param int $courseId
153
     *
154
     * @return bool
155
     */
156
    public function modifyCategory($courseId = 0)
157
    {
158
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
159
        $id = (int) $this->iid;
160
        $name = Database::escape_string($this->name);
161
        $description = Database::escape_string($this->description);
162
        $cat = $this->getCategory($id, $courseId);
163
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
164
        $courseInfo = api_get_course_info_by_id($courseId);
165
        if (empty($courseInfo)) {
166
            return false;
167
        }
168
169
        if ($cat) {
170
            $sql = "UPDATE $table SET
171
                        title = '$name',
172
                        description = '$description'
173
                    WHERE iid = $id";
174
            Database::query($sql);
175
176
            api_item_property_update(
177
                $courseInfo,
178
                TOOL_TEST_CATEGORY,
179
                $this->iid,
180
                'TestCategoryModified',
181
                api_get_user_id()
182
            );
183
184
            return true;
185
        }
186
187
        return false;
188
    }
189
190
    /**
191
     * Gets the number of question of category id=in_id.
192
     */
193
    public function getCategoryQuestionsNumber()
194
    {
195
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
196
        $id = (int) $this->iid;
197
        $sql = "SELECT count(*) AS nb
198
                FROM $table
199
                WHERE category_id = $id AND c_id = ".api_get_course_int_id();
200
        $res = Database::query($sql);
201
        $row = Database::fetch_array($res);
202
203
        return $row['nb'];
204
    }
205
206
    /**
207
     * Return an array of all Category objects in the database
208
     * If $field=="" Return an array of all category objects in the database
209
     * Otherwise, return an array of all in_field value
210
     * in the database (in_field = id or name or description).
211
     *
212
     * @param string $field
213
     * @param int    $courseId
214
     *
215
     * @return array
216
     */
217
    public static function getCategoryListInfo($field = '', $courseId = 0)
218
    {
219
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
220
221
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
222
        $categories = [];
223
        if (empty($field)) {
224
            $sql = "SELECT iid FROM $table
225
                    WHERE c_id = $courseId
226
                    ORDER BY title ASC";
227
            $res = Database::query($sql);
228
            while ($row = Database::fetch_array($res)) {
229
                $category = new TestCategory();
230
                $categories[] = $category->getCategory($row['iid'], $courseId);
231
            }
232
        } else {
233
            $field = Database::escape_string($field);
234
            $sql = "SELECT $field FROM $table
235
                    WHERE c_id = $courseId
236
                    ORDER BY `$field` ASC";
237
            $res = Database::query($sql);
238
            while ($row = Database::fetch_array($res)) {
239
                $categories[] = $row[$field];
240
            }
241
        }
242
243
        return $categories;
244
    }
245
246
    /**
247
     * Return the TestCategory id for question with question_id = $questionId
248
     * In this version, a question has only 1 TestCategory.
249
     * Return the TestCategory id, 0 if none.
250
     *
251
     * @param int $questionId
252
     * @param int $courseId
253
     *
254
     * @return int
255
     */
256
    public static function getCategoryForQuestion($questionId, $courseId = 0)
257
    {
258
        $categoryInfo = self::getCategoryInfoForQuestion($questionId, $courseId);
259
260
        if (!empty($categoryInfo) && isset($categoryInfo['category_id'])) {
261
            return (int) $categoryInfo['category_id'];
262
        }
263
264
        return 0;
265
    }
266
267
    public static function getCategoryInfoForQuestion($questionId, $courseId = 0)
268
    {
269
        $courseId = (int) $courseId;
270
        $questionId = (int) $questionId;
271
272
        if (empty($courseId)) {
273
            $courseId = api_get_course_int_id();
274
        }
275
276
        if (empty($courseId) || empty($questionId)) {
277
            return 0;
278
        }
279
280
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
281
        $sql = "SELECT *
282
                FROM $table
283
                WHERE question_id = $questionId AND c_id = $courseId";
284
        $res = Database::query($sql);
285
        if (Database::num_rows($res) > 0) {
286
            return Database::fetch_array($res, 'ASSOC');
287
        }
288
289
        return [];
290
    }
291
292
    /**
293
     * Return the category name for question with question_id = $questionId
294
     * In this version, a question has only 1 category.
295
     *
296
     * @param $questionId
297
     * @param int $courseId
298
     *
299
     * @return string
300
     */
301
    public static function getCategoryNameForQuestion($questionId, $courseId = 0)
302
    {
303
        if (empty($courseId)) {
304
            $courseId = api_get_course_int_id();
305
        }
306
        $courseId = (int) $courseId;
307
        $categoryId = self::getCategoryForQuestion($questionId, $courseId);
308
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
309
        $sql = "SELECT title
310
                FROM $table
311
                WHERE iid = $categoryId";
312
        $res = Database::query($sql);
313
        $data = Database::fetch_array($res);
314
        $result = '';
315
        if (Database::num_rows($res) > 0) {
316
            $result = $data['title'];
317
        }
318
319
        return $result;
320
    }
321
322
    /**
323
     * Return the list of different categories ID for a test in the current course
324
     * hubert.borderiou 07-04-2011.
325
     *
326
     * @param int $exerciseId
327
     * @param int $courseId
328
     *
329
     * @return array
330
     */
331
    public static function getListOfCategoriesIDForTest($exerciseId, $courseId = 0)
332
    {
333
        // Check test questions, obtaining unique categories in a table
334
        $exercise = new Exercise($courseId);
335
        $exercise->read($exerciseId, false);
336
        $categoriesInExercise = $exercise->getQuestionWithCategories();
337
        // the array given by selectQuestionList start at indice 1 and not at indice 0 !!! ???
338
        $categories = [];
339
        if (!empty($categoriesInExercise)) {
340
            foreach ($categoriesInExercise as $category) {
341
                $categories[$category['iid']] = $category;
342
            }
343
        }
344
345
        return $categories;
346
    }
347
348
    /**
349
     * @return array
350
     */
351
    public static function getListOfCategoriesIDForTestObject(Exercise $exercise)
352
    {
353
        // Check the categories of a test, obtaining unique categories in table
354
        $categories_in_exercise = [];
355
        $question_list = $exercise->getQuestionOrderedListByName();
356
357
        // the array given by selectQuestionList start at indice 1 and not at indice 0 !!! ???
358
        foreach ($question_list as $questionInfo) {
359
            $question_id = $questionInfo['question_id'];
360
            $category_list = self::getCategoryForQuestion($question_id);
361
            if (is_numeric($category_list)) {
362
                $category_list = [$category_list];
363
            }
364
365
            if (!empty($category_list)) {
366
                $categories_in_exercise = array_merge($categories_in_exercise, $category_list);
367
            }
368
        }
369
        if (!empty($categories_in_exercise)) {
370
            $categories_in_exercise = array_unique(array_filter($categories_in_exercise));
371
        }
372
373
        return $categories_in_exercise;
374
    }
375
376
    /**
377
     * Return the list of different categories NAME for a test.
378
     *
379
     * @param int $exerciseId
380
     * @param bool
381
     *
382
     * @return array
383
     *
384
     * @author function rewrote by jmontoya
385
     */
386
    public static function getListOfCategoriesNameForTest($exerciseId, $grouped_by_category = true)
387
    {
388
        $result = [];
389
        $categories = self::getListOfCategoriesIDForTest($exerciseId);
390
391
        foreach ($categories as $catInfo) {
392
            $categoryId = $catInfo['iid'];
393
            if (!empty($categoryId)) {
394
                $result[$categoryId] = [
395
                    'iid' => $categoryId,
396
                    'title' => $catInfo['title'],
397
                    //'parent_id' =>  $catInfo['parent_id'],
398
                    'parent_id' => '',
399
                    'c_id' => $catInfo['c_id'],
400
                ];
401
            }
402
        }
403
404
        return $result;
405
    }
406
407
    /**
408
     * @return array
409
     */
410
    public static function getListOfCategoriesForTest(Exercise $exercise)
411
    {
412
        $result = [];
413
        $categories = self::getListOfCategoriesIDForTestObject($exercise);
414
        foreach ($categories as $cat_id) {
415
            $cat = new TestCategory();
416
            //Todo remove this weird array casting
417
            $cat = (array) $cat->getCategory($cat_id);
418
            $cat['title'] = $cat['name'];
419
            $result[$cat['iid']] = $cat;
420
        }
421
422
        return $result;
423
    }
424
425
    /**
426
     * return the number of question of a category id in a test.
427
     *
428
     * @param int $exerciseId
429
     * @param int $categoryId
430
     *
431
     * @return int
432
     *
433
     * @author hubert.borderiou 07-04-2011
434
     */
435
    public static function getNumberOfQuestionsInCategoryForTest($exerciseId, $categoryId)
436
    {
437
        $nbCatResult = 0;
438
        $quiz = new Exercise();
439
        $quiz->read($exerciseId);
440
        $questionList = $quiz->selectQuestionList();
441
        // the array given by selectQuestionList start at indice 1 and not at indice 0 !!! ? ? ?
442
        for ($i = 1; $i <= count($questionList); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
443
            if (self::getCategoryForQuestion($questionList[$i]) == $categoryId) {
444
                $nbCatResult++;
445
            }
446
        }
447
448
        return $nbCatResult;
449
    }
450
451
    /**
452
     * return the number of question for a test using random by category
453
     * input  : test_id, number of random question (min 1).
454
     *
455
     * @param int $exerciseId
456
     * @param int $random
457
     *
458
     * @return int
459
     *             hubert.borderiou 07-04-2011
460
     *             question without categories are not counted
461
     */
462
    public static function getNumberOfQuestionRandomByCategory($exerciseId, $random)
463
    {
464
        $count = 0;
465
        $categories = self::getListOfCategoriesIDForTest($exerciseId);
466
        foreach ($categories as $category) {
467
            if (empty($category['iid'])) {
468
                continue;
469
            }
470
471
            $nbQuestionInThisCat = self::getNumberOfQuestionsInCategoryForTest(
472
                $exerciseId,
473
                $category['iid']
474
            );
475
476
            if ($nbQuestionInThisCat > $random) {
477
                $count += $random;
478
            } else {
479
                $count += $nbQuestionInThisCat;
480
            }
481
        }
482
483
        return $count;
484
    }
485
486
    /**
487
     * Return an array (id=>name)
488
     * array[0] = get_lang('NoCategory');.
489
     *
490
     * @param int $courseId
491
     *
492
     * @return array
493
     */
494
    public static function getCategoriesIdAndName($courseId = 0)
495
    {
496
        if (empty($courseId)) {
497
            $courseId = api_get_course_int_id();
498
        }
499
        $categories = self::getCategoryListInfo('', $courseId);
500
        $result = ['0' => get_lang('NoCategorySelected')];
501
        for ($i = 0; $i < count($categories); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
502
            $result[$categories[$i]->iid] = Security::remove_XSS($categories[$i]->name);
503
        }
504
505
        return $result;
506
    }
507
508
    /**
509
     * Returns an array of question ids for each category
510
     * $categories[1][30] = 10, array with category id = 1 and question_id = 10
511
     * A question has "n" categories.
512
     *
513
     * @param int   $exerciseId
514
     * @param array $check_in_question_list
515
     * @param array $categoriesAddedInExercise
516
     *
517
     * @return array
518
     */
519
    public static function getQuestionsByCat(
520
        $exerciseId,
521
        $check_in_question_list = [],
522
        $categoriesAddedInExercise = [],
523
        $onlyMandatory = false
524
    ) {
525
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
526
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
527
        $TBL_QUESTION_REL_CATEGORY = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
528
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
529
        $exerciseId = (int) $exerciseId;
530
        $courseId = api_get_course_int_id();
531
532
        $mandatoryCondition = '';
533
        if ($onlyMandatory) {
534
            $mandatoryCondition = ' AND qrc.mandatory = 1';
535
        }
536
537
        $sql = "SELECT DISTINCT qrc.question_id, qrc.category_id
538
                FROM $TBL_QUESTION_REL_CATEGORY qrc
539
                INNER JOIN $TBL_EXERCICE_QUESTION eq
540
                ON (eq.question_id = qrc.question_id)
541
                INNER JOIN $categoryTable c
542
                ON (c.iid = qrc.category_id AND c.c_id = eq.c_id)
543
                INNER JOIN $tableQuestion q
544
                ON q.iid = qrc.question_id
545
                WHERE
546
                    exercice_id = $exerciseId AND
547
                    qrc.c_id = $courseId
548
                    $mandatoryCondition
549
                ";
550
551
        $res = Database::query($sql);
552
        $categories = [];
553
        while ($data = Database::fetch_array($res)) {
554
            if (!empty($check_in_question_list)) {
555
                if (!in_array($data['question_id'], $check_in_question_list)) {
556
                    continue;
557
                }
558
            }
559
560
            if (!isset($categories[$data['category_id']]) ||
561
                !is_array($categories[$data['category_id']])
562
            ) {
563
                $categories[$data['category_id']] = [];
564
            }
565
566
            $categories[$data['category_id']][] = $data['question_id'];
567
        }
568
569
        if (!empty($categoriesAddedInExercise)) {
570
            $newCategoryList = [];
571
            foreach ($categoriesAddedInExercise as $category) {
572
                $categoryId = $category['category_id'];
573
                if (isset($categories[$categoryId])) {
574
                    $newCategoryList[$categoryId] = $categories[$categoryId];
575
                }
576
            }
577
578
            $checkQuestionsWithNoCategory = false;
579
            foreach ($categoriesAddedInExercise as $category) {
580
                if (empty($category['category_id'])) {
581
                    // Check
582
                    $checkQuestionsWithNoCategory = true;
583
                    break;
584
                }
585
            }
586
587
            // Select questions that don't have any category related
588
            if ($checkQuestionsWithNoCategory) {
589
                $originalQuestionList = $check_in_question_list;
590
                foreach ($originalQuestionList as $questionId) {
591
                    $categoriesFlatten = array_flatten($categories);
592
                    if (!in_array($questionId, $categoriesFlatten)) {
593
                        $newCategoryList[0][] = $questionId;
594
                    }
595
                }
596
            }
597
            $categories = $newCategoryList;
598
        }
599
600
        return $categories;
601
    }
602
603
    /**
604
     * Returns an array of $numberElements from $elements.
605
     *
606
     * @param array $elements
607
     * @param int   $numberElements
608
     * @param bool  $shuffle
609
     * @param array $mandatoryElements
610
     *
611
     * @return array
612
     */
613
    public static function getNElementsFromArray($elements, $numberElements, $shuffle = true, $mandatoryElements = [])
614
    {
615
        $countElements = count($elements);
616
        $countMandatory = count($mandatoryElements);
617
618
        if (!empty($countMandatory)) {
619
            if ($countMandatory >= $numberElements) {
620
                if ($shuffle) {
621
                    shuffle($mandatoryElements);
622
                }
623
                $elements = array_slice($mandatoryElements, 0, $numberElements);
624
625
                return $elements;
626
            }
627
628
            $diffCount = $numberElements - $countMandatory;
629
            $diffElements = array_diff($elements, $mandatoryElements);
630
            if ($shuffle) {
631
                shuffle($diffElements);
632
            }
633
            $elements = array_slice($diffElements, 0, $diffCount);
634
            $totalElements = array_merge($mandatoryElements, $elements);
635
            if ($shuffle) {
636
                shuffle($totalElements);
637
            }
638
639
            return $totalElements;
640
        }
641
642
        if ($shuffle) {
643
            shuffle($elements);
644
        }
645
646
        if ($numberElements < $countElements) {
647
            $elements = array_slice($elements, 0, $numberElements);
648
        }
649
650
        return $elements;
651
    }
652
653
    /**
654
     * @param int $questionId
655
     * @param int $displayCategoryName
656
     */
657
    public static function displayCategoryAndTitle($questionId, $displayCategoryName = 1)
658
    {
659
        echo self::returnCategoryAndTitle($questionId, $displayCategoryName);
660
    }
661
662
    /**
663
     * @param int $questionId
664
     * @param int $in_display_category_name
665
     *
666
     * @return string|null
667
     */
668
    public static function returnCategoryAndTitle($questionId, $in_display_category_name = 1)
669
    {
670
        $is_student = !(api_is_allowed_to_edit(null, true) || api_is_session_admin());
671
        $objExercise = Session::read('objExercise');
672
        if (!empty($objExercise)) {
673
            $in_display_category_name = $objExercise->display_category_name;
674
        }
675
        $content = null;
676
        if (self::getCategoryNameForQuestion($questionId) != '' &&
677
            ($in_display_category_name == 1 || !$is_student)
678
        ) {
679
            $content .= '<div class="page-header">';
680
            $content .= '<h4>'.get_lang('Category').": ".Security::remove_XSS(self::getCategoryNameForQuestion($questionId)).'</h4>';
681
            $content .= "</div>";
682
        }
683
684
        return $content;
685
    }
686
687
    /**
688
     * sortTabByBracketLabel ($tabCategoryQuestions)
689
     * key of $tabCategoryQuestions are the category id (0 for not in a category)
690
     * value is the array of question id of this category
691
     * Sort question by Category.
692
     */
693
    public static function sortTabByBracketLabel($in_tab)
694
    {
695
        $tabResult = [];
696
        $tabCatName = []; // tab of category name
697
        foreach ($in_tab as $cat_id => $tabquestion) {
698
            $category = new TestCategory();
699
            $category = $category->getCategory($cat_id);
700
            $tabCatName[$cat_id] = $category->name;
701
        }
702
        reset($in_tab);
703
        // sort table by value, keeping keys as they are
704
        asort($tabCatName);
705
        // keys of $tabCatName are keys order for $in_tab
706
        foreach ($tabCatName as $key => $val) {
707
            $tabResult[$key] = $in_tab[$key];
708
        }
709
710
        return $tabResult;
711
    }
712
713
    /**
714
     * Return the number max of question in a category
715
     * count the number of questions in all categories, and return the max.
716
     *
717
     * @param int $exerciseId
718
     *
719
     * @author - hubert borderiou
720
     *
721
     * @return int
722
     */
723
    public static function getNumberMaxQuestionByCat($exerciseId)
724
    {
725
        $res_num_max = 0;
726
        // foreach question
727
        $categories = self::getListOfCategoriesIDForTest($exerciseId);
728
        foreach ($categories as $category) {
729
            if (empty($category['iid'])) {
730
                continue;
731
            }
732
733
            $nbQuestionInThisCat = self::getNumberOfQuestionsInCategoryForTest(
734
                $exerciseId,
735
                $category['iid']
736
            );
737
738
            if ($nbQuestionInThisCat > $res_num_max) {
739
                $res_num_max = $nbQuestionInThisCat;
740
            }
741
        }
742
743
        return $res_num_max;
744
    }
745
746
    /**
747
     * Returns a category summary report.
748
     *
749
     * @param Exercise $exercise
750
     * @param array    $category_list
751
     *                                pre filled array with the category_id, score, and weight
752
     *                                example: array(1 => array('score' => '10', 'total' => 20));
753
     *
754
     * @return string
755
     */
756
    public static function get_stats_table_by_attempt($exercise, $category_list = [])
757
    {
758
        if (empty($category_list) || empty($exercise)) {
759
            return '';
760
        }
761
762
        $hide = (int) $exercise->getPageConfigurationAttribute('hide_category_table');
763
        if (1 === $hide) {
764
            return '';
765
        }
766
767
        $exerciseId = $exercise->iid;
768
        $categoryNameList = self::getListOfCategoriesNameForTest($exerciseId);
769
770
        $table = new HTML_Table(
771
            [
772
                'class' => 'table table-hover table-striped table-bordered',
773
                'id' => 'category_results',
774
            ]
775
        );
776
        $table->setHeaderContents(0, 0, get_lang('Categories'));
777
        $table->setHeaderContents(0, 1, get_lang('AbsoluteScore'));
778
        $table->setHeaderContents(0, 2, get_lang('RelativeScore'));
779
        $row = 1;
780
781
        $none_category = [];
782
        if (isset($category_list['none'])) {
783
            $none_category = $category_list['none'];
784
            unset($category_list['none']);
785
        }
786
787
        $total = [];
788
        if (isset($category_list['total'])) {
789
            $total = $category_list['total'];
790
            unset($category_list['total']);
791
        }
792
793
        $radar = '';
794
        $countCategories = count($category_list);
795
        if ($countCategories > 1) {
796
            $tempResult = [];
797
            $labels = [];
798
            $labelsWithId = array_column($categoryNameList, 'title', 'iid');
799
            asort($labelsWithId);
800
            foreach ($labelsWithId as $category_id => $title) {
801
                if (!isset($category_list[$category_id])) {
802
                    continue;
803
                }
804
                $labels[] = $title;
805
                $category_item = $category_list[$category_id];
806
807
                $table->setCellContents($row, 0, $title);
808
                $table->setCellContents(
809
                    $row,
810
                    1,
811
                    ExerciseLib::show_score(
812
                        $category_item['score'],
813
                        $category_item['total'],
814
                        false
815
                    )
816
                );
817
                $table->setCellContents(
818
                    $row,
819
                    2,
820
                    ExerciseLib::show_score(
821
                        $category_item['score'],
822
                        $category_item['total'],
823
                        true,
824
                        false,
825
                        true
826
                    )
827
                );
828
                $tempResult[$category_id] = round($category_item['score'] / $category_item['total'] * 10);
829
                $row++;
830
            }
831
832
            // Radar requires more than 3 categories.
833
            if ($countCategories > 2 && RESULT_DISABLE_RADAR === (int) $exercise->results_disabled) {
834
                $resultsArray = [];
835
                foreach ($labelsWithId as $categoryId => $label) {
836
                    if (isset($tempResult[$categoryId])) {
837
                        $resultsArray[] = $tempResult[$categoryId];
838
                    } else {
839
                        $resultsArray[] = 0;
840
                    }
841
                }
842
                $radar = $exercise->getRadar($labels, [$resultsArray]);
843
            }
844
845
            if (!empty($none_category)) {
846
                $table->setCellContents($row, 0, get_lang('None'));
847
                $table->setCellContents(
848
                    $row,
849
                    1,
850
                    ExerciseLib::show_score(
851
                        $none_category['score'],
852
                        $none_category['total'],
853
                        false
854
                    )
855
                );
856
                $table->setCellContents(
857
                    $row,
858
                    2,
859
                    ExerciseLib::show_score(
860
                        $none_category['score'],
861
                        $none_category['total'],
862
                        true,
863
                        false,
864
                        true
865
                    )
866
                );
867
                $row++;
868
            }
869
            if (!empty($total)) {
870
                $table->setCellContents($row, 0, get_lang('Total'));
871
                $table->setCellContents(
872
                    $row,
873
                    1,
874
                    ExerciseLib::show_score(
875
                        $total['score'],
876
                        $total['total'],
877
                        false
878
                    )
879
                );
880
                $table->setCellContents(
881
                    $row,
882
                    2,
883
                    ExerciseLib::show_score(
884
                        $total['score'],
885
                        $total['total'],
886
                        true,
887
                        false,
888
                        true
889
                    )
890
                );
891
            }
892
893
            return $radar.$table->toHtml();
894
        }
895
896
        return '';
897
    }
898
899
    /**
900
     * @param Exercise $exercise
901
     * @param int      $courseId
902
     * @param string   $order
903
     * @param bool     $shuffle
904
     * @param bool     $excludeCategoryWithNoQuestions
905
     *
906
     * @return array
907
     */
908
    public function getCategoryExerciseTree(
909
        $exercise,
910
        $courseId,
911
        $order = null,
912
        $shuffle = false,
913
        $excludeCategoryWithNoQuestions = true
914
    ) {
915
        if (empty($exercise)) {
916
            return [];
917
        }
918
919
        $courseId = (int) $courseId;
920
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
921
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
922
        $exercise->iid = (int) $exercise->iid;
923
924
        $sql = "SELECT * FROM $table qc
925
                LEFT JOIN $categoryTable c
926
                ON (c.iid = qc.category_id)
927
                WHERE qc.c_id = $courseId AND exercise_id = {$exercise->iid} ";
928
929
        if (!empty($order)) {
930
            $order = Database::escape_string($order);
931
            $sql .= "ORDER BY $order";
932
        }
933
934
        $categories = [];
935
        $result = Database::query($sql);
936
        if (Database::num_rows($result)) {
937
            while ($row = Database::fetch_array($result, 'ASSOC')) {
938
                if ($excludeCategoryWithNoQuestions) {
939
                    if ($row['count_questions'] == 0) {
940
                        continue;
941
                    }
942
                }
943
                if (empty($row['title']) && empty($row['category_id'])) {
944
                    $row['title'] = get_lang('NoCategory');
945
                }
946
                $categories[$row['category_id']] = $row;
947
            }
948
        }
949
950
        if ($shuffle) {
951
            shuffle_assoc($categories);
952
        }
953
954
        return $categories;
955
    }
956
957
    /**
958
     * @param FormValidator $form
959
     * @param string        $action
960
     */
961
    public function getForm(&$form, $action = 'new')
962
    {
963
        switch ($action) {
964
            case 'new':
965
                $header = get_lang('AddACategory');
966
                $submit = get_lang('AddTestCategory');
967
                break;
968
            case 'edit':
969
                $header = get_lang('EditCategory');
970
                $submit = get_lang('ModifyCategory');
971
                break;
972
        }
973
974
        // Setting the form elements
975
        $form->addElement('header', $header);
976
        $form->addElement('hidden', 'category_id');
977
        $form->addElement(
978
            'text',
979
            'category_name',
980
            get_lang('CategoryName'),
981
            ['class' => 'span6']
982
        );
983
        $form->add_html_editor(
984
            'category_description',
985
            get_lang('CategoryDescription'),
986
            false,
987
            false,
988
            [
989
                'ToolbarSet' => 'test_category',
990
                'Width' => '90%',
991
                'Height' => '200',
992
            ]
993
        );
994
        $category_parent_list = [];
995
996
        $options = [
997
                '1' => get_lang('Visible'),
998
                '0' => get_lang('Hidden'),
999
        ];
1000
        $form->addElement(
1001
            'select',
1002
            'visibility',
1003
            get_lang('Visibility'),
1004
            $options
1005
        );
1006
        $script = null;
1007
        if (!empty($this->parent_id)) {
1008
            $parent_cat = new TestCategory();
1009
            $parent_cat = $parent_cat->getCategory($this->parent_id);
1010
            $category_parent_list = [$parent_cat->iid => $parent_cat->name];
1011
            $script .= '<script>$(function() { $("#parent_id").trigger("addItem",[{"title": "'.$parent_cat->name.'", "value": "'.$parent_cat->iid.'"}]); });</script>';
1012
        }
1013
        $form->addElement('html', $script);
1014
1015
        $form->addElement('select', 'parent_id', get_lang('Parent'), $category_parent_list, ['id' => 'parent_id']);
1016
        $form->addElement('style_submit_button', 'SubmitNote', $submit, 'class="add"');
1017
1018
        // setting the defaults
1019
        $defaults = [];
1020
        $defaults["category_id"] = $this->iid;
1021
        $defaults["category_name"] = $this->name;
1022
        $defaults["category_description"] = $this->description;
1023
        $defaults["parent_id"] = $this->parent_id;
1024
        $defaults["visibility"] = $this->visibility;
1025
        $form->setDefaults($defaults);
1026
1027
        // setting the rules
1028
        $form->addRule('category_name', get_lang('ThisFieldIsRequired'), 'required');
1029
    }
1030
1031
    /**
1032
     * Returns the category form.
1033
     *
1034
     * @return string
1035
     */
1036
    public function returnCategoryForm(Exercise $exercise)
1037
    {
1038
        $categories = $this->getListOfCategoriesForTest($exercise);
1039
        $sortedCategories = [];
1040
        foreach ($categories as $catId => $cat) {
1041
            $sortedCategories[$cat['title']] = $cat;
1042
        }
1043
        ksort($sortedCategories);
1044
        $saved_categories = $exercise->getCategoriesInExercise();
1045
        $return = null;
1046
1047
        if (!empty($sortedCategories)) {
1048
            $nbQuestionsTotal = $exercise->getNumberQuestionExerciseCategory();
1049
            $exercise->setCategoriesGrouping(true);
1050
            $real_question_count = count($exercise->getQuestionList());
1051
1052
            $warning = null;
1053
            if ($nbQuestionsTotal != $real_question_count) {
1054
                $warning = Display::return_message(
1055
                    get_lang('CheckThatYouHaveEnoughQuestionsInYourCategories'),
1056
                    'warning'
1057
                );
1058
            }
1059
1060
            $return .= $warning;
1061
            $return .= '<table class="table table-hover table-bordered data_table">';
1062
            $return .= '<tr>';
1063
            $return .= '<th height="24">'.get_lang('Categories').'</th>';
1064
            $return .= '<th width="70" height="24">'.get_lang('Number').'</th></tr>';
1065
1066
            $emptyCategory = [
1067
                'id' => '0',
1068
                'name' => get_lang('NoCategory'),
1069
                'description' => '',
1070
                'iid' => '0',
1071
                'title' => get_lang('NoCategory'),
1072
            ];
1073
1074
            $sortedCategories[] = $emptyCategory;
1075
1076
            foreach ($sortedCategories as $category) {
1077
                $cat_id = $category['iid'];
1078
                $return .= '<tr>';
1079
                $return .= '<td>';
1080
                $return .= Display::div($category['name']);
1081
                $return .= '</td>';
1082
                $return .= '<td>';
1083
                $value = isset($saved_categories) && isset($saved_categories[$cat_id]) ? $saved_categories[$cat_id]['count_questions'] : -1;
1084
                $return .= Display::input(
1085
                    'number',
1086
                    "category[$cat_id]",
1087
                    $value,
1088
                    ['class' => 'form-control', 'min' => -1, 'step' => 1]
1089
                );
1090
                $return .= '</td>';
1091
                $return .= '</tr>';
1092
            }
1093
1094
            $return .= '</table>';
1095
            $return .= get_lang('ZeroMeansNoQuestionWillBeSelectedMinusOneMeansThatAllQuestionsWillBeSelected');
1096
        }
1097
1098
        return $return;
1099
    }
1100
1101
    /**
1102
     * Return true if a category already exists with the same name.
1103
     *
1104
     * @param string $name
1105
     * @param int    $courseId
1106
     *
1107
     * @return bool
1108
     */
1109
    public static function categoryTitleExists($name, $courseId = 0)
1110
    {
1111
        $categories = self::getCategoryListInfo('title', $courseId);
1112
        foreach ($categories as $title) {
1113
            if ($title == $name) {
1114
                return true;
1115
            }
1116
        }
1117
1118
        return false;
1119
    }
1120
1121
    /**
1122
     * Return the id of the test category with title = $in_title.
1123
     *
1124
     * @param string $title
1125
     * @param int    $courseId
1126
     *
1127
     * @return int is id of test category
1128
     */
1129
    public static function get_category_id_for_title($title, $courseId = 0)
1130
    {
1131
        $out_res = 0;
1132
        if (empty($courseId)) {
1133
            $courseId = api_get_course_int_id();
1134
        }
1135
        $courseId = (int) $courseId;
1136
        $tbl_cat = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
1137
        $sql = "SELECT iid FROM $tbl_cat
1138
                WHERE c_id = $courseId AND title = '".Database::escape_string($title)."'";
1139
        $res = Database::query($sql);
1140
        if (Database::num_rows($res) > 0) {
1141
            $data = Database::fetch_array($res);
1142
            $out_res = $data['iid'];
1143
        }
1144
1145
        return $out_res;
1146
    }
1147
1148
    /**
1149
     * Add a relation between question and category in table c_quiz_question_rel_category.
1150
     *
1151
     * @param int $categoryId
1152
     * @param int $questionId
1153
     * @param int $courseId
1154
     *
1155
     * @return string|false
1156
     */
1157
    public static function addCategoryToQuestion($categoryId, $questionId, $courseId)
1158
    {
1159
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
1160
        // if question doesn't have a category
1161
        // @todo change for 1.10 when a question can have several categories
1162
        if (self::getCategoryForQuestion($questionId, $courseId) == 0 &&
1163
            $questionId > 0 &&
1164
            $courseId > 0
1165
        ) {
1166
            $sql = "INSERT INTO $table (c_id, question_id, category_id)
1167
                    VALUES (".intval($courseId).", ".intval($questionId).", ".intval($categoryId).")";
1168
            Database::query($sql);
1169
            $id = Database::insert_id();
1170
1171
            return $id;
1172
        }
1173
1174
        return false;
1175
    }
1176
1177
    /**
1178
     * @param int $courseId
1179
     * @param int $sessionId
1180
     *
1181
     * @return array
1182
     */
1183
    public function getCategories($courseId, $sessionId = 0)
1184
    {
1185
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
1186
        $itemProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
1187
        $sessionId = intval($sessionId);
1188
        $courseId = intval($courseId);
1189
1190
        if (empty($sessionId)) {
1191
            $sessionCondition = api_get_session_condition(
1192
                $sessionId,
1193
                true,
1194
                false,
1195
                'i.session_id'
1196
            );
1197
        } else {
1198
            $sessionCondition = api_get_session_condition(
1199
                $sessionId,
1200
                true,
1201
                true,
1202
                'i.session_id'
1203
            );
1204
        }
1205
1206
        if (empty($courseId)) {
1207
            return [];
1208
        }
1209
1210
        $sql = "SELECT cat.* FROM $table cat
1211
                INNER JOIN $itemProperty i
1212
                ON cat.c_id = i.c_id AND i.ref = cat.iid
1213
                WHERE
1214
                    cat.c_id = $courseId AND
1215
                    i.tool = '".TOOL_TEST_CATEGORY."'
1216
                    $sessionCondition
1217
                ORDER BY title ASC";
1218
        $result = Database::query($sql);
1219
1220
        return Database::store_result($result, 'ASSOC');
1221
    }
1222
1223
    /**
1224
     * @param int $courseId
1225
     * @param int $sessionId
1226
     *
1227
     * @return string
1228
     */
1229
    public function displayCategories($courseId, $sessionId = 0)
1230
    {
1231
        $sessionId = (int) $sessionId;
1232
        $categories = $this->getCategories($courseId, $sessionId);
1233
        $html = '';
1234
        foreach ($categories as $category) {
1235
            $tmpobj = new TestCategory();
1236
            $tmpobj = $tmpobj->getCategory($category['iid']);
1237
            $nb_question = $tmpobj->getCategoryQuestionsNumber();
1238
            $rowname = self::protectJSDialogQuote($category['title']);
1239
            $nb_question_label = $nb_question == 1 ? $nb_question.' '.get_lang('Question') : $nb_question.' '.get_lang('Questions');
1240
            $content = "<span style='float:right'>".$nb_question_label."</span>";
1241
            $content .= '<div class="sectioncomment">';
1242
            $content .= Security::remove_XSS($category['description']);
1243
            $content .= '</div>';
1244
            $links = '';
1245
1246
            if (!$sessionId) {
1247
                $links .= '<a href="'.api_get_self().'?action=editcategory&category_id='.$category['iid'].'&'.api_get_cidreq().'">'.
1248
                    Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_SMALL).'</a>';
1249
                $links .= ' <a href="'.api_get_self().'?'.api_get_cidreq().'&action=deletecategory&category_id='.$category['iid'].'" ';
1250
                $links .= 'onclick="return confirmDelete(\''.self::protectJSDialogQuote(get_lang('DeleteCategoryAreYouSure').'['.$rowname).'] ?\', \'id_cat'.$category['iid'].'\');">';
1251
                $links .= Display::return_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_SMALL).'</a>';
1252
            }
1253
1254
            $html .= Display::panel($content, Security::remove_XSS($category['title']).$links);
1255
        }
1256
1257
        return $html;
1258
    }
1259
1260
    /**
1261
     * To allowed " in javascript dialog box without bad surprises
1262
     * replace " with two '.
1263
     *
1264
     * @param string $text
1265
     *
1266
     * @return mixed
1267
     */
1268
    public function protectJSDialogQuote($text)
1269
    {
1270
        $res = $text;
1271
        $res = str_replace("'", "\'", $res);
1272
        // super astuce pour afficher les " dans les boite de dialogue
1273
        $res = str_replace('"', "\'\'", $res);
1274
1275
        return $res;
1276
    }
1277
}
1278