Completed
Push — master ( 4dac83...c22b6a )
by Julito
09:28 queued 12s
created

TestCategory   F

Complexity

Total Complexity 129

Size/Duplication

Total Lines 1173
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 541
dl 0
loc 1173
rs 2
c 0
b 0
f 0
wmc 129

33 Methods

Rating   Name   Duplication   Size   Complexity  
A getCategoryListInfo() 0 27 5
A save() 0 47 5
B getCategoryExerciseTree() 0 47 10
A getNumberMaxQuestionByCat() 0 21 4
B returnCategoryForm() 0 58 6
A getListOfCategoriesIDForTestObject() 0 23 5
A getCategoryQuestionsNumber() 0 11 1
A getCategory() 0 20 3
A getForm() 0 70 4
A getNumberOfQuestionsInCategoryForTest() 0 14 3
A getListOfCategoriesForTest() 0 13 2
A getCategoryNameForQuestion() 0 19 3
C getQuestionsByCat() 0 76 14
A getListOfCategoriesIDForTest() 0 15 3
A protectJSDialogQuote() 0 8 1
A getCategories() 0 18 3
A addCategoryToQuestion() 0 17 4
A returnCategoryAndTitle() 0 17 6
A displayCategories() 0 29 4
A modifyCategory() 0 24 4
A getCategoryForQuestion() 0 25 5
A deleteResource() 0 16 2
A __construct() 0 4 1
A sortTabByBracketLabel() 0 18 3
A get_category_id_for_title() 0 17 3
A getListOfCategoriesNameForTest() 0 18 3
A getNElementsFromArray() 0 13 3
A removeCategory() 0 21 2
A categoryTitleExists() 0 9 1
A getCategoriesIdAndName() 0 12 3
A getNumberOfQuestionRandomByCategory() 0 22 4
B get_stats_table_by_attempt() 0 104 8
A displayCategoryAndTitle() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like TestCategory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TestCategory, and based on these observations, apply Extract Interface, too.

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 $id;
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 id=in_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 id = $id AND c_id = ".$courseId;
46
        $res = Database::query($sql);
47
48
        if (Database::num_rows($res)) {
49
            $row = Database::fetch_array($res);
50
51
            $this->id = $row['id'];
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
            $repo = Container::getQuestionCategoryRepository();
86
            $category = new CQuizQuestionCategory();
87
            $category
88
                ->setTitle($this->name)
89
                ->setCourse($courseInfo['entity'])
90
                ->setDescription($this->description);
91
            $em = $repo->getEntityManager();
92
            $em->persist($category);
93
94
            if ($category) {
0 ignored issues
show
introduced by
$category is of type CQuizQuestionCategory, thus it always evaluated to true.
Loading history...
95
                $repo->addResourceToCourse(
96
                    $category,
97
                    ResourceLink::VISIBILITY_PUBLISHED,
98
                    api_get_user_entity(api_get_user_id()),
99
                    $courseInfo['entity'],
100
                    api_get_session_entity(),
101
                    api_get_group_entity()
102
                );
103
104
                $repo->getEntityManager()->flush();
105
106
                $newId = $category->getIid();
107
                $sql = "UPDATE $table SET id = iid WHERE iid = $newId";
108
                Database::query($sql);
109
110
                return $newId;
111
            }
112
        }
113
114
        return false;
115
    }
116
117
    /**
118
     * Removes the category from the database
119
     * if there were question in this category, the link between question and category is removed.
120
     *
121
     * @param int $id
122
     *
123
     * @return bool
124
     */
125
    public function removeCategory($id)
126
    {
127
        $tbl_question_rel_cat = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
128
        $id = (int) $id;
129
        $course_id = api_get_course_int_id();
130
        $category = $this->getCategory($id, $course_id);
131
132
        if ($category) {
0 ignored issues
show
introduced by
$category is of type TestCategory, thus it always evaluated to true.
Loading history...
133
            // remove link between question and category
134
            $sql = "DELETE FROM $tbl_question_rel_cat
135
                    WHERE category_id = $id AND c_id=".$course_id;
136
            Database::query($sql);
137
138
            $repo = Container::getQuestionCategoryRepository();
139
            $category = $repo->find($id);
140
            $repo->hardDelete($category);
141
142
            return true;
143
        }
144
145
        return false;
146
    }
147
148
    /**
149
     * @param                                                   $primaryKeys
150
     * @param                                                   $allPrimaryKeys
151
     * @param \Symfony\Component\HttpFoundation\Session\Session $session
152
     * @param                                                   $parameters
153
     */
