Passed
Pull Request — master (#7211)
by
unknown
08:41
created

Category   F

Complexity

Total Complexity 405

Size/Duplication

Total Lines 2808
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 1268
c 0
b 0
f 0
dl 0
loc 2808
rs 0.8
wmc 405

98 Methods

Rating   Name   Duplication   Size   Complexity  
A get_description() 0 3 1
A get_user_id() 0 3 1
A is_visible() 0 3 1
A getCertificateMinScore() 0 7 2
A set_description() 0 3 1
A getIsRequirement() 0 3 1
A get_course_code() 0 3 1
A get_parent_id() 0 3 1
A set_user_id() 0 3 1
A get_id() 0 3 1
A get_weight() 0 3 1
A set_id() 0 3 1
A is_locked() 0 3 3
A get_name() 0 3 1
A set_name() 0 3 1
A lock() 0 6 1
A find_category() 0 10 3
A setCourseListDependency() 0 8 2
A hasEvaluationsWithStudentResults() 0 25 5
B get_target_categories() 0 49 8
A get_icon_name() 0 3 1
B loadSessionCategories() 0 30 7
C get_evaluations() 0 94 15
B get_subcategories() 0 84 10
A get_type() 0 3 1
A setGenerateCertificates() 0 3 1
A set_locked() 0 3 1
F calc_score() 0 291 67
A get_item_type() 0 3 1
A get_date() 0 3 1
A get_tree() 0 43 5
A get_all_courses() 0 18 3
A __construct() 0 18 1
A setMinimumToValidate() 0 3 1
A set_parent_id() 0 3 1
A set_skills() 0 3 1
A getCategories() 0 11 1
A getMinimumToValidate() 0 3 1
A get_skills() 0 11 2
A get_not_created_course_categories() 0 32 3
A set_grade_model_id() 0 3 1
A getCourseListDependency() 0 3 1
A set_weight() 0 3 1
A is_movable() 0 3 3
A set_session_id() 0 3 1
A getGenerateCertificates() 0 3 1
B apply_visibility_to_children() 0 30 7
B get_links() 0 58 11
A move_to_cat() 0 8 2
B delete_all() 0 44 7
B lockAllItems() 0 23 7
A set_visible() 0 3 1
B get_root_categories_for_student() 0 76 10
A get_grade_model_id() 0 7 2
A get_session_id() 0 3 1
A setIsRequirement() 0 3 1
B getIndependentCategoriesWithStudentResult() 0 25 7
A getSkillsForSelect() 0 11 3
A get_root_categories_for_teacher() 0 43 5
A set_certificate_min_score() 0 3 1
F load() 0 71 13
A updateChildrenWeight() 0 11 4
A show_message_resource_delete() 0 20 3
A showAllCategoryInfo() 0 12 2
A delete() 0 5 1
A is_certificate_available() 0 20 4
A is_course() 0 4 3
C add() 0 83 16
C save() 0 77 12
A setDocumentId() 0 3 1
A create_root_category() 0 15 1
A getRemainingWeight() 0 12 2
A setUrl() 0 11 3
B create_category_objects_from_sql_result() 0 39 7
B exportAllCertificates() 0 46 9
B calculateCurrentScore() 0 52 7
A getUserScoreForEvaluation() 0 10 2
F generateUserCertificate() 0 143 23
A getCourseId() 0 3 1
A addTargetSubcategories() 0 19 3
A setGradeBooksToValidateInDependence() 0 5 1
A setCourseId() 0 12 2
A can_be_moved_to_cat() 0 3 1
A userMeetsMinimumScores() 0 15 5
A getStudentList() 0 3 1
A applyCourseCodeToChildren() 0 27 4
A getGradeBooksToValidateInDependence() 0 3 1
C calculateFlatViewTotalPercent() 0 49 17
A getCurrentScore() 0 35 3
A add_subtree() 0 21 3
A setStudentList() 0 3 1
A getUrl() 0 14 3
A deleteAllCertificates() 0 12 4
A getDocumentId() 0 3 1
A getDownloadCertificateBlock() 0 18 2
A registerCurrentScore() 0 9 1
A userFinishedCourse() 0 18 1
A findByCertificate() 0 18 3

How to fix   Complexity   

Complex Class

Complex classes like Category 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 Category, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\GradebookCategory;
6
use Chamilo\CoreBundle\Enums\ActionIcon;
7
use Chamilo\CoreBundle\Framework\Container;
8
use ChamiloSession as Session;
9
10
/**
11
 * Class Category
12
 * Defines a gradebook Category object.
13
 */
14
class Category implements GradebookItem
15
{
16
    public $studentList;
17
    public $evaluations;
18
    public $links;
19
    public $subCategories;
20
    /** @var GradebookCategory */
21
    public $entity;
22
    private int $id;
23
    private $name;
24
    private $description;
25
    private $user_id;
26
    private $course_code;
27
    private $courseId;
28
    private $parent;
29
    private $weight;
30
    private $visible;
31
    private $certificate_min_score;
32
    private $session_id;
33
    private $skills = [];
34
    private $grade_model_id;
35
    private $generateCertificates;
36
    private $isRequirement;
37
    private $courseDependency;
38
    private $minimumToValidate;
39
    private $documentId;
40
    /** @var int */
41
    private $gradeBooksToValidateInDependence;
42
43
    /**
44
     * Consctructor.
45
     */
46
    public function __construct()
47
    {
48
        $this->id = 0;
49
        $this->name = null;
50
        $this->description = '';
51
        $this->user_id = 0;
52
        $this->courseId = 0;
53
        $this->parent = 0;
54
        $this->weight = 0;
55
        $this->visible = false;
56
        $this->certificate_min_score = 0;
57
        $this->session_id = 0;
58
        $this->grade_model_id = 0;
59
        $this->generateCertificates = false;
60
        $this->isRequirement = false;
61
        $this->courseDependency = [];
62
        $this->documentId = 0;
63
        $this->minimumToValidate = null;
64
    }
65
66
    /**
67
     * @return int
68
     */
69
    public function get_id()
70
    {
71
        return $this->id;
72
    }
73
74
    /**
75
     * @return string
76
     */
77
    public function get_name()
78
    {
79
        return $this->name;
80
    }
81
82
    /**
83
     * @return string
84
     */
85
    public function get_description()
86
    {
87
        return $this->description;
88
    }
89
90
    /**
91
     * @return int
92
     */
93
    public function get_user_id()
94
    {
95
        return $this->user_id;
96
    }
97
98
    /**
99
     * @return int|null
100
     */
101
    public function getCertificateMinScore()
102
    {
103
        if (!empty($this->certificate_min_score)) {
104
            return $this->certificate_min_score;
105
        }
106
107
        return null;
108
    }
109
110
    /**
111
     * @return string
112
     */
113
    public function get_course_code()
114
    {
115
        return $this->course_code;
116
    }
117
118
    /**
119
     * @return int
120
     */
121
    public function get_parent_id()
122
    {
123
        return $this->parent;
124
    }
125
126
    /**
127
     * @return int
128
     */
129
    public function get_weight()
130
    {
131
        return $this->weight;
132
    }
133
134
    /**
135
     * @return bool
136
     */
137
    public function is_locked()
138
    {
139
        return isset($this->locked) && 1 == $this->locked ? true : false;
140
    }
141
142
    /**
143
     * @return bool
144
     */
145
    public function is_visible()
146
    {
147
        return $this->visible;
148
    }
149
150
    /**
151
     * Get $isRequirement.
152
     *
153
     * @return int
154
     */
155
    public function getIsRequirement()
156
    {
157
        return $this->isRequirement;
158
    }
159
160
    /**
161
     * @param int $id
162
     */
163
    public function set_id($id)
164
    {
165
        $this->id = $id;
166
    }
167
168
    /**
169
     * @param string $name
170
     */
171
    public function set_name($name)
172
    {
173
        $this->name = $name;
174
    }
175
176
    /**
177
     * @param string $description
178
     */
179
    public function set_description($description)
180
    {
181
        $this->description = $description;
182
    }
183
184
    /**
185
     * @param int $user_id
186
     */
187
    public function set_user_id($user_id)
188
    {
189
        $this->user_id = $user_id;
190
    }
191
192
    /**
193
     * @param float $min_score
194
     */
195
    public function set_certificate_min_score($min_score = null)
196
    {
197
        $this->certificate_min_score = $min_score;
198
    }
199
200
    /**
201
     * @param int $parent
202
     */
203
    public function set_parent_id($parent)
204
    {
205
        $this->parent = (int) $parent;
206
    }
207
208
    /**
209
     * Filters to int and sets the session ID.
210
     *
211
     * @param   int     The session ID from the Dokeos course session
212
     */
213
    public function set_session_id($session_id = 0)
214
    {
215
        $this->session_id = (int) $session_id;
216
    }
217
218
    /**
219
     * @param $weight
220
     */
221
    public function set_weight($weight)
222
    {
223
        $this->weight = $weight;
224
    }
225
226
    /**
227
     * @param $visible
228
     */
229
    public function set_visible($visible)
230
    {
231
        $this->visible = $visible;
232
    }
233
234
    /**
235
     * @param int $id
236
     */
237
    public function set_grade_model_id($id)
238
    {
239
        $this->grade_model_id = $id;
240
    }
241
242
    /**
243
     * @param $locked
244
     */
245
    public function set_locked($locked)
246
    {
247
        $this->locked = $locked;
248
    }
249
250
    /**
251
     * Set $isRequirement.
252
     *
253
     * @param int $isRequirement
254
     */
255
    public function setIsRequirement($isRequirement)
256
    {
257
        $this->isRequirement = $isRequirement;
258
    }
259
260
    /**
261
     * @param $value
262
     */
263
    public function setCourseListDependency($value)
264
    {
265
        $this->courseDependency = [];
266
267
        $unserialized = UnserializeApi::unserialize('not_allowed_classes', $value, true);
268
269
        if (false !== $unserialized) {
270
            $this->courseDependency = $unserialized;
271
        }
272
    }
273
274
    /**
275
     * Course id list.
276
     *
277
     * @return array
278
     */
279
    public function getCourseListDependency()
280
    {
281
        return $this->courseDependency;
282
    }
283
284
    /**
285
     * @param int $value
286
     */
287
    public function setMinimumToValidate($value)
288
    {
289
        $this->minimumToValidate = $value;
290
    }
291
292
    public function getMinimumToValidate()
293
    {
294
        return $this->minimumToValidate;
295
    }
296
297
    /**
298
     * @return int|null
299
     */
300
    public function get_grade_model_id()
301
    {
302
        if ($this->grade_model_id < 0) {
303
            return null;
304
        }
305
306
        return $this->grade_model_id;
307
    }
308
309
    /**
310
     * @return string
311
     */
312
    public function get_type()
313
    {
314
        return 'category';
315
    }
316
317
    /**
318
     * @param bool $from_db
319
     *
320
     * @return array|resource
321
     */
322
    public function get_skills($from_db = true)
323
    {
324
        if ($from_db) {
325
            $categoryId = $this->get_id();
326
            $gradebook = new Gradebook();
327
            $skills = $gradebook->getSkillsByGradebook($categoryId);
328
        } else {
329
            $skills = $this->skills;
330
        }
331
332
        return $skills;
333
    }
334
335
    /**
336
     * @return array
337
     */
338
    public function getSkillsForSelect()
339
    {
340
        $skills = $this->get_skills();
341
        $skill_select = [];
342
        if (!empty($skills)) {
343
            foreach ($skills as $skill) {
344
                $skill_select[$skill['id']] = $skill['name'];
345
            }
346
        }
347
348
        return $skill_select;
349
    }
350
351
    /**
352
     * Set the generate_certificates value.
353
     *
354
     * @param int $generateCertificates
355
     */
356
    public function setGenerateCertificates($generateCertificates)
357
    {
358
        $this->generateCertificates = $generateCertificates;
359
    }
360
361
    /**
362
     * Get the generate_certificates value.
363
     *
364
     * @return int
365
     */
366
    public function getGenerateCertificates()
367
    {
368
        return $this->generateCertificates;
369
    }
370
371
    /**
372
     * @param int $id
373
     * @param int $session_id
374
     *
375
     * @return array
376
     */
377
    public static function loadSessionCategories(
378
        $id = null,
379
        $session_id = null
380
    ) {
381
        if (isset($id) && 0 === (int) $id) {
382
            $cats = [];
383
            $cats[] = self::create_root_category();
384
385
            return $cats;
386
        }
387
        $courseId = api_get_course_int_id();
388
        $session_id = (int) $session_id;
389
390
        if (!empty($session_id)) {
391
            $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
392
            $sql = 'SELECT id, c_id
393
                    FROM '.$table.'
394
                    WHERE session_id = '.$session_id;
395
            $result_session = Database::query($sql);
396
            if (Database::num_rows($result_session) > 0) {
397
                $categoryList = [];
398
                while ($data_session = Database::fetch_array($result_session)) {
399
                    $parent_id = $data_session['id'];
400
                    if ($data_session['c_id'] == $courseId) {
401
                        $categories = self::load($parent_id);
402
                        $categoryList = array_merge($categoryList, $categories);
403
                    }
404
                }
405
406
                return $categoryList;
407
            }
408
        }
409
    }
410
411
    /**
412
     * Retrieve categories and return them as an array of Category objects.
413
     *
414
     * @param ?int  $id category id
415
     * @param ?int  $user_id (category owner)
416
     * @param ?int  $courseId course id (int)
417
     * @param ?int  $parent_id parent category
418
     * @param ?int  $visible 0 or 1
419
     * @param ?int  $session_id (in case we are in a session)
420
     * @param ?bool $order_by Whether to show all "session"
421
     *                            categories (true) or hide them (false) in case there is no session id
422
     *
423
     * @return array<static>
424
     * @throws \Doctrine\DBAL\Exception
425
     * @throws Exception
426
     */
427
    public static function load(
428
        ?int $id = null,
429
        ?int $user_id = null,
430
        ?int $courseId = 0,
431
        ?int $parent_id = null,
432
        ?int $visible = null,
433
        ?int $session_id = null,
434
        ?string $order_by = null
435
    ): array {
436
        //if the category given is explicitly 0 (not null), then create
437
        // a root category object (in memory)
438
        if (isset($id) && 0 === $id) {
439
            $cats = [];
440
            $cats[] = self::create_root_category();
441
442
            return $cats;
443
        }
444
445
        $bond = ' WHERE';
446
447
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
448
        $sql = 'SELECT * FROM '.$table;
449
        if (!empty($id)) {
450
            $sql .= ' WHERE id = '.$id;
451
            $bond = ' AND';
452
        }
453
454
        if (isset($user_id)) {
455
            $sql .= $bond.' user_id = '.$user_id;
456
            $bond = ' AND';
457
        }
458
459
460
461
        if (!empty($courseId)) {
462
            $sql .= $bond." c_id = $courseId";
463
            $bond = ' AND';
464
        }
465
466
        if (!isset($session_id)) {
467
            $session_id = api_get_session_id();
468
        }
469
470
        if (!empty($session_id)) {
471
            $sql .= $bond.' session_id = '.$session_id;
472
        } else {
473
            if (empty($id)) {
474
                $sql .= $bond.' (session_id IS NULL OR session_id = 0) ';
475
            }
476
        }
477
        $bond = ' AND';
478
479
        if (!empty($parent_id)) {
480
            $sql .= $bond.' parent_id = '.$parent_id;
481
        }
482
483
        if (isset($visible)) {
484
            $sql .= $bond.' visible = '.$visible;
485
        }
486
487
        if (isset($order_by)) {
488
            $sql .= ' '.Database::escape_string($order_by);
489
        }
490
491
        $result = Database::query($sql);
492
        $categories = [];
493
        if (Database::num_rows($result) > 0) {
494
            $categories = self::create_category_objects_from_sql_result($result);
495
        }
496
497
        return $categories;
498
    }
499
500
    /**
501
     * Insert this category into the database.
502
     * @throws \Doctrine\ORM\Exception\NotSupported
503
     */
504
    public function add()
505
    {
506
        if (isset($this->name) && '-1' == $this->name) {
507
            return false;
508
        }
509
510
        if (isset($this->name) && isset($this->user_id)) {
511
            $em = Database::getManager();
512
513
            $course = api_get_course_entity($this->courseId);
514
            $parent = null;
515
            if (!empty($this->parent)) {
516
                $parent = $em->getRepository(GradebookCategory::class)->find($this->parent);
517
            }
518
519
            $category = new GradebookCategory();
520
            $category->setTitle($this->name);
521
            $category->setDescription($this->description);
522
            $userId = is_numeric($this->user_id) ? (int) $this->user_id : api_get_user_id();
523
            $category->setUser(api_get_user_entity($userId));
524
            $category->setUser(api_get_user_entity($this->user_id));
525
            $category->setCourse($course);
526
            $category->setParent($parent);
527
            $category->setWeight(api_float_val($this->weight));
528
            $category->setVisible($this->visible ? true : false);
529
            $category->setCertifMinScore($this->certificate_min_score);
530
            $category->setSession(api_get_session_entity($this->session_id));
531
            $category->setGenerateCertificates($this->generateCertificates);
532
            if (!empty($this->grade_model_id)) {
533
                $model = $em->getRepository(\Chamilo\CoreBundle\Entity\GradeModel::class)->find($this->grade_model_id);
534
                $category->setGradeModel($model);
535
            }
536
537
            $category->setIsRequirement($this->isRequirement);
538
            $category->setLocked(0);
539
540
            $em->persist($category);
541
            $em->flush();
542
543
            $id = $category->getId();
544
            $this->set_id($id);
545
546
            if (!empty($id)) {
547
                $parent_id = $this->get_parent_id();
548
                $grade_model_id = $this->get_grade_model_id();
549
                if (0 == $parent_id) {
550
                    //do something
551
                    if (isset($grade_model_id) &&
552
                        !empty($grade_model_id) &&
553
                        '-1' != $grade_model_id
554
                    ) {
555
                        $obj = new GradeModel();
556
                        $components = $obj->get_components($grade_model_id);
557
                        $default_weight_setting = api_get_setting('gradebook_default_weight');
558
                        $default_weight = 100;
559
                        if (isset($default_weight_setting)) {
560
                            $default_weight = $default_weight_setting;
561
                        }
562
                        foreach ($components as $component) {
563
                            $gradebook = new Gradebook();
564
                            $params = [];
565
566
                            $params['name'] = $component['acronym'];
567
                            $params['description'] = $component['title'];
568
                            $params['user_id'] = api_get_user_id();
569
                            $params['parent_id'] = $id;
570
                            $params['weight'] = $component['percentage'] / 100 * $default_weight;
571
                            $params['session_id'] = api_get_session_id();
572
                            $params['c_id'] = $this->getCourseId();
573
574
                            $gradebook->save($params);
575
                        }
576
                    }
577
                }
578
            }
579
580
            $gradebook = new Gradebook();
581
            $gradebook->updateSkillsToGradeBook(
582
                $this->id,
583
                $this->get_skills(false)
584
            );
585
586
            return $id;
587
        }
588
    }
589
590
    /**
591
     * Update the properties of this category in the database.
592
     *
593
     * @throws \Doctrine\ORM\Exception\NotSupported
594
     * @throws \Doctrine\ORM\Exception\ORMException
595
     * @todo fix me
596
     */
597
    public function save()
598
    {
599
        $em = Database::getManager();
600
        $repo = $em->getRepository(GradebookCategory::class);
601
602
        /** @var GradebookCategory $category */
603
        $category = $repo->find($this->id);
604
605
        if (null === $category) {
606
            return false;
607
        }
608
609
        $parent = null;
610
        if (!empty($this->parent)) {
611
            $parent = $repo->find($this->parent);
612
        }
613
        $course = api_get_course_entity();
614
615
        $category->setTitle($this->name);
616
        $category->setDescription($this->description);
617
        $userId = is_numeric($this->user_id) ? (int) $this->user_id : api_get_user_id();
618
        $category->setUser(api_get_user_entity($userId));
619
        $category->setCourse($course);
620
        $category->setParent($parent);
621
        $category->setWeight($this->weight);
622
        $category->setVisible($this->visible);
623
        $category->setCertifMinScore($this->certificate_min_score);
624
        $category->setGenerateCertificates($this->generateCertificates);
625
        if (!empty($this->grade_model_id)) {
626
            $model = $em->getRepository(\Chamilo\CoreBundle\Entity\GradeModel::class)->find($this->grade_model_id);
627
            $category->setGradeModel($model);
628
        }
629
630
        $category->setIsRequirement($this->isRequirement);
631
632
        $em->persist($category);
633
        $em->flush();
634
635
        if (!empty($this->id)) {
636
            $parent_id = $this->get_parent_id();
637
            $grade_model_id = $this->get_grade_model_id();
638
            if (0 == $parent_id) {
639
                if (!empty($grade_model_id) &&
640
                    '-1' != $grade_model_id
641
                ) {
642
                    $obj = new GradeModel();
643
                    $components = $obj->get_components($grade_model_id);
644
                    $default_weight_setting = api_get_setting('gradebook_default_weight');
645
                    $default_weight = 100;
646
                    if (isset($default_weight_setting)) {
647
                        $default_weight = $default_weight_setting;
648
                    }
649
                    $final_weight = $this->get_weight();
650
                    if (!empty($final_weight)) {
651
                        $default_weight = $this->get_weight();
652
                    }
653
                    foreach ($components as $component) {
654
                        $gradebook = new Gradebook();
655
                        $params = [];
656
                        $params['name'] = $component['acronym'];
657
                        $params['description'] = $component['title'];
658
                        $params['user_id'] = api_get_user_id();
659
                        $params['parent_id'] = $this->id;
660
                        $params['weight'] = $component['percentage'] / 100 * $default_weight;
661
                        $params['session_id'] = api_get_session_id();
662
                        $params['c_id'] = $this->getCourseId();
663
                        $gradebook->save($params);
664
                    }
665
                }
666
            }
667
        }
668
669
        $gradebook = new Gradebook();
670
        $gradebook->updateSkillsToGradeBook(
671
            $this->id,
672
            $this->get_skills(false),
673
            true
674
        );
675
    }
676
677
    /**
678
     * Update link weights see #5168.
679
     *
680
     * @param int $new_weight
681
     */
682
    public function updateChildrenWeight($new_weight)
683
    {
684
        $links = $this->get_links();
685
        $old_weight = $this->get_weight();
686
687
        if (!empty($links)) {
688
            foreach ($links as $link_item) {
689
                if (isset($link_item)) {
690
                    $new_item_weight = $new_weight * $link_item->get_weight() / $old_weight;
691
                    $link_item->set_weight($new_item_weight);
692
                    $link_item->save();
693
                }
694
            }
695
        }
696
    }
697
698
    /**
699
     * Delete this evaluation from the database.
700
     * @throws Exception
701
     */
702
    public function delete(): void
703
    {
704
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
705
        $sql = 'DELETE FROM '.$table.' WHERE id = '.intval($this->id);
706
        Database::query($sql);
707
    }
708
709
    /**
710
     * Return an HTML span block if the given resource has been deleted
711
     * @param ?int $courseId
712
     *
713
     * @return string
714
     * @throws \Doctrine\DBAL\Exception
715
     * @throws Exception
716
     */
717
    public static function show_message_resource_delete(?int $courseId): string
718
    {
719
        if (empty($courseId)) {
720
            return '';
721
        }
722
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
723
        $sql = "SELECT count(*) AS num
724
                FROM $table
725
                WHERE
726
                    c_id = $courseId AND
727
                    visible = 3";
728
        $res = Database::query($sql);
729
        $option = Database::fetch_array($res);
730
        if ($option['num'] >= 1) {
731
            return '&nbsp;&nbsp;<span class="resource-deleted">
732
                (&nbsp;'.get_lang('The resource has been deleted').'&nbsp;)
733
                </span>';
734
        }
735
736
        return '';
737
    }
738
739
    /**
740
     * Shows all information of a category.
741
     *
742
     * @param int $categoryId
743
     *
744
     * @return array
745
     * @throws \Doctrine\DBAL\Exception
746
     * @throws Exception
747
     */
748
    public function showAllCategoryInfo(int $categoryId): array
749
    {
750
        if (empty($categoryId)) {
751
            return [];
752
        }
753
754
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
755
        $sql = "SELECT * FROM $table
756
                WHERE id = $categoryId";
757
        $result = Database::query($sql);
758
759
        return Database::fetch_array($result);
760
    }
761
762
    /**
763
     * Checks if the certificate is available for the given user in this category.
764
     *
765
     * @param int $user_id User ID
766
     *
767
     * @return bool True if conditions match, false if fails
768
     */
769
    public function is_certificate_available($user_id)
770
    {
771
        $score = $this->calc_score(
772
            $user_id,
773
            null,
774
            $this->courseId,
775
            $this->session_id
776
        );
777
778
        if (isset($score) && isset($score[0])) {
779
            // Get a percentage score to compare to minimum certificate score
780
            // $certification_score = $score[0] / $score[1] * 100;
781
            // Get real score not a percentage.
782
            $certification_score = $score[0];
783
            if ($certification_score >= $this->certificate_min_score) {
784
                return true;
785
            }
786
        }
787
788
        return false;
789
    }
790
791
    /**
792
     * Is this category the main one in a course ?
793
     * A category is a course if it has a course code and no parent category.
794
     */
795
    public function is_course(): bool
796
    {
797
        return !empty($this->getCourseId())
798
            && (!isset($this->parent) || 0 == $this->parent);
799
    }
800
801
    /**
802
     * Calculate the score of this category.
803
     */
804
    public function calc_score(
805
        ?int $studentId = null,
806
        ?string $type = null,
807
        ?int $courseId = 0,
808
        ?int $session_id = null
809
    ): ?array {
810
        $key = 'category:'.$this->id.'student:'.(int) $studentId.'type:'.$type.'course:'.$courseId.'session:'.(int) $session_id;
811
        $useCache = ('true' === api_get_setting('gradebook.gradebook_use_apcu_cache'));
812
        $cacheAvailable = api_get_configuration_value('apc') && $useCache;
813
814
        if ($cacheAvailable) {
815
            $cache = new \Symfony\Component\Cache\Adapter\ApcuAdapter();
816
            if ($cache->hasItem($key)) {
817
                return $cache->getItem($key)->get();
818
            }
819
        }
820
        // Classic
821
        if (!empty($studentId) && '' == $type) {
822
            if (!empty($courseId)) {
823
                $cats = $this->get_subcategories(
824
                    $studentId,
825
                    $courseId,
826
                    $session_id
827
                );
828
                $evals = $this->get_evaluations($studentId, false, $courseId);
829
                $links = $this->get_links($studentId, false, $courseId);
830
            } else {
831
                $cats = $this->get_subcategories($studentId);
832
                $evals = $this->get_evaluations($studentId);
833
                $links = $this->get_links($studentId);
834
            }
835
836
            // Calculate score
837
            $count = 0;
838
            $ressum = 0;
839
            $weightsum = 0;
840
            if (!empty($cats)) {
841
                /** @var Category $cat */
842
                foreach ($cats as $cat) {
843
                    $cat->set_session_id($session_id);
844
                    $cat->setCourseId($courseId);
845
                    $cat->setStudentList($this->getStudentList());
846
                    $score = $cat->calc_score(
847
                        $studentId,
848
                        null,
849
                        $courseId,
850
                        $session_id
851
                    );
852
853
                    $catweight = 0;
854
                    if (0 != $cat->get_weight()) {
855
                        $catweight = $cat->get_weight();
856
                        $weightsum += $catweight;
857
                    }
858
859
                    if (isset($score) && !empty($score[1]) && !empty($catweight)) {
860
                        $ressum += $score[0] / $score[1] * $catweight;
861
                    }
862
                }
863
            }
864
865
            if (!empty($evals)) {
866
                /** @var Evaluation $eval */
867
                foreach ($evals as $eval) {
868
                    $eval->setStudentList($this->getStudentList());
869
                    $evalres = $eval->calc_score($studentId);
870
                    if (isset($evalres) && 0 != $eval->get_weight()) {
871
                        $evalweight = $eval->get_weight();
872
                        $weightsum += $evalweight;
873
                        if (!empty($evalres[1])) {
874
                            $ressum += $evalres[0] / $evalres[1] * $evalweight;
875
                        }
876
                    } else {
877
                        if (0 != $eval->get_weight()) {
878
                            $evalweight = $eval->get_weight();
879
                            $weightsum += $evalweight;
880
                        }
881
                    }
882
                }
883
            }
884
885
            if (!empty($links)) {
886
                /** @var EvalLink|ExerciseLink $link */
887
                foreach ($links as $link) {
888
                    $link->setStudentList($this->getStudentList());
889
890
                    if ($session_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $session_id of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
891
                        $link->set_session_id($session_id);
892
                    }
893
894
                    $linkres = $link->calc_score($studentId, null);
895
                    if (!empty($linkres) && 0 != $link->get_weight()) {
896
                        $linkweight = $link->get_weight();
897
                        $link_res_denom = 0 == $linkres[1] ? 1 : $linkres[1];
898
                        $weightsum += $linkweight;
899
                        $ressum += $linkres[0] / $link_res_denom * $linkweight;
900
                    } else {
901
                        // Adding if result does not exists
902
                        if (0 != $link->get_weight()) {
903
                            $linkweight = $link->get_weight();
904
                            $weightsum += $linkweight;
905
                        }
906
                    }
907
                }
908
            }
909
        } else {
910
            if (!empty($courseId)) {
911
                $cats = $this->get_subcategories(
912
                    null,
913
                    $courseId,
914
                    $session_id
915
                );
916
                $evals = $this->get_evaluations(null, false, $courseId);
917
                $links = $this->get_links(null, false, $courseId);
918
            } else {
919
                $cats = $this->get_subcategories(null);
920
                $evals = $this->get_evaluations(null);
921
                $links = $this->get_links(null);
922
            }
923
924
            // Calculate score
925
            $ressum = 0;
926
            $weightsum = 0;
927
            $bestResult = 0;
928
            $totalScorePerStudent = [];
929
930
            if (!empty($cats)) {
931
                /** @var Category $cat */
932
                foreach ($cats as $cat) {
933
                    $cat->setStudentList($this->getStudentList());
934
                    $score = $cat->calc_score(
935
                        null,
936
                        $type,
937
                        $courseId,
938
                        $session_id
939
                    );
940
941
                    $catweight = 0;
942
                    if (0 != $cat->get_weight()) {
943
                        $catweight = $cat->get_weight();
944
                        $weightsum += $catweight;
945
                    }
946
947
                    if (isset($score) && !empty($score[1]) && !empty($catweight)) {
948
                        $ressum += $score[0] / $score[1] * $catweight;
949
950
                        if ($ressum > $bestResult) {
951
                            $bestResult = $ressum;
952
                        }
953
                    }
954
                }
955
            }
956
957
            if (!empty($evals)) {
958
                if ('best' === $type) {
959
                    $studentList = $this->getStudentList();
960
                    foreach ($studentList as $student) {
961
                        $studentId = $student['user_id'];
962
                        foreach ($evals as $eval) {
963
                            $linkres = $eval->calc_score($studentId, null);
964
                            $linkweight = $eval->get_weight();
965
                            $link_res_denom = 0 == $linkres[1] ? 1 : $linkres[1];
966
                            $ressum = $linkres[0] / $link_res_denom * $linkweight;
967
968
                            if (!isset($totalScorePerStudent[$studentId])) {
969
                                $totalScorePerStudent[$studentId] = 0;
970
                            }
971
                            $totalScorePerStudent[$studentId] += $ressum;
972
                        }
973
                    }
974
                } else {
975
                    /** @var Evaluation $eval */
976
                    foreach ($evals as $eval) {
977
                        $evalres = $eval->calc_score(null, $type);
978
                        $eval->setStudentList($this->getStudentList());
979
980
                        if (isset($evalres) && 0 != $eval->get_weight()) {
981
                            $evalweight = $eval->get_weight();
982
                            $weightsum += $evalweight;
983
                            if (!empty($evalres[1])) {
984
                                $ressum += $evalres[0] / $evalres[1] * $evalweight;
985
                            }
986
987
                            if ($ressum > $bestResult) {
988
                                $bestResult = $ressum;
989
                            }
990
                        } else {
991
                            if (0 != $eval->get_weight()) {
992
                                $evalweight = $eval->get_weight();
993
                                $weightsum += $evalweight;
994
                            }
995
                        }
996
                    }
997
                }
998
            }
999
1000
            if (!empty($links)) {
1001
                $studentList = $this->getStudentList();
1002
                if ('best' === $type) {
1003
                    foreach ($studentList as $student) {
1004
                        $studentId = $student['user_id'];
1005
                        foreach ($links as $link) {
1006
                            $linkres = $link->calc_score($studentId, null);
1007
                            $linkweight = $link->get_weight();
1008
                            if ($linkres) {
1009
                                $link_res_denom = 0 == $linkres[1] ? 1 : $linkres[1];
1010
                                $ressum = $linkres[0] / $link_res_denom * $linkweight;
1011
                            }
1012
1013
                            if (!isset($totalScorePerStudent[$studentId])) {
1014
                                $totalScorePerStudent[$studentId] = 0;
1015
                            }
1016
                            $totalScorePerStudent[$studentId] += $ressum;
1017
                        }
1018
                    }
1019
                } else {
1020
                    /** @var EvalLink|ExerciseLink $link */
1021
                    foreach ($links as $link) {
1022
                        $link->setStudentList($this->getStudentList());
1023
1024
                        if ($session_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $session_id of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1025
                            $link->set_session_id($session_id);
1026
                        }
1027
1028
                        $linkres = $link->calc_score($studentId, $type);
1029
1030
                        if (!empty($linkres) && 0 != $link->get_weight()) {
1031
                            $linkweight = $link->get_weight();
1032
                            $link_res_denom = 0 == $linkres[1] ? 1 : $linkres[1];
1033
1034
                            $weightsum += $linkweight;
1035
                            $ressum += $linkres[0] / $link_res_denom * $linkweight;
1036
                            if ($ressum > $bestResult) {
1037
                                $bestResult = $ressum;
1038
                            }
1039
                        } else {
1040
                            // Adding if result does not exist
1041
                            if (0 != $link->get_weight()) {
1042
                                $linkweight = $link->get_weight();
1043
                                $weightsum += $linkweight;
1044
                            }
1045
                        }
1046
                    }
1047
                }
1048
            }
1049
        }
1050
1051
        switch ($type) {
1052
            case 'best':
1053
                arsort($totalScorePerStudent);
1054
                $maxScore = current($totalScorePerStudent);
1055
1056
                return [$maxScore, $this->get_weight()];
1057
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
1058
            case 'average':
1059
                if (empty($ressum)) {
1060
                    if ($cacheAvailable) {
1061
                        $cacheItem = $cache->getItem($key);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cache does not seem to be defined for all execution paths leading up to this point.
Loading history...
1062
                        $cacheItem->set(null);
1063
1064
                        $cache->save($cacheItem);
1065
                    }
1066
1067
                    return null;
1068
                }
1069
1070
                if ($cacheAvailable) {
1071
                    $cacheItem = $cache->getItem($key);
1072
                    $cacheItem->set([$ressum, $weightsum]);
1073
1074
                    $cache->save($cacheItem);
1075
                }
1076
1077
                return [$ressum, $weightsum];
1078
                //break;
1079
            case 'ranking':
1080
                // category ranking is calculated in gradebook_data_generator.class.php
1081
                // function get_data
1082
                return null;
1083
1084
                //return AbstractLink::getCurrentUserRanking($studentId, []);
1085
                //break;
1086
            default:
1087
                if ($cacheAvailable) {
1088
                    $cacheItem = $cache->getItem($key);
1089
                    $cacheItem->set([$ressum, $weightsum]);
1090
1091
                    $cache->save($cacheItem);
1092
                }
1093
1094
                return [$ressum, $weightsum];
1095
        }
1096
    }
1097
1098
    /**
1099
     * Delete this category and every subcategory, evaluation and result inside.
1100
     */
1101
    public function delete_all()
1102
    {
1103
        $cats = self::load(null, null, $this->getCourseId(), $this->id, null);
1104
        $evals = Evaluation::load(
1105
            null,
1106
            null,
1107
            $this->getCourseId(),
1108
            $this->id,
1109
            null
1110
        );
1111
1112
        $links = LinkFactory::load(
1113
            null,
1114
            null,
1115
            null,
1116
            null,
1117
            $this->getCourseId(),
1118
            $this->id,
1119
            null
1120
        );
1121
1122
        if (!empty($cats)) {
1123
            /** @var Category $cat */
1124
            foreach ($cats as $cat) {
1125
                $cat->delete_all();
1126
                $cat->delete();
1127
            }
1128
        }
1129
1130
        if (!empty($evals)) {
1131
            /** @var Evaluation $eval */
1132
            foreach ($evals as $eval) {
1133
                $eval->delete_with_results();
1134
            }
1135
        }
1136
1137
        if (!empty($links)) {
1138
            /** @var AbstractLink $link */
1139
            foreach ($links as $link) {
1140
                $link->delete();
1141
            }
1142
        }
1143
1144
        $this->delete();
1145
    }
1146
1147
    /**
1148
     * Return array of Category objects where a student is subscribed to.
1149
     *
1150
     * @param int $stud_id
1151
     * @param int $courseId
1152
     * @param int $session_id
1153
     *
1154
     * @return array
1155
     * @throws \Doctrine\DBAL\Exception
1156
     */
1157
    public function get_root_categories_for_student(
1158
        int $stud_id,
1159
        int $courseId = 0,
1160
        int $session_id = 0
1161
    ) {
1162
        $main_course_user_table = Database::get_main_table(TABLE_MAIN_COURSE_USER);
1163
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
1164
1165
        $sql = "SELECT * FROM $table WHERE parent_id = 0";
1166
1167
        if (!api_is_allowed_to_edit()) {
1168
            $sql .= ' AND visible = 1';
1169
            //proceed with checks on optional parameters course & session
1170
            if (!empty($courseId)) {
1171
                // TODO: considering it highly improbable that a user would get here
1172
                // if he doesn't have the rights to view this course and this
1173
                // session, we don't check his registration to these, but this
1174
                // could be an improvement
1175
                if (!empty($session_id)) {
1176
                    $sql .= " AND c_id = '".$courseId."' AND session_id = ".$session_id;
1177
                } else {
1178
                    $sql .= " AND c_id = '".$courseId."' AND session_id is null OR session_id=0";
1179
                }
1180
            } else {
1181
                //no optional parameter, proceed as usual
1182
                $sql .= ' AND c_id in
1183
                     (
1184
                        SELECT c.id
1185
                        FROM '.$main_course_user_table.' cu
1186
                        WHERE cu.user_id = '.intval($stud_id).'
1187
                        AND cu.status = '.STUDENT.'
1188
                    )';
1189
            }
1190
        } elseif (!api_is_platform_admin()) {
1191
            //proceed with checks on optional parameters course & session
1192
            if (!empty($courseId)) {
1193
                // TODO: considering it highly improbable that a user would get here
1194
                // if he doesn't have the rights to view this course and this
1195
                // session, we don't check his registration to these, but this
1196
                // could be an improvement
1197
                $sql .= " AND c_id  = $courseId";
1198
                if (!empty($session_id)) {
1199
                    $sql .= " AND session_id = ".$session_id;
1200
                } else {
1201
                    $sql .= 'AND session_id IS NULL OR session_id = 0';
1202
                }
1203
            } else {
1204
                $sql .= ' AND c_id IN
1205
                     (
1206
                        SELECT c.id
1207
                        FROM '.$main_course_user_table.' cu
1208
                        WHERE
1209
                            cu.user_id = '.api_get_user_id().' AND
1210
                            cu.status = '.COURSEMANAGER.'
1211
                    )';
1212
            }
1213
        } elseif (api_is_platform_admin()) {
1214
            if (0 != $session_id) {
1215
                $sql .= ' AND session_id='.$session_id;
1216
            } else {
1217
                $sql .= ' AND coalesce(session_id,0)=0';
1218
            }
1219
        }
1220
        $result = Database::query($sql);
1221
        $cats = self::create_category_objects_from_sql_result($result);
1222
1223
        // course independent categories
1224
        if (empty($courseId)) {
1225
            $cats = $this->getIndependentCategoriesWithStudentResult(
1226
                0,
1227
                $stud_id,
1228
                $cats
1229
            );
1230
        }
1231
1232
        return $cats;
1233
    }
1234
1235
    /**
1236
     * Return array of Category objects where a teacher is admin for.
1237
     *
1238
     * @param int    $user_id (to return everything, use 'null' here)
1239
     * @param ?int   $courseId (optional)
1240
     * @param ?int    $session_id (optional)
1241
     *
1242
     * @return array
1243
     * @throws \Doctrine\DBAL\Exception
1244
     */
1245
    public function get_root_categories_for_teacher(
1246
        int $user_id,
1247
        ?int $courseId = null,
1248
        ?int $session_id = null
1249
    ) {
1250
        if (null == $user_id) {
1251
            return self::load(null, null, $courseId, 0, null, $session_id);
1252
        }
1253
1254
        $main_course_user_table = Database::get_main_table(TABLE_MAIN_COURSE_USER);
1255
        $tbl_grade_categories = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
1256
1257
        $sql = 'SELECT * FROM '.$tbl_grade_categories.'
1258
                WHERE parent_id = 0 ';
1259
        if (!empty($courseId)) {
1260
            $sql .= " AND c_id = $courseId ";
1261
            if (!empty($session_id)) {
1262
                $sql .= " AND session_id = ".$session_id;
1263
            }
1264
        } else {
1265
            $sql .= ' AND c_id in
1266
                 (
1267
                    SELECT cu.id
1268
                    FROM '.$main_course_user_table.' cu
1269
                    WHERE user_id = '.$user_id.'
1270
                )';
1271
        }
1272
        $result = Database::query($sql);
1273
        $cats = self::create_category_objects_from_sql_result($result);
1274
        // course independent categories
1275
        if (!empty($courseId)) {
1276
            $indcats = self::load(
1277
                null,
1278
                $user_id,
1279
                $courseId,
1280
                0,
1281
                null,
1282
                $session_id
1283
            );
1284
            $cats = array_merge($cats, $indcats);
1285
        }
1286
1287
        return $cats;
1288
    }
1289
1290
    /**
1291
     * Can this category be moved to somewhere else ?
1292
     * The root and courses cannot be moved.
1293
     *
1294
     * @return bool
1295
     */
1296
    public function is_movable()
1297
    {
1298
        return !(!isset($this->id) || 0 == $this->id || $this->is_course());
1299
    }
1300
1301
    /**
1302
     * Generate an array of possible categories where this category can be moved to.
1303
     * Notice: its own parent will be included in the list: it's up to the frontend
1304
     * to disable this element.
1305
     *
1306
     * @return array 2-dimensional array - every element contains 3 subelements (id, name, level)
1307
     */
1308
    public function get_target_categories()
1309
    {
1310
        // the root or a course -> not movable
1311
        if (!$this->is_movable()) {
1312
            return null;
1313
        } else {
1314
            // otherwise:
1315
            // - course independent category
1316
            //   -> movable to root or other independent categories
1317
            // - category inside a course
1318
            //   -> movable to root, independent categories or categories inside the course
1319
            $user = api_is_platform_admin() ? null : api_get_user_id();
1320
            $targets = [];
1321
            $level = 0;
1322
1323
            $root = [0, get_lang('Main folder'), $level];
1324
            $targets[] = $root;
1325
1326
            if (!empty($this->courseId)) {
1327
                $crscats = self::load(null, null, $this->courseId, 0);
1328
                foreach ($crscats as $cat) {
1329
                    if ($this->can_be_moved_to_cat($cat)) {
1330
                        $targets[] = [
1331
                            $cat->get_id(),
1332
                            $cat->get_name(),
1333
                            $level + 1,
1334
                        ];
1335
                        $targets = $this->addTargetSubcategories(
1336
                            $targets,
1337
                            $level + 1,
1338
                            $cat->get_id()
1339
                        );
1340
                    }
1341
                }
1342
            }
1343
1344
            $indcats = self::load(null, $user, 0, 0);
1345
            foreach ($indcats as $cat) {
1346
                if ($this->can_be_moved_to_cat($cat)) {
1347
                    $targets[] = [$cat->get_id(), $cat->get_name(), $level + 1];
1348
                    $targets = $this->addTargetSubcategories(
1349
                        $targets,
1350
                        $level + 1,
1351
                        $cat->get_id()
1352
                    );
1353
                }
1354
            }
1355
1356
            return $targets;
1357
        }
1358
    }
1359
1360
    /**
1361
     * Move this category to the given category.
1362
     * If this category moves from inside a course to outside,
1363
     * its course code must be changed, as well as the course code
1364
     * of all underlying categories and evaluations. All links will
1365
     * be deleted as well !
1366
     */
1367
    public function move_to_cat($cat)
1368
    {
1369
        $this->set_parent_id($cat->get_id());
1370
        if ($this->get_course_code() != $cat->get_course_code()) {
1371
            $this->setCourseId($cat->getCourseId());
1372
            $this->applyCourseCodeToChildren();
1373
        }
1374
        $this->save();
1375
    }
1376
1377
    /**
1378
     * Generate an array of all categories the user can navigate to.
1379
     */
1380
    public function get_tree()
1381
    {
1382
        $targets = [];
1383
        $level = 0;
1384
        $root = [0, get_lang('Main folder'), $level];
1385
        $targets[] = $root;
1386
1387
        // course or platform admin
1388
        if (api_is_allowed_to_edit()) {
1389
            $user = api_is_platform_admin() ? null : api_get_user_id();
1390
            $cats = self::get_root_categories_for_teacher($user);
0 ignored issues
show
Bug Best Practice introduced by
The method Category::get_root_categories_for_teacher() 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

1390
            /** @scrutinizer ignore-call */ 
1391
            $cats = self::get_root_categories_for_teacher($user);
Loading history...
1391
            foreach ($cats as $cat) {
1392
                $targets[] = [
1393
                    $cat->get_id(),
1394
                    $cat->get_name(),
1395
                    $level + 1,
1396
                ];
1397
                $targets = $this->add_subtree(
1398
                    $targets,
1399
                    $level + 1,
1400
                    $cat->get_id(),
1401
                    null
1402
                );
1403
            }
1404
        } else {
1405
            // student
1406
            $cats = $this->get_root_categories_for_student(api_get_user_id());
1407
            foreach ($cats as $cat) {
1408
                $targets[] = [
1409
                    $cat->get_id(),
1410
                    $cat->get_name(),
1411
                    $level + 1,
1412
                ];
1413
                $targets = $this->add_subtree(
1414
                    $targets,
1415
                    $level + 1,
1416
                    $cat->get_id(),
1417
                    1
1418
                );
1419
            }
1420
        }
1421
1422
        return $targets;
1423
    }
1424
1425
    /**
1426
     * Generate an array of courses that a teacher hasn't created a category for.
1427
     *
1428
     * @param int $user_id
1429
     *
1430
     * @return array 2-dimensional array - every element contains 2 subelements (code, title)
1431
     */
1432
    public static function get_not_created_course_categories($user_id)
1433
    {
1434
        $tbl_main_courses = Database::get_main_table(TABLE_MAIN_COURSE);
1435
        $tbl_main_course_user = Database::get_main_table(TABLE_MAIN_COURSE_USER);
1436
        $tbl_grade_categories = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
1437
1438
        $user_id = (int) $user_id;
1439
1440
        $sql = 'SELECT DISTINCT(cc.code), title
1441
                FROM '.$tbl_main_courses.' cc, '.$tbl_main_course_user.' cu
1442
                WHERE
1443
                    cc.id = cu.c_id AND
1444
                    cu.status = '.COURSEMANAGER;
1445
1446
        if (!api_is_platform_admin()) {
1447
            $sql .= ' AND cu.user_id = '.$user_id;
1448
        }
1449
        $sql .= ' AND cc.id NOT IN
1450
             (
1451
                SELECT c_id FROM '.$tbl_grade_categories.'
1452
                WHERE
1453
                    parent_id = 0 AND
1454
                    c_id IS NOT NULL
1455
                )';
1456
        $result = Database::query($sql);
1457
1458
        $cats = [];
1459
        while ($data = Database::fetch_array($result)) {
1460
            $cats[] = [$data['code'], $data['title']];
1461
        }
1462
1463
        return $cats;
1464
    }
1465
1466
    /**
1467
     * Generate an array of all courses that a teacher is admin of.
1468
     *
1469
     * @param int $user_id
1470
     *
1471
     * @return array 2-dimensional array - every element contains 2 sub-elements (code, title)
1472
     * @throws Exception
1473
     */
1474
    public static function get_all_courses(int $user_id): array
1475
    {
1476
        $tbl_main_courses = Database::get_main_table(TABLE_MAIN_COURSE);
1477
        $tbl_main_course_user = Database::get_main_table(TABLE_MAIN_COURSE_USER);
1478
        $sql = 'SELECT DISTINCT(code), title, cc.id
1479
                FROM '.$tbl_main_courses.' cc, '.$tbl_main_course_user.' cu
1480
                WHERE cc.id = cu.c_id AND cu.status = '.COURSEMANAGER;
1481
        if (!api_is_platform_admin()) {
1482
            $sql .= ' AND cu.user_id = '.$user_id;
1483
        }
1484
1485
        $result = Database::query($sql);
1486
        $cats = [];
1487
        while ($data = Database::fetch_array($result)) {
1488
            $cats[] = [$data['code'], $data['title']];
1489
        }
1490
1491
        return $cats;
1492
    }
1493
1494
    /**
1495
     * Apply the same visibility to every subcategory, evaluation and link.
1496
     */
1497
    public function apply_visibility_to_children()
1498
    {
1499
        $cats = self::load(null, null, 0, $this->id, null);
1500
        $evals = Evaluation::load(null, null, null, $this->id, null);
1501
        $links = LinkFactory::load(
1502
            null,
1503
            null,
1504
            null,
1505
            null,
1506
            null,
1507
            $this->id,
1508
            null
1509
        );
1510
        if (!empty($cats)) {
1511
            foreach ($cats as $cat) {
1512
                $cat->set_visible($this->is_visible());
1513
                $cat->save();
1514
                $cat->apply_visibility_to_children();
1515
            }
1516
        }
1517
        if (!empty($evals)) {
1518
            foreach ($evals as $eval) {
1519
                $eval->set_visible($this->is_visible());
1520
                $eval->save();
1521
            }
1522
        }
1523
        if (!empty($links)) {
1524
            foreach ($links as $link) {
1525
                $link->set_visible($this->is_visible());
1526
                $link->save();
1527
            }
1528
        }
1529
    }
1530
1531
    /**
1532
     * Check if a category contains evaluations with a result for a given student.
1533
     *
1534
     * @param int $studentId
1535
     *
1536
     * @return bool
1537
     */
1538
    public function hasEvaluationsWithStudentResults($studentId)
1539
    {
1540
        $evals = Evaluation::get_evaluations_with_result_for_student(
1541
            $studentId,
1542
            $this->id
1543
        );
1544
        if (0 != count($evals)) {
1545
            return true;
1546
        } else {
1547
            $cats = self::load(
1548
                null,
1549
                null,
1550
                0,
1551
                $this->id,
1552
                api_is_allowed_to_edit() ? null : 1
1553
            );
1554
1555
            /** @var Category $cat */
1556
            foreach ($cats as $cat) {
1557
                if ($cat->hasEvaluationsWithStudentResults($studentId)) {
1558
                    return true;
1559
                }
1560
            }
1561
1562
            return false;
1563
        }
1564
    }
1565
1566
    /**
1567
     * Retrieve all categories inside a course independent category
1568
     * that should be visible to a student.
1569
     *
1570
     * @param int   $categoryId parent category
1571
     * @param int   $studentId
1572
     * @param array $cats       optional: if defined, the categories will be added to this array
1573
     *
1574
     * @return array
1575
     */
1576
    public function getIndependentCategoriesWithStudentResult(
1577
        $categoryId,
1578
        $studentId,
1579
        $cats = []
1580
    ) {
1581
        $creator = api_is_allowed_to_edit() && !api_is_platform_admin() ? api_get_user_id() : null;
1582
1583
        $categories = self::load(
1584
            null,
1585
            $creator,
1586
            0,
1587
            $categoryId,
1588
            api_is_allowed_to_edit() ? null : 1
1589
        );
1590
1591
        if (!empty($categories)) {
1592
            /** @var Category $category */
1593
            foreach ($categories as $category) {
1594
                if ($category->hasEvaluationsWithStudentResults($studentId)) {
1595
                    $cats[] = $category;
1596
                }
1597
            }
1598
        }
1599
1600
        return $cats;
1601
    }
1602
1603
    /**
1604
     * Return the session id (in any case, even if it's null or 0).
1605
     *
1606
     * @return int Session id (can be null)
1607
     */
1608
    public function get_session_id()
1609
    {
1610
        return $this->session_id;
1611
    }
1612
1613
    /**
1614
     * Get appropriate subcategories visible for the user (and optionally the course and session).
1615
     *
1616
     * @param ?int   $studentId student id (default: all students)
1617
     * @param ?int  $courseId Course code (optional)
1618
     * @param ?int  $session_id Session ID (optional)
1619
     * @param ?string $order A sorting string like 'ORDER BY id'
1620
     *
1621
     * @return array Array of subcategories
1622
     * @throws \Doctrine\DBAL\Exception
1623
     */
1624
    public function get_subcategories(
1625
        ?int $studentId = null,
1626
        ?int $courseId = 0,
1627
        ?int $session_id = 0,
1628
        ?string $order = null
1629
    ): array {
1630
        // 1 student
1631
        if (isset($studentId)) {
1632
            // Special case: this is the root
1633
            if (0 == $this->id) {
1634
                return $this->get_root_categories_for_student($studentId, $courseId, $session_id);
1635
            } else {
1636
                return self::load(
1637
                    null,
1638
                    null,
1639
                    $courseId,
1640
                    $this->id,
1641
                    api_is_allowed_to_edit() ? null : 1,
1642
                    $session_id,
1643
                    $order
1644
                );
1645
            }
1646
        } else {
1647
            // All students
1648
            // Course admin
1649
            if (api_is_allowed_to_edit() && !api_is_platform_admin()) {
1650
1651
                // root
1652
                if (0 == $this->id) {
1653
1654
                    // inside a course
1655
                    return $this->get_root_categories_for_teacher(
1656
                        api_get_user_id(),
1657
                        $courseId,
1658
                        $session_id
1659
                    );
1660
                } elseif (!empty($this->courseId)) {
1661
1662
                    return self::load(
1663
                        null,
1664
                        null,
1665
                        $this->courseId,
1666
                        $this->id,
1667
                        null,
1668
                        $session_id,
1669
                        $order
1670
                    );
1671
                } elseif (!empty($courseId)) {
1672
1673
                    // course independent
1674
                    return self::load(
1675
                        null,
1676
                        null,
1677
                        $courseId,
1678
                        $this->id,
1679
                        null,
1680
                        $session_id,
1681
                        $order
1682
                    );
1683
                } else {
1684
                    return self::load(
1685
                        null,
1686
                        api_get_user_id(),
1687
                        0,
1688
                        $this->id,
1689
                        null
1690
                    );
1691
                }
1692
            } elseif (api_is_platform_admin()) {
1693
                // platform admin
1694
                // we explicitly avoid listing subcats from another session
1695
                return self::load(
1696
                    null,
1697
                    null,
1698
                    $courseId,
1699
                    $this->id,
1700
                    null,
1701
                    $session_id,
1702
                    $order
1703
                );
1704
            }
1705
        }
1706
1707
        return [];
1708
    }
1709
1710
    /**
1711
     * Get appropriate evaluations visible for the user.
1712
     *
1713
     * @param ?int  $studentId student id (default: all students)
1714
     * @param ?bool $recursive process subcategories (default: no recursion)
1715
     * @param ?int  $courseId
1716
     * @param ?int  $sessionId
1717
     *
1718
     * @return array
1719
     * @throws \Doctrine\DBAL\Exception
1720
     */
1721
    public function get_evaluations(
1722
        ?int $studentId = null,
1723
        ?bool $recursive = false,
1724
        ?int $courseId = 0,
1725
        ?int $sessionId = 0
1726
    ): array
1727
    {
1728
        $evals = [];
1729
        $courseId = empty($courseId) ? $this->getCourseId() : $courseId;
1730
        $sessionId = empty($sessionId) ? $this->get_session_id() : $sessionId;
1731
1732
        // 1 student
1733
        if (!empty($studentId)) {
1734
            // Special case: this is the root
1735
            if (0 == $this->id) {
1736
                $evals = Evaluation::get_evaluations_with_result_for_student(
1737
                    $studentId,
1738
                    0
1739
                );
1740
            } else {
1741
                $evals = Evaluation::load(
1742
                    null,
1743
                    null,
1744
                    $courseId,
1745
                    $this->id,
1746
                    api_is_allowed_to_edit() ? null : 1
1747
                );
1748
            }
1749
        } else {
1750
            // All students
1751
            // course admin
1752
            if ((api_is_allowed_to_edit() || api_is_drh() || api_is_session_admin()) &&
1753
                !api_is_platform_admin()
1754
            ) {
1755
                // root
1756
                if (0 == $this->id) {
1757
                    $evals = Evaluation::load(
1758
                        null,
1759
                        api_get_user_id(),
1760
                        null,
1761
                        $this->id,
1762
                        null
1763
                    );
1764
                } elseif (!empty($this->courseId)) {
1765
                    // inside a course
1766
                    $evals = Evaluation::load(
1767
                        null,
1768
                        null,
1769
                        $courseId,
1770
                        $this->id,
1771
                        null
1772
                    );
1773
                } else {
1774
                    // course independent
1775
                    $evals = Evaluation::load(
1776
                        null,
1777
                        api_get_user_id(),
1778
                        null,
1779
                        $this->id,
1780
                        null
1781
                    );
1782
                }
1783
            } else {
1784
                $evals = Evaluation::load(
1785
                    null,
1786
                    null,
1787
                    $courseId,
1788
                    $this->id,
1789
                    null
1790
                );
1791
            }
1792
        }
1793
1794
        if ($recursive) {
1795
            $subcats = $this->get_subcategories(
1796
                $studentId,
1797
                $courseId,
1798
                $sessionId
1799
            );
1800
1801
            if (!empty($subcats)) {
1802
                foreach ($subcats as $subcat) {
1803
                    /* @var Category $subcat */
1804
                    $subevals = $subcat->get_evaluations(
1805
                        $studentId,
1806
                        true,
1807
                        $courseId
1808
                    );
1809
                    $evals = array_merge($evals, $subevals);
1810
                }
1811
            }
1812
        }
1813
1814
        return $evals;
1815
    }
1816
1817
    /**
1818
     * Get appropriate links visible for the user.
1819
     *
1820
     * @param ?int    $studentId   student id (default: all students)
1821
     * @param ?bool   $recursive   process subcategories (default: no recursion)
1822
     * @param ?int $courseId
1823
     * @param ?int    $sessionId
1824
     *
1825
     * @return array
1826
     */
1827
    public function get_links(
1828
        ?int $studentId = null,
1829
        ?bool $recursive = false,
1830
        ?int $courseId = 0,
1831
        ?int $sessionId = 0
1832
    ): array
1833
    {
1834
        $links = [];
1835
        $courseId = empty($courseId) ? $this->getCourseId() : $courseId;
1836
        $sessionId = empty($sessionId) ? $this->get_session_id() : $sessionId;
1837
1838
        // no links in root or course independent categories
1839
        if (0 == $this->id) {
1840
        } elseif (isset($studentId)) {
1841
            // 1 student $studentId
1842
            $links = LinkFactory::load(
1843
                null,
1844
                null,
1845
                null,
1846
                null,
1847
                empty($courseId) ? null : $courseId,
1848
                $this->id,
1849
                api_is_allowed_to_edit() ? null : 1
1850
            );
1851
        } else {
1852
            // All students -> only for course/platform admin
1853
            $links = LinkFactory::load(
1854
                null,
1855
                null,
1856
                null,
1857
                null,
1858
                empty($courseId) ? null : $courseId,
1859
                $this->id,
1860
                null
1861
            );
1862
        }
1863
1864
        if ($recursive) {
1865
            $subcats = $this->get_subcategories(
1866
                $studentId,
1867
                $courseId,
1868
                $sessionId
1869
            );
1870
            if (!empty($subcats)) {
1871
                /** @var Category $subcat */
1872
                foreach ($subcats as $subcat) {
1873
                    $sublinks = $subcat->get_links(
1874
                        $studentId,
1875
                        false,
1876
                        $courseId,
1877
                        $sessionId
1878
                    );
1879
                    $links = array_merge($links, $sublinks);
1880
                }
1881
            }
1882
        }
1883
1884
        return $links;
1885
    }
1886
1887
    /**
1888
     * Get all the categories from with the same given direct parent.
1889
     *
1890
     * @param int $catId Category parent ID
1891
     *
1892
     * @return array Array of Category objects
1893
     */
1894
    public function getCategories($catId)
1895
    {
1896
        $catId = (int) $catId;
1897
        $tblGradeCategories = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
1898
        $sql = 'SELECT * FROM '.$tblGradeCategories.'
1899
                WHERE parent_id = '.$catId;
1900
1901
        $result = Database::query($sql);
1902
        $categories = self::create_category_objects_from_sql_result($result);
1903
1904
        return $categories;
1905
    }
1906
1907
    /**
1908
     * Gets the type for the current object.
1909
     *
1910
     * @return string 'C' to represent "Category" object type
1911
     */
1912
    public function get_item_type()
1913
    {
1914
        return 'C';
1915
    }
1916
1917
    /**
1918
     * @param array $skills
1919
     */
1920
    public function set_skills($skills)
1921
    {
1922
        $this->skills = $skills;
1923
    }
1924
1925
    public function get_date()
1926
    {
1927
        return null;
1928
    }
1929
1930
    /**
1931
     * @return string
1932
     */
1933
    public function get_icon_name()
1934
    {
1935
        return 'cat';
1936
    }
1937
1938
    /**
1939
     * Find category by name.
1940
     *
1941
     * @param string $name_mask search string
1942
     *
1943
     * @return array category objects matching the search criterium
1944
     */
1945
    public static function find_category($name_mask, $allcat)
1946
    {
1947
        $categories = [];
1948
        foreach ($allcat as $search_cat) {
1949
            if (!(false === strpos(strtolower($search_cat->get_name()), strtolower($name_mask)))) {
1950
                $categories[] = $search_cat;
1951
            }
1952
        }
1953
1954
        return $categories;
1955
    }
1956
1957
    /**
1958
     * This function, locks a category , only one who can unlock it is
1959
     * the platform administrator.
1960
     *
1961
     * @param int $locked locked = 1, unlocked = 0
1962
     *
1963
     * @return void
1964
     *
1965
     * @throws Exception
1966
     */
1967
    public function lock(int $locked): void
1968
    {
1969
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
1970
        $sql = "UPDATE $table SET locked = '".intval($locked)."'
1971
                WHERE id = ".$this->id;
1972
        Database::query($sql);
1973
    }
1974
1975
    /**
1976
     * @param $locked
1977
     * @throws \Doctrine\ORM\Exception\ORMException
1978
     * @throws Exception
1979
     */
1980
    public function lockAllItems($locked)
1981
    {
1982
        if ('true' == api_get_setting('gradebook_locking_enabled')) {
1983
            $this->lock($locked);
1984
            $evals_to_lock = $this->get_evaluations();
1985
            if (!empty($evals_to_lock)) {
1986
                foreach ($evals_to_lock as $item) {
1987
                    $item->lock($locked);
1988
                }
1989
            }
1990
1991
            $link_to_lock = $this->get_links();
1992
            if (!empty($link_to_lock)) {
1993
                foreach ($link_to_lock as $item) {
1994
                    $item->lock($locked);
1995
                }
1996
            }
1997
1998
            $event_type = LOG_GRADEBOOK_UNLOCKED;
1999
            if (1 == $locked) {
2000
                $event_type = LOG_GRADEBOOK_LOCKED;
2001
            }
2002
            Event::addEvent($event_type, LOG_GRADEBOOK_ID, $this->id);
0 ignored issues
show
Bug introduced by
The method addEvent() does not exist on Event. ( Ignorable by Annotation )

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

2002
            Event::/** @scrutinizer ignore-call */ 
2003
                   addEvent($event_type, LOG_GRADEBOOK_ID, $this->id);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2003
        }
2004
    }
2005
2006
    /**
2007
     * Generates a certificate for this user if everything matches.
2008
     */
2009
    public static function generateUserCertificate(
2010
        GradebookCategory $category,
2011
        int $user_id,
2012
        bool $sendNotification = false,
2013
        bool $skipGenerationIfExists = false
2014
    ) {
2015
        $categoryId = $category->getId();
2016
        $sessionId = $category->getSession() ? $category->getSession()->getId() : 0;
2017
        $courseId = $category->getCourse()->getId();
2018
2019
        // check if all min_score requirements are met
2020
        if (!self::userMeetsMinimumScores($user_id, $category)) {
2021
            return false; // Do not generate certificate if the user does not meet all min_score criteria
2022
        }
2023
2024
        $skillToolEnabled = SkillModel::hasAccessToUserSkill(api_get_user_id(), $user_id);
2025
        $userHasSkills = false;
2026
        if ($skillToolEnabled) {
2027
            $skill = new SkillModel();
2028
            $skill->addSkillToUser($user_id, $category, $courseId, $sessionId);
2029
            $objSkillRelUser = new SkillRelUserModel();
2030
            $userSkills = $objSkillRelUser->getUserSkills($user_id, $courseId, $sessionId);
2031
            $userHasSkills = !empty($userSkills);
2032
        }
2033
2034
        // If certificate generation is disabled, return only badge link (if available)
2035
        if (empty($category->getGenerateCertificates())) {
2036
            if ($userHasSkills) {
2037
                return [
2038
                    'badge_link' => Display::toolbarButton(
2039
                        get_lang('Export badges'),
2040
                        api_get_path(WEB_CODE_PATH)."gradebook/get_badges.php?user=$user_id",
2041
                        'open-in-new'
2042
                    ),
2043
                ];
2044
            }
2045
2046
            return false;
2047
        }
2048
        $my_certificate = GradebookUtils::get_certificate_by_user_id($categoryId, $user_id);
2049
2050
        // If certificate already exists and we should skip regeneration, return false
2051
        if ($skipGenerationIfExists && !empty($my_certificate)) {
2052
            return false;
2053
        }
2054
2055
        $catArr = Category::load($categoryId);
2056
        $catObj = $catArr[0] ?? null;
2057
2058
        $scoreForCertificate = 0.0;
2059
        if ($catObj) {
2060
            $scoreForCertificate = self::calculateFlatViewTotalPercent($catObj, (int) $user_id);
2061
        }
2062
2063
        GradebookUtils::registerUserInfoAboutCertificate(
2064
            (int) $categoryId,
2065
            (int) $user_id,
2066
            (float) $scoreForCertificate,
2067
            api_get_utc_datetime()
2068
        );
2069
2070
// Now fetch the (possibly existing) certificate
2071
        $my_certificate = GradebookUtils::get_certificate_by_user_id($categoryId, $user_id);
2072
2073
        $html = [];
2074
        if (!empty($my_certificate)) {
2075
            $pathToCertificate = $category->getDocument()->getResourceNode()->getResourceFiles()->first()->getFile()->getPathname();
2076
2077
            $certificate_obj = new Certificate(
2078
                $my_certificate['id'],
2079
                0,
2080
                $sendNotification,
2081
                true,
2082
                $pathToCertificate
2083
            );
2084
2085
            $fileWasGenerated = $certificate_obj->isHtmlFileGenerated();
2086
2087
            // Fix when using a custom certificate plugin
2088
            if ('true' === api_get_plugin_setting('customcertificate', 'enable_plugin_customcertificate')) {
2089
                $infoCertificate = CustomCertificatePlugin::getCertificateData($my_certificate['id'], $user_id);
2090
                if (!empty($infoCertificate)) {
2091
                    $fileWasGenerated = true;
2092
                }
2093
            }
2094
2095
            $isOwner = api_get_user_id() == $user_id;
2096
            $isPlatformAdmin = api_is_platform_admin();
2097
            $isCourseAdmin = api_is_course_admin($courseId);
2098
2099
            $canViewCertificate = $isOwner || $isPlatformAdmin || $isCourseAdmin || !empty($my_certificate['publish']);
2100
2101
            if (!empty($fileWasGenerated) && $canViewCertificate) {
2102
                $certificates = '';
2103
                $exportToPDF = null;
2104
                $pdfUrl = null;
2105
2106
                if (!empty($my_certificate['pathCertificate'])) {
2107
                    $hash = pathinfo($my_certificate['pathCertificate'], PATHINFO_FILENAME);
2108
2109
                    $url = api_get_path(WEB_PATH) . 'certificates/' . $hash . '.html';
0 ignored issues
show
Bug introduced by
Are you sure $hash of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

2109
                    $url = api_get_path(WEB_PATH) . 'certificates/' . /** @scrutinizer ignore-type */ $hash . '.html';
Loading history...
2110
                    $pdfUrl = api_get_path(WEB_PATH) . 'certificates/' . $hash . '.pdf';
2111
2112
                    $certificates = Display::toolbarButton(
2113
                        get_lang('Display certificate'),
2114
                        $url,
2115
                        'eye',
2116
                        'primary',
2117
                        ['target' => '_blank']
2118
                    );
2119
2120
                    $exportToPDF = Display::url(
2121
                        Display::getMdiIcon(ActionIcon::EXPORT_PDF, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Export to PDF')),
2122
                        $pdfUrl,
2123
                        ['target' => '_blank']
2124
                    );
2125
                }
2126
2127
                $hideExportLink = api_get_setting('certificate.hide_certificate_export_link');
2128
                $hideExportLinkStudent = api_get_setting('gradebook.hide_certificate_export_link_students');
2129
                if ('true' === $hideExportLink || (api_is_student() && 'true' === $hideExportLinkStudent)) {
2130
                    $exportToPDF = null;
2131
                }
2132
2133
                $html = [
2134
                    'certificate_link' => $certificates,
2135
                    'pdf_link' => $exportToPDF,
2136
                    'pdf_url' => $pdfUrl,
2137
                ];
2138
            }
2139
2140
            if ($skillToolEnabled && $userHasSkills) {
2141
                $html['badge_link'] = Display::toolbarButton(
2142
                    get_lang('Export badges'),
2143
                    api_get_path(WEB_CODE_PATH)."gradebook/get_badges.php?user=$user_id",
2144
                    'open-in-new'
2145
                );
2146
            }
2147
2148
            return $html;
2149
        }
2150
2151
        return false;
2152
    }
2153
2154
2155
    private static function calculateFlatViewTotalPercent(Category $cat, int $userId): float
2156
    {
2157
        $sessionId = api_get_session_id();
2158
        $courseId = api_get_course_int_id();
2159
2160
        $parentId = (int) $cat->get_parent_id();
2161
        $allcat = $cat->get_subcategories(null, $courseId, $sessionId, 'ORDER BY id');
2162
2163
        // Root category with visible subcategories.
2164
        if ($parentId === 0 && !empty($allcat)) {
2165
            $itemValueTotal = 0.0;
2166
            $itemTotal = 0.0;
2167
2168
            foreach ($allcat as $subCat) {
2169
                $isVisible = true;
2170
                if (method_exists($subCat, 'is_visible')) {
2171
                    $isVisible = (bool) $subCat->is_visible();
2172
                } elseif (method_exists($subCat, 'get_visible')) {
2173
                    $isVisible = (bool) $subCat->get_visible();
2174
                }
2175
2176
                $subWeight = (float) $subCat->get_weight();
2177
                if (!$isVisible || $subWeight <= 0) {
2178
                    continue;
2179
                }
2180
2181
                $score = $subCat->calc_score($userId);
2182
                $den = (isset($score[1]) && (float) $score[1] > 0) ? (float) $score[1] : 0.0;
2183
                $num = isset($score[0]) ? (float) $score[0] : 0.0;
2184
2185
                $ratio = ($den > 0.0) ? ($num / $den) : 0.0;
2186
2187
                $itemValueTotal += $ratio * $subWeight;
2188
                $itemTotal += $subWeight;
2189
            }
2190
2191
            $percent = ($itemTotal > 0.0) ? (($itemValueTotal / $itemTotal) * 100.0) : 0.0;
2192
2193
            return round($percent, 2);
2194
        }
2195
2196
        // Fallback: use category calc_score for non-root or no-subcat cases.
2197
        $total = $cat->calc_score($userId);
2198
        $den = (isset($total[1]) && (float) $total[1] > 0) ? (float) $total[1] : 0.0;
2199
        $num = isset($total[0]) ? (float) $total[0] : 0.0;
2200
2201
        $percent = ($den > 0.0) ? (($num / $den) * 100.0) : 0.0;
2202
2203
        return round($percent, 2);
2204
    }
2205
2206
    /**
2207
     * Checks whether the user has met the minimum score (`min_score`) in all required evaluations.
2208
     */
2209
    public static function userMeetsMinimumScores(int $userId, GradebookCategory $category): bool
2210
    {
2211
        $evaluations = $category->getEvaluations();
2212
2213
        foreach ($evaluations as $evaluation) {
2214
            $minScore = $evaluation->getMinScore();
2215
            if ($minScore !== null) {
2216
                $userScore = self::getUserScoreForEvaluation($userId, $evaluation->getId());
2217
                if ($userScore === null || $userScore < $minScore) {
2218
                    return false; // If at least one evaluation is below `min_score`, return false
2219
                }
2220
            }
2221
        }
2222
2223
        return true;
2224
    }
2225
2226
    /**
2227
     * Retrieves the score of a user for a specific evaluation using the GradebookResult repository.
2228
     */
2229
    public static function getUserScoreForEvaluation(int $userId, int $evaluationId): ?float
2230
    {
2231
        $gradebookResultRepo = Container::getGradebookResultRepository();
2232
2233
        $gradebookResult = $gradebookResultRepo->findOneBy([
2234
            'user' => $userId,
2235
            'evaluation' => $evaluationId,
2236
        ]);
2237
2238
        return $gradebookResult ? $gradebookResult->getScore() : null;
2239
    }
2240
2241
    /**
2242
     * @param int   $catId
2243
     * @param array $userList
2244
     */
2245
    public static function exportAllCertificates($catId, $userList = [])
2246
    {
2247
        $orientation = api_get_setting('certificate.certificate_pdf_orientation');
2248
2249
        $params['orientation'] = 'landscape';
0 ignored issues
show
Comprehensibility Best Practice introduced by
$params was never initialized. Although not strictly required by PHP, it is generally a good practice to add $params = array(); before regardless.
Loading history...
2250
        if (!empty($orientation)) {
2251
            $params['orientation'] = $orientation;
2252
        }
2253
2254
        $params['left'] = 0;
2255
        $params['right'] = 0;
2256
        $params['top'] = 0;
2257
        $params['bottom'] = 0;
2258
        $page_format = 'landscape' == $params['orientation'] ? 'A4-L' : 'A4';
2259
        $pdf = new PDF($page_format, $params['orientation'], $params);
2260
        if ('true' === api_get_setting('certificate.add_certificate_pdf_footer')) {
2261
            $pdf->setCertificateFooter();
2262
        }
2263
        $certificate_list = GradebookUtils::get_list_users_certificates($catId, $userList);
2264
        $certificate_path_list = [];
2265
2266
        if (!empty($certificate_list)) {
2267
            foreach ($certificate_list as $index => $value) {
2268
                $list_certificate = GradebookUtils::get_list_gradebook_certificates_by_user_id(
2269
                    $value['user_id'],
2270
                    $catId
2271
                );
2272
                foreach ($list_certificate as $value_certificate) {
2273
                    $certificate_obj = new Certificate($value_certificate['id']);
2274
                    $certificate_obj->generate(['hide_print_button' => true]);
2275
                    if ($certificate_obj->isHtmlFileGenerated()) {
2276
                        $certificate_path_list[] = $certificate_obj->html_file;
2277
                    }
2278
                }
2279
            }
2280
        }
2281
2282
        if (!empty($certificate_path_list)) {
2283
            // Print certificates (without the common header/footer/watermark
2284
            //  stuff) and return as one multiple-pages PDF
2285
            $pdf->html_to_pdf(
2286
                $certificate_path_list,
2287
                get_lang('Certificates'),
2288
                null,
2289
                false,
2290
                false
2291
            );
2292
        }
2293
    }
2294
2295
    /**
2296
     * @param int $catId
2297
     */
2298
    public static function deleteAllCertificates($catId)
2299
    {
2300
        $certificate_list = GradebookUtils::get_list_users_certificates($catId);
2301
        if (!empty($certificate_list)) {
2302
            foreach ($certificate_list as $index => $value) {
2303
                $list_certificate = GradebookUtils::get_list_gradebook_certificates_by_user_id(
2304
                    $value['user_id'],
2305
                    $catId
2306
                );
2307
                foreach ($list_certificate as $value_certificate) {
2308
                    $certificate_obj = new Certificate($value_certificate['id']);
2309
                    $certificate_obj->delete(true);
2310
                }
2311
            }
2312
        }
2313
    }
2314
2315
    /**
2316
     * Check whether a user has finished a course by its gradebook.
2317
     */
2318
    public static function userFinishedCourse(
2319
        int $userId,
2320
        GradebookCategory $category,
2321
        bool $recalculateScore = false,
2322
        ?int $courseId = null,
2323
        ?int $sessionId = null
2324
    ): bool {
2325
        $currentScore = self::getCurrentScore(
2326
            $userId,
2327
            $category,
2328
            $recalculateScore,
2329
            $courseId,
2330
            $sessionId
2331
        );
2332
2333
        $minCertificateScore = $category->getCertifMinScore();
2334
2335
        return $currentScore >= $minCertificateScore;
2336
    }
2337
2338
    /**
2339
     * Get the current score (as percentage) on a gradebook category for a user.
2340
     */
2341
    public static function getCurrentScore(
2342
        int               $userId,
2343
        GradebookCategory $category,
2344
        bool              $recalculate = false,
2345
        ?int              $courseId = null,
2346
        ?int              $sessionId = null
2347
    ): float|int {
2348
2349
        if ($recalculate) {
2350
            return self::calculateCurrentScore(
2351
                $userId,
2352
                $category,
2353
                $courseId,
2354
                $sessionId
2355
            );
2356
        }
2357
2358
        $resultData = Database::select(
2359
            '*',
2360
            Database::get_main_table(TABLE_MAIN_GRADEBOOK_SCORE_LOG),
2361
            [
2362
                'where' => [
2363
                    'category_id = ? AND user_id = ?' => [$category->getId(), $userId],
2364
                ],
2365
                'order' => 'registered_at DESC',
2366
                'limit' => '1',
2367
            ],
2368
            'first'
2369
        );
2370
2371
        if (empty($resultData)) {
2372
            return 0;
2373
        }
2374
2375
        return $resultData['score'];
2376
    }
2377
2378
    /**
2379
     * Register the current score for a user on a category gradebook.
2380
     *
2381
     * @param float $score      The achieved score
2382
     * @param int   $userId     The user id
2383
     * @param int   $categoryId The gradebook category
2384
     *
2385
     * @return int The insert id
2386
     */
2387
    public static function registerCurrentScore($score, $userId, $categoryId)
2388
    {
2389
        return Database::insert(
0 ignored issues
show
Bug Best Practice introduced by
The expression return Database::insert(...pi_get_utc_datetime())) could also return false which is incompatible with the documented return type integer. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
2390
            Database::get_main_table(TABLE_MAIN_GRADEBOOK_SCORE_LOG),
2391
            [
2392
                'category_id' => intval($categoryId),
2393
                'user_id' => intval($userId),
2394
                'score' => api_float_val($score),
2395
                'registered_at' => api_get_utc_datetime(),
2396
            ]
2397
        );
2398
    }
2399
2400
    /**
2401
     * @return array
2402
     */
2403
    public function getStudentList()
2404
    {
2405
        return $this->studentList;
2406
    }
2407
2408
    /**
2409
     * @param array $list
2410
     */
2411
    public function setStudentList($list)
2412
    {
2413
        $this->studentList = $list;
2414
    }
2415
2416
    /**
2417
     * @return string
2418
     */
2419
    public static function getUrl()
2420
    {
2421
        $url = Session::read('gradebook_dest');
2422
        if (empty($url)) {
2423
            // We guess the link
2424
            $courseInfo = api_get_course_info();
2425
            if (!empty($courseInfo)) {
2426
                return api_get_path(WEB_CODE_PATH).'gradebook/index.php?'.api_get_cidreq().'&';
2427
            } else {
2428
                return api_get_path(WEB_CODE_PATH).'gradebook/gradebook.php?';
2429
            }
2430
        }
2431
2432
        return $url;
2433
    }
2434
2435
    /**
2436
     * Destination is index.php or gradebook.php.
2437
     *
2438
     * @param string $url
2439
     */
2440
    public static function setUrl($url)
2441
    {
2442
        switch ($url) {
2443
            case 'gradebook.php':
2444
                $url = api_get_path(WEB_CODE_PATH).'gradebook/gradebook.php?';
2445
                break;
2446
            case 'index.php':
2447
                $url = api_get_path(WEB_CODE_PATH).'gradebook/index.php?'.api_get_cidreq().'&';
2448
                break;
2449
        }
2450
        Session::write('gradebook_dest', $url);
2451
    }
2452
2453
    /**
2454
     * @return int
2455
     */
2456
    public function getGradeBooksToValidateInDependence()
2457
    {
2458
        return $this->gradeBooksToValidateInDependence;
2459
    }
2460
2461
    /**
2462
     * @param int $value
2463
     *
2464
     * @return Category
2465
     */
2466
    public function setGradeBooksToValidateInDependence($value)
2467
    {
2468
        $this->gradeBooksToValidateInDependence = $value;
2469
2470
        return $this;
2471
    }
2472
2473
    /**
2474
     * Return HTML code with links to download and view certificate.
2475
     *
2476
     * @return string
2477
     */
2478
    public static function getDownloadCertificateBlock(array $certificate)
2479
    {
2480
        if (!isset($certificate['pdf_url'])) {
2481
            return '';
2482
        }
2483
2484
        $downloadLink = Display::toolbarButton(
2485
            get_lang('Download certificate in PDF'),
2486
            $certificate['pdf_url'],
2487
            'file-pdf-box'
2488
        );
2489
        $viewLink = $certificate['certificate_link'];
2490
2491
        return "
2492
            <div class='panel panel-default'>
2493
                <div class='panel-body'>
2494
                    <h3 class='text-center'>".get_lang('You can now download your certificate by clicking here')."</h3>
2495
                    <div class='text-center'>$downloadLink $viewLink</div>
2496
                </div>
2497
            </div>
2498
        ";
2499
    }
2500
2501
    /**
2502
     * Find a gradebook category by the certificate ID.
2503
     *
2504
     * @param int $id certificate id
2505
     *
2506
     * @throws \Doctrine\ORM\NonUniqueResultException
2507
     *
2508
     * @return Category|null
2509
     */
2510
    public static function findByCertificate($id)
2511
    {
2512
        $category = Database::getManager()
2513
            ->createQuery('SELECT c.catId FROM ChamiloCoreBundle:GradebookCertificate c WHERE c.id = :id')
2514
            ->setParameters(['id' => $id])
2515
            ->getOneOrNullResult();
2516
2517
        if (empty($category)) {
2518
            return null;
2519
        }
2520
2521
        $category = self::load($category['catId']);
2522
2523
        if (empty($category)) {
2524
            return null;
2525
        }
2526
2527
        return $category[0];
2528
    }
2529
2530
    /**
2531
     * @param int $value
2532
     */
2533
    public function setDocumentId($value)
2534
    {
2535
        $this->documentId = (int) $value;
2536
    }
2537
2538
    /**
2539
     * @return int
2540
     */
2541
    public function getDocumentId()
2542
    {
2543
        return $this->documentId;
2544
    }
2545
2546
    /**
2547
     * Get the remaining weight in root category.
2548
     *
2549
     * @return int
2550
     */
2551
    public function getRemainingWeight()
2552
    {
2553
        $subCategories = $this->get_subcategories();
2554
2555
        $subWeight = 0;
2556
2557
        /** @var Category $subCategory */
2558
        foreach ($subCategories as $subCategory) {
2559
            $subWeight += $subCategory->get_weight();
2560
        }
2561
2562
        return $this->weight - $subWeight;
2563
    }
2564
2565
    /**
2566
     * @return int
2567
     */
2568
    public function getCourseId()
2569
    {
2570
        return $this->courseId;
2571
    }
2572
2573
    /**
2574
     * Sets both the course ID and course code. If course ID is empty, set both to null;
2575
     * @param ?int $courseId
2576
     *
2577
     * @return Category
2578
     */
2579
    public function setCourseId(?int $courseId = null): Category
2580
    {
2581
        $courseInfo = api_get_course_info_by_id($courseId);
2582
        if (!empty($courseInfo)) {
2583
            $this->course_code = $courseInfo['code'];
2584
            $this->courseId = $courseId;
2585
        } else {
2586
            $this->course_code = null;
2587
            $this->courseId = null;
2588
        }
2589
2590
        return $this;
2591
    }
2592
2593
    /**
2594
     * @return Category
2595
     */
2596
    private static function create_root_category()
2597
    {
2598
        $cat = new Category();
2599
        $cat->set_id(0);
2600
        $cat->set_name(get_lang('Main folder'));
2601
        $cat->set_description('');
2602
        $cat->set_user_id(0);
2603
        $cat->setCourseId(0);
2604
        $cat->set_parent_id(0);
2605
        $cat->set_weight(0);
2606
        $cat->set_visible(1);
2607
        $cat->setGenerateCertificates(0);
2608
        $cat->setIsRequirement(0);
2609
2610
        return $cat;
2611
    }
2612
2613
    /**
2614
     * @param ?Doctrine\DBAL\Result $result
2615
     *
2616
     * @return array
2617
     * @throws \Doctrine\DBAL\Exception
2618
     */
2619
    private static function create_category_objects_from_sql_result(?Doctrine\DBAL\Result $result)
2620
    {
2621
        $categories = [];
2622
        $allow = ('true' === api_get_setting('gradebook.allow_gradebook_stats'));
2623
        if ($allow) {
2624
            $em = Database::getManager();
2625
            $repo = $em->getRepository(GradebookCategory::class);
2626
        }
2627
2628
        if (!empty($result)) {
2629
            while ($data = Database::fetch_array($result)) {
2630
                $cat = new Category();
2631
                $cat->set_id($data['id']);
2632
                $cat->set_name($data['title']);
2633
                $cat->set_description($data['description']);
2634
                $cat->set_user_id($data['user_id']);
2635
                $cat->setCourseId($data['c_id']);
2636
                $cat->set_parent_id($data['parent_id']);
2637
                $cat->set_weight($data['weight']);
2638
                $cat->set_visible($data['visible']);
2639
                $cat->set_session_id($data['session_id']);
2640
                $cat->set_certificate_min_score($data['certif_min_score']);
2641
                $cat->set_grade_model_id((int) $data['grade_model_id']);
2642
                $cat->set_locked($data['locked']);
2643
                $cat->setGenerateCertificates($data['generate_certificates']);
2644
                $cat->setIsRequirement($data['is_requirement']);
2645
                //$cat->setCourseListDependency(isset($data['depends']) ? $data['depends'] : []);
2646
                $cat->setMinimumToValidate(isset($data['minimum_to_validate']) ? $data['minimum_to_validate'] : null);
2647
                $cat->setGradeBooksToValidateInDependence(isset($data['gradebooks_to_validate_in_dependence']) ? $data['gradebooks_to_validate_in_dependence'] : null);
2648
                $cat->setDocumentId($data['document_id']);
2649
                if ($allow) {
2650
                    $cat->entity = $repo->find($data['id']);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $repo does not seem to be defined for all execution paths leading up to this point.
Loading history...
2651
                }
2652
2653
                $categories[] = $cat;
2654
            }
2655
        }
2656
2657
        return $categories;
2658
    }
2659
2660
    /**
2661
     * Internal function used by get_target_categories().
2662
     *
2663
     * @param array $targets
2664
     * @param int   $level
2665
     * @param int   $catid
2666
     *
2667
     * @return array
2668
     */
2669
    private function addTargetSubcategories($targets, $level, $catid)
2670
    {
2671
        $subcats = self::load(null, null, 0, $catid);
2672
        foreach ($subcats as $cat) {
2673
            if ($this->can_be_moved_to_cat($cat)) {
2674
                $targets[] = [
2675
                    $cat->get_id(),
2676
                    $cat->get_name(),
2677
                    $level + 1,
2678
                ];
2679
                $targets = $this->addTargetSubcategories(
2680
                    $targets,
2681
                    $level + 1,
2682
                    $cat->get_id()
2683
                );
2684
            }
2685
        }
2686
2687
        return $targets;
2688
    }
2689
2690
    /**
2691
     * Internal function used by get_target_categories() and addTargetSubcategories()
2692
     * Can this category be moved to the given category ?
2693
     * Impossible when origin and target are the same... children won't be processed
2694
     * either. (a category can't be moved to one of its own children).
2695
     */
2696
    private function can_be_moved_to_cat($cat)
2697
    {
2698
        return $cat->get_id() != $this->get_id();
2699
    }
2700
2701
    /**
2702
     * Internal function used by move_to_cat().
2703
     */
2704
    private function applyCourseCodeToChildren()
2705
    {
2706
        $cats = self::load(null, null, 0, $this->id, null);
2707
        $evals = Evaluation::load(null, null, 0, $this->id, null);
2708
        $links = LinkFactory::load(
2709
            null,
2710
            null,
2711
            null,
2712
            null,
2713
            0,
2714
            $this->id,
2715
            null
2716
        );
2717
        /** @var Category $cat */
2718
        foreach ($cats as $cat) {
2719
            $cat->setCourseId($this->getCourseId());
2720
            $cat->save();
2721
            $cat->applyCourseCodeToChildren();
2722
        }
2723
2724
        foreach ($evals as $eval) {
2725
            $eval->setCourseId($this->getCourseId());
2726
            $eval->save();
2727
        }
2728
2729
        foreach ($links as $link) {
2730
            $link->delete();
2731
        }
2732
    }
2733
2734
    /**
2735
     * Internal function used by get_tree().
2736
     *
2737
     * @param int      $level
2738
     * @param int|null $visible
2739
     *
2740
     * @return array
2741
     */
2742
    private function add_subtree($targets, $level, $catid, $visible)
2743
    {
2744
        $subcats = self::load(null, null, 0, $catid, $visible);
2745
2746
        if (!empty($subcats)) {
2747
            foreach ($subcats as $cat) {
2748
                $targets[] = [
2749
                    $cat->get_id(),
2750
                    $cat->get_name(),
2751
                    $level + 1,
2752
                ];
2753
                $targets = self::add_subtree(
0 ignored issues
show
Bug Best Practice introduced by
The method Category::add_subtree() 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

2753
                /** @scrutinizer ignore-call */ 
2754
                $targets = self::add_subtree(
Loading history...
2754
                    $targets,
2755
                    $level + 1,
2756
                    $cat->get_id(),
2757
                    $visible
2758
                );
2759
            }
2760
        }
2761
2762
        return $targets;
2763
    }
2764
2765
    /**
2766
     * Calculate the current score on a gradebook category for a user.
2767
     *
2768
     * @return float The score
2769
     */
2770
    private static function calculateCurrentScore(
2771
        int $userId,
2772
        ?GradebookCategory $category = null,
2773
        ?int $courseId = null,
2774
        ?int $sessionId = null,
2775
    ): float|int {
2776
2777
        if (null === $category) {
2778
            return 0;
2779
        }
2780
2781
        $categoryList = self::load(
2782
            null,
2783
            null,
2784
            $courseId,
2785
            null,
2786
            null,
2787
            $sessionId
2788
        );
2789
2790
        /* @var Category $category */
2791
        $category = $categoryList[0] ?? null;
2792
2793
        if (null === $category) {
2794
            return 0;
2795
        }
2796
2797
        $courseEvaluations = $category->get_evaluations($userId, true);
2798
        $courseLinks = $category->get_links($userId, true);
2799
        $evaluationsAndLinks = array_merge($courseEvaluations, $courseLinks);
2800
        $count = count($evaluationsAndLinks);
2801
        if (empty($count)) {
2802
            return 0;
2803
        }
2804
2805
        $categoryScore = 0;
2806
        for ($i = 0; $i < $count; $i++) {
2807
            /** @var AbstractLink $item */
2808
            $item = $evaluationsAndLinks[$i];
2809
            // Set session id from category
2810
            $item->set_session_id($category->get_session_id());
2811
            $score = $item->calc_score($userId);
2812
            $itemValue = 0;
2813
            if (!empty($score)) {
2814
                $divider = 0 == $score[1] ? 1 : $score[1];
2815
                $itemValue = $score[0] / $divider * $item->get_weight();
2816
            }
2817
2818
            $categoryScore += $itemValue;
2819
        }
2820
2821
        return api_float_val($categoryScore);
2822
    }
2823
}
2824