154
    public function deleteResource(
155
        $primaryKeys,
156
        $allPrimaryKeys,
157
        Symfony\Component\HttpFoundation\Session\Session $session,
158
        $parameters
159
    ) {
160
        $repo = Container::getQuestionCategoryRepository();
161
        $translator = Container::getTranslator();
162
        foreach ($primaryKeys as $id) {
163
            $category = $repo->find($id);
164
            $repo->hardDelete($category);
165
        }
166
167
        Display::addFlash(Display::return_message($translator->trans('Deleted')));
168
        header('Location:'.api_get_self().'?'.api_get_cidreq());
169
        exit;
170
    }
171
172
    /**
173
     * Modify category name or description of category with id=in_id.
174
     *
175
     * @param int $courseId
176
     *
177
     * @return bool
178
     */
179
    public function modifyCategory($courseId = 0)
180
    {
181
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
182
        $courseInfo = api_get_course_info_by_id($courseId);
183
        if (empty($courseInfo)) {
184
            return false;
185
        }
186
187
        $repo = Container::getQuestionCategoryRepository();
188
        /** @var CQuizQuestionCategory $category */
189
        $category = $repo->find($this->id);
190
        if ($category) {
0 ignored issues
show
introduced by
$category is of type CQuizQuestionCategory, thus it always evaluated to true.
Loading history...
191
            $category
192
                ->setTitle($this->name)
193
                ->setDescription($this->description)
194
            ;
195
196
            $repo->getEntityManager()->persist($category);
197
            $repo->getEntityManager()->flush();
198
199
            return true;
200
        }
201
202
        return false;
203
    }
204
205
    /**
206
     * Gets the number of question of category id=in_id.
207
     */
208
    public function getCategoryQuestionsNumber()
209
    {
210
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
211
        $id = (int) $this->id;
212
        $sql = "SELECT count(*) AS nb
213
                FROM $table
214
                WHERE category_id = $id AND c_id=".api_get_course_int_id();
215
        $res = Database::query($sql);
216
        $row = Database::fetch_array($res);
217
218
        return $row['nb'];
219
    }
220
221
    /**
222
     * Return an array of all Category objects in the database
223
     * If $field=="" Return an array of all category objects in the database
224
     * Otherwise, return an array of all in_field value
225
     * in the database (in_field = id or name or description).
226
     *
227
     * @param string $field
228
     * @param int    $courseId
229
     *
230
     * @return array
231
     */
232
    public static function getCategoryListInfo($field = '', $courseId = 0)
233
    {
234
        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
235
236
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
237
        $categories = [];
238
        if (empty($field)) {
239
            $sql = "SELECT id FROM $table
240
                    WHERE c_id = $courseId
241
                    ORDER BY title ASC";
242
            $res = Database::query($sql);
243
            while ($row = Database::fetch_array($res)) {
244
                $category = new TestCategory();
245
                $categories[] = $category->getCategory($row['id'], $courseId);
246
            }
247
        } else {
248
            $field = Database::escape_string($field);
249
            $sql = "SELECT $field FROM $table
250
                    WHERE c_id = $courseId
251
                    ORDER BY $field ASC";
252
            $res = Database::query($sql);
253
            while ($row = Database::fetch_array($res)) {
254
                $categories[] = $row[$field];
255
            }
256
        }
257
258
        return $categories;
259
    }
260
261
    /**
262
     * Return the TestCategory id for question with question_id = $questionId
263
     * In this version, a question has only 1 TestCategory.
264
     * Return the TestCategory id, 0 if none.
265
     *
266
     * @param int $questionId
267
     * @param int $courseId
268
     *
269
     * @return int
270
     */
271
    public static function getCategoryForQuestion($questionId, $courseId = 0)
272
    {
273
        $courseId = (int) $courseId;
274
        $questionId = (int) $questionId;
275
276
        if (empty($courseId)) {
277
            $courseId = api_get_course_int_id();
278
        }
279
280
        if (empty($courseId) || empty($questionId)) {
281
            return 0;
282
        }
283
284
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
285
        $sql = "SELECT category_id
286
                FROM $table
287
                WHERE question_id = $questionId AND c_id = $courseId";
288
        $res = Database::query($sql);
289
        $result = 0;
290
        if (Database::num_rows($res) > 0) {
291
            $data = Database::fetch_array($res);
292
            $result = (int) $data['category_id'];
293
        }
294
295
        return $result;
296
    }
297
298
    /**
299
     * Return the category name for question with question_id = $questionId
300
     * In this version, a question has only 1 category.
301
     *
302
     * @param $questionId
303
     * @param int $courseId
304
     *
305
     * @return string
306
     */
307
    public static function getCategoryNameForQuestion($questionId, $courseId = 0)
308
    {
309
        if (empty($courseId)) {
310
            $courseId = api_get_course_int_id();
311
        }
312
        $courseId = (int) $courseId;
313
        $categoryId = self::getCategoryForQuestion($questionId, $courseId);
314
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
315
        $sql = "SELECT title
316
                FROM $table
317
                WHERE id = $categoryId AND c_id = $courseId";
318
        $res = Database::query($sql);
319
        $data = Database::fetch_array($res);
320
        $result = '';
321
        if (Database::num_rows($res) > 0) {
322
            $result = $data['title'];
323
        }
324
325
        return $result;
326
    }
327
328
    /**
329
     * Return the list of differents categories ID for a test in the current course
330
     * input : test_id
331
     * return : array of category id (integer)
332
     * hubert.borderiou 07-04-2011.
333
     *
334
     * @param int $exerciseId
335
     * @param int $courseId
336
     *
337
     * @return array
338
     */
339
    public static function getListOfCategoriesIDForTest($exerciseId, $courseId = 0)
340
    {
341
        // parcourir les questions d'un test, recup les categories uniques dans un tableau
342
        $exercise = new Exercise($courseId);
343
        $exercise->read($exerciseId, false);
344
        $categoriesInExercise = $exercise->getQuestionWithCategories();
345
        // the array given by selectQuestionList start at indice 1 and not at indice 0 !!! ???
346
        $categories = [];
347
        if (!empty($categoriesInExercise)) {
348
            foreach ($categoriesInExercise as $category) {
349
                $categories[$category['id']] = $category;
350
            }
351
        }
352
353
        return $categories;
354
    }
355
356
    /**
357
     * @return array
358
     */
359
    public static function getListOfCategoriesIDForTestObject(Exercise $exercise)
360
    {
361
        // parcourir les questions d'un test, recup les categories uniques dans un tableau
362
        $categories_in_exercise = [];
363
        $question_list = $exercise->getQuestionOrderedListByName();
364
365
        // the array given by selectQuestionList start at indice 1 and not at indice 0 !!! ???
366
        foreach ($question_list as $questionInfo) {
367
            $question_id = $questionInfo['question_id'];
368
            $category_list = self::getCategoryForQuestion($question_id);
369
            if (is_numeric($category_list)) {
370
                $category_list = [$category_list];
371
            }
372
373
            if (!empty($category_list)) {
374
                $categories_in_exercise = array_merge($categories_in_exercise, $category_list);
375
            }
376
        }
377
        if (!empty($categories_in_exercise)) {
378
            $categories_in_exercise = array_unique(array_filter($categories_in_exercise));
379
        }
380
381
        return $categories_in_exercise;
382
    }
383
384
    /**
385
     * Return the list of different categories NAME for a test.
386
     *
387
     * @param int  $exerciseId
388
     * @param bool $grouped_by_category
389
     *
390
     * @return array
391
     *
392
     * @author function rewrote by jmontoya
393
     */
394
    public static function getListOfCategoriesNameForTest($exerciseId, $grouped_by_category = true)
395
    {
396
        $result = [];
397
        $categories = self::getListOfCategoriesIDForTest($exerciseId);
398
399
        foreach ($categories as $catInfo) {
400
            $categoryId = $catInfo['id'];
401
            if (!empty($categoryId)) {
402
                $result[$categoryId] = [
403
                    'title' => $catInfo['title'],
404
                    //'parent_id' =>  $catInfo['parent_id'],
405
                    'parent_id' => '',
406
                    'c_id' => $catInfo['c_id'],
407
                ];
408
            }
409
        }
410
411
        return $result;
412
    }
413
414
    /**
415
     * @return array
416
     */
417
    public static function getListOfCategoriesForTest(Exercise $exercise)
418
    {
419
        $result = [];
420
        $categories = self::getListOfCategoriesIDForTestObject($exercise);
421
        foreach ($categories as $cat_id) {
422
            $cat = new self();
423
            $cat = (array) $cat->getCategory($cat_id);
424
            $cat['iid'] = $cat['id'];
425
            $cat['title'] = $cat['name'];
426
            $result[$cat['id']] = $cat;
427
        }
428
429
        return $result;
430
    }
431
432
    /**
433
     * return the number of question of a category id in a test.
434
     *
435
     * @param int $exerciseId
436
     * @param int $categoryId
437
     *
438
     * @return int
439
     *
440
     * @author hubert.borderiou 07-04-2011
441
     */
442
    public static function getNumberOfQuestionsInCategoryForTest($exerciseId, $categoryId)
443
    {
444
        $nbCatResult = 0;
445
        $quiz = new Exercise();
446
        $quiz->read($exerciseId);
447
        $questionList = $quiz->selectQuestionList();
448
        // the array given by selectQuestionList start at indice 1 and not at indice 0 !!! ? ? ?
449
        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...
450
            if (self::getCategoryForQuestion($questionList[$i]) == $categoryId) {
451
                $nbCatResult++;
452
            }
453
        }
454
455
        return $nbCatResult;
456
    }
457
458
    /**
459
     * return the number of question for a test using random by category
460
     * input  : test_id, number of random question (min 1).
461
     *
462
     * @param int $exerciseId
463
     * @param int $random
464
     *
465
     * @return int
466
     *             hubert.borderiou 07-04-2011
467
     *             question without categories are not counted
468
     */
469
    public static function getNumberOfQuestionRandomByCategory($exerciseId, $random)
470
    {
471
        $count = 0;
472
        $categories = self::getListOfCategoriesIDForTest($exerciseId);
473
        foreach ($categories as $category) {
474
            if (empty($category['id'])) {
475
                continue;
476
            }
477
478
            $nbQuestionInThisCat = self::getNumberOfQuestionsInCategoryForTest(
479
                $exerciseId,
480
                $category['id']
481
            );
482
483
            if ($nbQuestionInThisCat > $random) {
484
                $count += $random;
485
            } else {
486
                $count += $nbQuestionInThisCat;
487
            }
488
        }
489
490
        return $count;
491
    }
492
493
    /**
494
     * Return an array (id=>name)
495
     * array[0] = get_lang('NoCategory');.
496
     *
497
     * @param int $courseId
498
     *
499
     * @return array
500
     */
501
    public static function getCategoriesIdAndName($courseId = 0)
502
    {
503
        if (empty($courseId)) {
504
        $courseId = api_get_course_int_id();
505
        }
506
        $categories = self::getCategoryListInfo('', $courseId);
507
        $result = ['0' => get_lang('NoCategorySelected')];
508
        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...
509
            $result[$categories[$i]->id] = $categories[$i]->name;
510
        }
511
512
        return $result;
513
    }
514
515
    /**
516
     * Returns an array of question ids for each category
517
     * $categories[1][30] = 10, array with category id = 1 and question_id = 10
518
     * A question has "n" categories.
519
     *
520
     * @param int   $exerciseId
521
     * @param array $check_in_question_list
522
     * @param array $categoriesAddedInExercise
523
     *
524
     * @return array
525
     */
526
    public static function getQuestionsByCat(
527
        $exerciseId,
528
        $check_in_question_list = [],
529
        $categoriesAddedInExercise = []
530
    ) {
531
        $tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
532
        $TBL_EXERCICE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
533
        $TBL_QUESTION_REL_CATEGORY = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
534
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
535
        $exerciseId = (int) $exerciseId;
536
        $courseId = api_get_course_int_id();
537
538
        $sql = "SELECT DISTINCT qrc.question_id, qrc.category_id
539
                FROM $TBL_QUESTION_REL_CATEGORY qrc
540
                INNER JOIN $TBL_EXERCICE_QUESTION eq
541
                ON (eq.question_id = qrc.question_id AND qrc.c_id = eq.c_id)
542
                INNER JOIN $categoryTable c
543
                ON (c.id = qrc.category_id AND c.c_id = eq.c_id)
544
                INNER JOIN $tableQuestion q
545
                ON (q.id = qrc.question_id AND q.c_id = eq.c_id)
546
                WHERE
547
                    exercice_id = $exerciseId AND
548
                    qrc.c_id = $courseId
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
584
                    break;
585
                }
586
            }
587
588
            // Select questions that don't have any category related
589
            if ($checkQuestionsWithNoCategory) {
590
                $originalQuestionList = $check_in_question_list;
591
                foreach ($originalQuestionList as $questionId) {
592
                    $categoriesFlatten = array_flatten($categories);
593
                    if (!in_array($questionId, $categoriesFlatten)) {
594
                        $newCategoryList[0][] = $questionId;
595
                    }
596
                }
597
            }
598
            $categories = $newCategoryList;
599
        }
600
601
        return $categories;
602
    }
603
604
    /**
605
     * Returns an array of $numberElements from $array.
606
     *
607
     * @param array $array
608
     * @param int   $numberElements
609
     * @param bool  $shuffle
610
     *
611
     * @return array
612
     */
613
    public static function getNElementsFromArray($array, $numberElements, $shuffle = true)
614
    {
615
        $list = $array;
616
617
        if ($shuffle) {
618
          shuffle($list);
619
        }
620
621
        if ($numberElements < count($list)) {
622
            $list = array_slice($list, 0, $numberElements);
623
        }
624
625
        return $list;
626
    }
627
628
    /**
629
     * @param int $questionId
630
     * @param int $displayCategoryName
631
     */
632
    public static function displayCategoryAndTitle($questionId, $displayCategoryName = 1)
633
    {
634
        echo self::returnCategoryAndTitle($questionId, $displayCategoryName);
635
    }
636
637
    /**
638
     * @param int $questionId
639
     * @param int $in_display_category_name
640
     *
641
     * @return string|null
642
     */
643
    public static function returnCategoryAndTitle($questionId, $in_display_category_name = 1)
644
    {
645
        $is_student = !(api_is_allowed_to_edit(null, true) || api_is_session_admin());
646
        $objExercise = Session::read('objExercise');
647
        if (!empty($objExercise)) {
648
            $in_display_category_name = $objExercise->display_category_name;
649
        }
650
        $content = null;
651
        if ('' != self::getCategoryNameForQuestion($questionId) &&
652
            (1 == $in_display_category_name || !$is_student)
653
        ) {
654
            $content .= '<div class="page-header">';
655
            $content .= '<h4>'.get_lang('Category').': '.self::getCategoryNameForQuestion($questionId).'</h4>';
656
            $content .= '</div>';
657
        }
658
659
        return $content;
660
    }
661
662
    /**
663
     * sortTabByBracketLabel ($tabCategoryQuestions)
664
     * key of $tabCategoryQuestions are the category id (0 for not in a category)
665
     * value is the array of question id of this category
666
     * Sort question by Category.
667
     */
668
    public static function sortTabByBracketLabel($in_tab)
669
    {
670
        $tabResult = [];
671
        $tabCatName = []; // tab of category name
672
        foreach ($in_tab as $cat_id => $tabquestion) {
673
            $category = new self();
674
            $category = $category->getCategory($cat_id);
675
            $tabCatName[$cat_id] = $category->name;
676
        }
677
        reset($in_tab);
678
        // sort table by value, keeping keys as they are
679
        asort($tabCatName);
680
        // keys of $tabCatName are keys order for $in_tab
681
        foreach ($tabCatName as $key => $val) {
682
            $tabResult[$key] = $in_tab[$key];
683
        }
684
685
        return $tabResult;
686
    }
687
688
    /**
689
     * Return the number max of question in a category
690
     * count the number of questions in all categories, and return the max.
691
     *
692
     * @param int $exerciseId
693
     *
694
     * @author - hubert borderiou
695
     *
696
     * @return int
697
     */
698
    public static function getNumberMaxQuestionByCat($exerciseId)
699
    {
700
        $res_num_max = 0;
701
        // foreach question
702
        $categories = self::getListOfCategoriesIDForTest($exerciseId);
703
        foreach ($categories as $category) {
704
            if (empty($category['id'])) {
705
                continue;
706
            }
707
708
            $nbQuestionInThisCat = self::getNumberOfQuestionsInCategoryForTest(
709
                $exerciseId,
710
                $category['id']
711
            );
712
713
            if ($nbQuestionInThisCat > $res_num_max) {
714
                $res_num_max = $nbQuestionInThisCat;
715
            }
716
        }
717
718
        return $res_num_max;
719
    }
720
721
    /**
722
     * Returns a category summary report.
723
     *
724
     * @param int   $exerciseId
725
     * @param array $category_list
726
     *                             pre filled array with the category_id, score, and weight
727
     *                             example: array(1 => array('score' => '10', 'total' => 20));
728
     *
729
     * @return string
730
     */
731
    public static function get_stats_table_by_attempt(
732
        $exerciseId,
733
        $category_list = []
734
    ) {
735
        if (empty($category_list)) {
736
            return null;
737
        }
738
        $category_name_list = self::getListOfCategoriesNameForTest($exerciseId);
739
740
        $table = new HTML_Table(['class' => 'table table-bordered', 'id' => 'category_results']);
741
        $table->setHeaderContents(0, 0, get_lang('Categories'));
742
        $table->setHeaderContents(0, 1, get_lang('Absolute score'));
743
        $table->setHeaderContents(0, 2, get_lang('Relative score'));
744
        $row = 1;
745
746
        $none_category = [];
747
        if (isset($category_list['none'])) {
748
            $none_category = $category_list['none'];
749
            unset($category_list['none']);
750
        }
751
752
        $total = [];
753
        if (isset($category_list['total'])) {
754
            $total = $category_list['total'];
755
            unset($category_list['total']);
756
        }
757
        if (count($category_list) > 1) {
758
            foreach ($category_list as $category_id => $category_item) {
759
                $table->setCellContents($row, 0, $category_name_list[$category_id]);
760
                $table->setCellContents(
761
                    $row,
762
                    1,
763
                    ExerciseLib::show_score(
764
                        $category_item['score'],
765
                        $category_item['total'],
766
                        false
767
                    )
768
                );
769
                $table->setCellContents(
770
                    $row,
771
                    2,
772
                    ExerciseLib::show_score(
773
                        $category_item['score'],
774
                        $category_item['total'],
775
                        true,
776
                        false,
777
                        true
778
                    )
779
                );
780
                $row++;
781
            }
782
783
            if (!empty($none_category)) {
784
                $table->setCellContents($row, 0, get_lang('none'));
785
                $table->setCellContents(
786
                    $row,
787
                    1,
788
                    ExerciseLib::show_score(
789
                        $none_category['score'],
790
                        $none_category['total'],
791
                        false
792
                    )
793
                );
794
                $table->setCellContents(
795
                    $row,
796
                    2,
797
                    ExerciseLib::show_score(
798
                        $none_category['score'],
799
                        $none_category['total'],
800
                        true,
801
                        false,
802
                        true
803
                    )
804
                );
805
                $row++;
806
            }
807
            if (!empty($total)) {
808
                $table->setCellContents($row, 0, get_lang('Total'));
809
                $table->setCellContents(
810
                    $row,
811
                    1,
812
                    ExerciseLib::show_score(
813
                        $total['score'],
814
                        $total['total'],
815
                        false
816
                    )
817
                );
818
                $table->setCellContents(
819
                    $row,
820
                    2,
821
                    ExerciseLib::show_score(
822
                        $total['score'],
823
                        $total['total'],
824
                        true,
825
                        false,
826
                        true
827
                    )
828
                );
829
            }
830
831
            return $table->toHtml();
832
        }
833
834
        return '';
835
    }
836
837
    /**
838
     * @param Exercise $exercise
839
     * @param int      $courseId
840
     * @param string   $order
841
     * @param bool     $shuffle
842
     * @param bool     $excludeCategoryWithNoQuestions
843
     *
844
     * @return array
845
     */
846
    public function getCategoryExerciseTree(
847
        $exercise,
848
        $courseId,
849
        $order = null,
850
        $shuffle = false,
851
        $excludeCategoryWithNoQuestions = true
852
    ) {
853
        if (empty($exercise)) {
854
            return [];
855
        }
856
857
        $courseId = (int) $courseId;
858
        $table = Database::get_course_table(TABLE_QUIZ_REL_CATEGORY);
859
        $categoryTable = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
860
        $exercise->id = (int) $exercise->id;
861
862
        $sql = "SELECT * FROM $table qc
863
                LEFT JOIN $categoryTable c
864
                ON (qc.c_id = c.c_id AND c.id = qc.category_id)
865
                WHERE qc.c_id = $courseId AND exercise_id = {$exercise->id} ";
866
867
        if (!empty($order)) {
868
            $order = Database::escape_string($order);
869
            $sql .= "ORDER BY $order";
870
        }
871
872
        $categories = [];
873
        $result = Database::query($sql);
874
        if (Database::num_rows($result)) {
875
            while ($row = Database::fetch_array($result, 'ASSOC')) {
876
                if ($excludeCategoryWithNoQuestions) {
877
                    if (0 == $row['count_questions']) {
878
                        continue;
879
                    }
880
                }
881
                if (empty($row['title']) && empty($row['category_id'])) {
882
                    $row['title'] = get_lang('General');
883
                }
884
                $categories[$row['category_id']] = $row;
885
            }
886
        }
887
888
        if ($shuffle) {
889
            shuffle_assoc($categories);
890
        }
891
892
        return $categories;
893
    }
894
895
    /**
896
     * @param FormValidator $form
897
     * @param string        $action
898
     */
899
    public function getForm(&$form, $action = 'new')
900
    {
901
        switch ($action) {
902
            case 'new':
903
                $header = get_lang('Add category');
904
                $submit = get_lang('Add test category');
905
906
                break;
907
            case 'edit':
908
                $header = get_lang('Edit this category');
909
                $submit = get_lang('Edit category');
910
911
                break;
912
        }
913
914
        // Setting the form elements
915
        $form->addElement('header', $header);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $header does not seem to be defined for all execution paths leading up to this point.
Loading history...
916
        $form->addElement('hidden', 'category_id');
917
        $form->addElement(
918
            'text',
919
            'category_name',
920
            get_lang('Category name'),
921
            ['class' => 'span6']
922
        );
923
        $form->add_html_editor(
924
            'category_description',
925
            get_lang('Category description'),
926
            false,
927
            false,
928
            [
929
                'ToolbarSet' => 'test_category',
930
                'Width' => '90%',
931
                'Height' => '200',
932
            ]
933
        );
934
        $category_parent_list = [];
935
936
        $options = [
937
                '1' => get_lang('Visible'),
938
                '0' => get_lang('Hidden'),
939
        ];
940
        $form->addElement(
941
            'select',
942
            'visibility',
943
            get_lang('Visibility'),
944
            $options
945
        );
946
        $script = null;
947
        if (!empty($this->parent_id)) {
948
            $parent_cat = new self();
949
            $parent_cat = $parent_cat->getCategory($this->parent_id);
0 ignored issues
show
Bug Best Practice introduced by
The property parent_id does not exist on TestCategory. Did you maybe forget to declare it?
Loading history...
950
            $category_parent_list = [$parent_cat->id => $parent_cat->name];
951
            $script .= '<script>$(function() { $("#parent_id").trigger("addItem",[{"title": "'.$parent_cat->name.'", "value": "'.$parent_cat->id.'"}]); });</script>';
952
        }
953
        $form->addElement('html', $script);
954
955
        $form->addElement('select', 'parent_id', get_lang('Parent'), $category_parent_list, ['id' => 'parent_id']);
956
        $form->addElement('style_submit_button', 'SubmitNote', $submit, 'class="add"');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $submit does not seem to be defined for all execution paths leading up to this point.
Loading history...
957
958
        // setting the defaults
959
        $defaults = [];
960
        $defaults['category_id'] = $this->id;
961
        $defaults['category_name'] = $this->name;
962
        $defaults['category_description'] = $this->description;
963
        $defaults['parent_id'] = $this->parent_id;
964
        $defaults['visibility'] = $this->visibility;
0 ignored issues
show
Bug Best Practice introduced by
The property visibility does not exist on TestCategory. Did you maybe forget to declare it?
Loading history...
965
        $form->setDefaults($defaults);
966
967
        // setting the rules
968
        $form->addRule('category_name', get_lang('Required field'), 'required');
969
    }
970
971
    /**
972
     * Returns the category form.
973
     *
974
     * @return string
975
     */
976
    public function returnCategoryForm(Exercise $exercise)
977
    {
978
        $categories = $this->getListOfCategoriesForTest($exercise);
979
        $saved_categories = $exercise->getCategoriesInExercise();
980
        $return = null;
981
982
        if (!empty($categories)) {
983
            $nbQuestionsTotal = $exercise->getNumberQuestionExerciseCategory();
984
            $exercise->setCategoriesGrouping(true);
985
            $real_question_count = count($exercise->getQuestionList());
986
987
            $warning = null;
988
            if ($nbQuestionsTotal != $real_question_count) {
989
                $warning = Display::return_message(
990
                    get_lang('Make sure you have enough questions in your categories.'),
991
                    'warning'
992
                );
993
            }
994
995
            $return .= $warning;
996
            $return .= '<table class="data_table">';
997
            $return .= '<tr>';
998
            $return .= '<th height="24">'.get_lang('Categories').'</th>';
999
            $return .= '<th width="70" height="24">'.get_lang('N°').'</th></tr>';
1000
1001
            $emptyCategory = [
1002
                'id' => '0',
1003
                'name' => get_lang('General'),
1004
                'description' => '',
1005
                'iid' => '0',
1006
                'title' => get_lang('General'),
1007
            ];
1008
1009
            $categories[] = $emptyCategory;
1010
1011
            foreach ($categories as $category) {
1012
                $cat_id = $category['iid'];
1013
                $return .= '<tr>';
1014
                $return .= '<td>';
1015
                $return .= Display::div($category['name']);
1016
                $return .= '</td>';
1017
                $return .= '<td>';
1018
                $value = isset($saved_categories) && isset($saved_categories[$cat_id]) ? $saved_categories[$cat_id]['count_questions'] : -1;
1019
                $return .= Display::input(
1020
                    'number',
1021
                    "category[$cat_id]",
1022
                    $value,
1023
                    ['class' => 'form-control', 'min' => -1, 'step' => 1]
1024
                );
1025
                $return .= '</td>';
1026
                $return .= '</tr>';
1027
            }
1028
1029
            $return .= '</table>';
1030
            $return .= get_lang('-1 = All questions will be selected.');
1031
        }
1032
1033
        return $return;
1034
    }
1035
1036
    /**
1037
     * Return true if a category already exists with the same name.
1038
     *
1039
     * @param string $name
1040
     * @param int    $courseId
1041
     *
1042
     * @return bool
1043
     */
1044
    public static function categoryTitleExists($name, $courseId)
1045
    {
1046
        $repo = Container::getQuestionCategoryRepository();
1047
        $criteria = [
1048
            'title' => $name,
1049
            'course' => $courseId,
1050
        ];
1051
1052
        return $repo->getRepository()->count($criteria) > 0;
1053
    }
1054
1055
    /**
1056
     * Return the id of the test category with title = $in_title.
1057
     *
1058
     * @param string $title
1059
     * @param int    $courseId
1060
     *
1061
     * @return int is id of test category
1062
     */
1063
    public static function get_category_id_for_title($title, $courseId = 0)
1064
    {
1065
        $out_res = 0;
1066
        if (empty($courseId)) {
1067
            $courseId = api_get_course_int_id();
1068
        }
1069
        $courseId = (int) $courseId;
1070
        $tbl_cat = Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY);
1071
        $sql = "SELECT id FROM $tbl_cat
1072
                WHERE c_id = $courseId AND title = '".Database::escape_string($title)."'";
1073
        $res = Database::query($sql);
1074
        if (Database::num_rows($res) > 0) {
1075
            $data = Database::fetch_array($res);
1076
            $out_res = $data['id'];
1077
        }
1078
1079
        return $out_res;
1080
    }
1081
1082
    /**
1083
     * Add a relation between question and category in table c_quiz_question_rel_category.
1084
     *
1085
     * @param int $categoryId
1086
     * @param int $questionId
1087
     * @param int $courseId
1088
     *
1089
     * @return string|false
1090
     */
1091
    public static function addCategoryToQuestion($categoryId, $questionId, $courseId)
1092
    {
1093
        $table = Database::get_course_table(TABLE_QUIZ_QUESTION_REL_CATEGORY);
1094
        // if question doesn't have a category
1095
        // @todo change for 1.10 when a question can have several categories
1096
        if (0 == self::getCategoryForQuestion($questionId, $courseId) &&
1097
            $questionId > 0 &&
1098
            $courseId > 0
1099
        ) {
1100
            $sql = "INSERT INTO $table (c_id, question_id, category_id)
1101
                    VALUES (".(int) $courseId.', '.(int) $questionId.', '.(int) $categoryId.')';
1102
            Database::query($sql);
1103
1104
            return Database::insert_id();
1105
        }
1106
1107
        return false;
1108
    }
1109
1110
    /**
1111
     * @param int $courseId
1112
     * @param int $sessionId
1113
     *
1114
     * @return array
1115
     */
1116
    public static function getCategories($courseId, $sessionId = 0)
1117
    {
1118
        if (empty($courseId)) {
1119
            return [];
1120
        }
1121
1122
        $sessionId = (int) $sessionId;
1123
        $courseId = (int) $courseId;
1124
        $sessionEntity = null;
1125
        if (!empty($sessionId)) {
1126
            $sessionEntity = api_get_session_entity($sessionId);
1127
        }
1128
1129
        $courseEntity = api_get_course_entity($courseId);
1130
        $repo = Container::getQuestionCategoryRepository();
1131
        $resources = $repo->getResourcesByCourse($courseEntity, $sessionEntity);
1132
1133
        return $resources->getQuery()->getResult();
1134
    }
1135
1136
    /**
1137
     * @param int $courseId
1138
     * @param int $sessionId
1139
     *
1140
     * @return string
1141
     */
1142
    public function displayCategories($courseId, $sessionId = 0)
1143
    {
1144
        $sessionId = (int) $sessionId;
1145
        $categories = $this->getCategories($courseId, $sessionId);
1146
        $html = '';
1147
        foreach ($categories as $category) {
1148
            $tmpobj = new TestCategory();
1149
            $tmpobj = $tmpobj->getCategory($category['id']);
1150
            $nb_question = $tmpobj->getCategoryQuestionsNumber();
1151
            $rowname = self::protectJSDialogQuote($category['title']);
0 ignored issues
show
Bug Best Practice introduced by
The method TestCategory::protectJSDialogQuote() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1151
            /** @scrutinizer ignore-call */ 
1152
            $rowname = self::protectJSDialogQuote($category['title']);
Loading history...
1152
            $nb_question_label = $nb_question == 1 ? $nb_question.' '.get_lang('Question') : $nb_question.' '.get_lang('Questions');
1153
            $content = "<span style='float:right'>".$nb_question_label."</span>";
1154
            $content .= '<div class="sectioncomment">';
1155
            $content .= $category['description'];
1156
            $content .= '</div>';
1157
            $links = '';
1158
1159
            if (!$sessionId) {
1160
                $links .= '<a href="'.api_get_self().'?action=editcategory&category_id='.$category['id'].'&'.api_get_cidreq().'">'.
1161
                    Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_SMALL).'</a>';
1162
                $links .= ' <a href="'.api_get_self().'?'.api_get_cidreq().'&action=deletecategory&category_id='.$category['id'].'" ';
1163
                $links .= 'onclick="return confirmDelete(\''.self::protectJSDialogQuote(get_lang('DeleteCategoryAreYouSure').'['.$rowname).'] ?\', \'id_cat'.$category['id'].'\');">';
1164
                $links .= Display::return_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_SMALL).'</a>';
1165
                }
1166
1167
            $html .= Display::panel($content, $category['title'].$links);
1168
                }
1169
1170
        return $html;
1171
        }
1172
1173
    /**
1174
     * To allowed " in javascript dialog box without bad surprises
1175
     * replace " with two '.
1176
     *
1177
     * @param string $text
1178
     *
1179
     * @return mixed
1180
     */
1181
    public function protectJSDialogQuote($text)
1182
    {
1183
        $res = $text;
1184
        $res = str_replace("'", "\'", $res);
1185
        // 8. Set route and request
1186
        $res = str_replace('"', "\'\'", $res);
1187
1188
        return $res;
1189
    }
1190
}
1191