Passed
Pull Request — master (#7119)
by
unknown
09:22
created

Category::calculateFlatViewTotalPercent()   F

Complexity

Conditions 19
Paths 232

Size

Total Lines 57
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 33
c 0
b 0
f 0
nc 232
nop 2
dl 0
loc 57
rs 3.2833

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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->setCourse($course);
525
            $category->setParent($parent);
526
            $category->setWeight(api_float_val($this->weight));
527
            $category->setVisible((bool) $this->visible);
528
            $category->setCertifMinScore($this->certificate_min_score);
529
            $category->setSession(api_get_session_entity($this->session_id));
530
            $category->setGenerateCertificates($this->generateCertificates);
531
            if (!empty($this->grade_model_id)) {
532
                $model = $em->getRepository(\Chamilo\CoreBundle\Entity\GradeModel::class)->find($this->grade_model_id);
533
                $category->setGradeModel($model);
534
            }
535
536
            $category->setIsRequirement($this->isRequirement);
537
            $category->setLocked(0);
538
539
            $em->persist($category);
540
            $em->flush();
541
542
            $id = $category->getId();
543
            $this->set_id($id);
544
545
            if (!empty($id)) {
546
                $parent_id = $this->get_parent_id();
547
                $grade_model_id = $this->get_grade_model_id();
548
                if (0 == $parent_id) {
549
                    //do something
550
                    if (isset($grade_model_id) &&
551
                        !empty($grade_model_id) &&
552
                        '-1' != $grade_model_id
553
                    ) {
554
                        $obj = new GradeModel();
555
                        $components = $obj->get_components($grade_model_id);
556
                        $default_weight_setting = api_get_setting('gradebook_default_weight');
557
                        $default_weight = 100;
558
                        if (isset($default_weight_setting)) {
559
                            $default_weight = $default_weight_setting;
560
                        }
561
                        foreach ($components as $component) {
562
                            $gradebook = new Gradebook();
563
                            $params = [];
564
565
                            $params['name'] = $component['acronym'];
566
                            $params['description'] = $component['title'];
567
                            $params['user_id'] = api_get_user_id();
568
                            $params['parent_id'] = $id;
569
                            $params['weight'] = $component['percentage'] / 100 * $default_weight;
570
                            $params['session_id'] = api_get_session_id();
571
                            $params['c_id'] = $this->getCourseId();
572
573
                            $gradebook->save($params);
574
                        }
575
                    }
576
                }
577
            }
578
579
            $gradebook = new Gradebook();
580
            $gradebook->updateSkillsToGradeBook(
581
                $this->id,
582
                $this->get_skills(false)
583
            );
584
585
            return $id;
586
        }
587
    }
588
589
    /**
590
     * Update the properties of this category in the database.
591
     *
592
     * @throws \Doctrine\ORM\Exception\NotSupported
593
     * @throws \Doctrine\ORM\Exception\ORMException
594
     * @todo fix me
595
     */
596
    public function save()
597
    {
598
        $em = Database::getManager();
599
        $repo = $em->getRepository(GradebookCategory::class);
600
601
        /** @var GradebookCategory $category */
602
        $category = $repo->find($this->id);
603
604
        if (null === $category) {
605
            return false;
606
        }
607
608
        $parent = null;
609
        if (!empty($this->parent)) {
610
            $parent = $repo->find($this->parent);
611
        }
612
        $course = api_get_course_entity();
613
614
        $category->setTitle($this->name);
615
        $category->setDescription($this->description);
616
        $userId = is_numeric($this->user_id) ? (int) $this->user_id : api_get_user_id();
617
        $category->setUser(api_get_user_entity($userId));
618
        $category->setCourse($course);
619
        $category->setParent($parent);
620
        $category->setWeight($this->weight);
621
        $category->setVisible($this->visible);
622
        $category->setCertifMinScore($this->certificate_min_score);
623
        $category->setGenerateCertificates($this->generateCertificates);
624
        if (!empty($this->grade_model_id)) {
625
            $model = $em->getRepository(\Chamilo\CoreBundle\Entity\GradeModel::class)->find($this->grade_model_id);
626
            $category->setGradeModel($model);
627
        }
628
629
        $category->setIsRequirement($this->isRequirement);
630
631
        $em->persist($category);
632
        $em->flush();
633
634
        if (!empty($this->id)) {
635
            $parent_id = $this->get_parent_id();
636
            $grade_model_id = $this->get_grade_model_id();
637
            if (0 == $parent_id) {
638
                if (!empty($grade_model_id) &&
639
                    '-1' != $grade_model_id
640
                ) {
641
                    $obj = new GradeModel();
642
                    $components = $obj->get_components($grade_model_id);
643
                    $default_weight_setting = api_get_setting('gradebook_default_weight');
644
                    $default_weight = 100;
645
                    if (isset($default_weight_setting)) {
646
                        $default_weight = $default_weight_setting;
647
                    }
648
                    $final_weight = $this->get_weight();
649
                    if (!empty($final_weight)) {
650
                        $default_weight = $this->get_weight();
651
                    }
652
                    foreach ($components as $component) {
653
                        $gradebook = new Gradebook();
654
                        $params = [];
655
                        $params['name'] = $component['acronym'];
656
                        $params['description'] = $component['title'];
657
                        $params['user_id'] = api_get_user_id();
658
                        $params['parent_id'] = $this->id;
659
                        $params['weight'] = $component['percentage'] / 100 * $default_weight;
660
                        $params['session_id'] = api_get_session_id();
661
                        $params['c_id'] = $this->getCourseId();
662
                        $gradebook->save($params);
663
                    }
664
                }
665
            }
666
        }
667
668
        $gradebook = new Gradebook();
669
        $gradebook->updateSkillsToGradeBook(
670
            $this->id,
671
            $this->get_skills(false),
672
            true
673
        );
674
    }
675
676
    /**
677
     * Update link weights see #5168.
678
     *
679
     * @param int $new_weight
680
     */
681
    public function updateChildrenWeight($new_weight)
682
    {
683
        $links = $this->get_links();
684
        $old_weight = $this->get_weight();
685
686
        if (!empty($links)) {
687
            foreach ($links as $link_item) {
688
                if (isset($link_item)) {
689
                    $new_item_weight = $new_weight * $link_item->get_weight() / $old_weight;
690
                    $link_item->set_weight($new_item_weight);
691
                    $link_item->save();
692
                }
693
            }
694
        }
695
    }
696
697
    /**
698
     * Delete this evaluation from the database.
699
     * @throws Exception
700
     */
701
    public function delete(): void
702
    {
703
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
704
        $sql = 'DELETE FROM '.$table.' WHERE id = '.intval($this->id);
705
        Database::query($sql);
706
    }
707
708
    /**
709
     * Return an HTML span block if the given resource has been deleted
710
     * @param ?int $courseId
711
     *
712
     * @return string
713
     * @throws \Doctrine\DBAL\Exception
714
     * @throws Exception
715
     */
716
    public static function show_message_resource_delete(?int $courseId): string
717
    {
718
        if (empty($courseId)) {
719
            return '';
720
        }
721
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
722
        $sql = "SELECT count(*) AS num
723
                FROM $table
724
                WHERE
725
                    c_id = $courseId AND
726
                    visible = 3";
727
        $res = Database::query($sql);
728
        $option = Database::fetch_array($res);
729
        if ($option['num'] >= 1) {
730
            return '&nbsp;&nbsp;<span class="resource-deleted">
731
                (&nbsp;'.get_lang('The resource has been deleted').'&nbsp;)
732
                </span>';
733
        }
734
735
        return '';
736
    }
737
738
    /**
739
     * Shows all information of a category.
740
     *
741
     * @param int $categoryId
742
     *
743
     * @return array
744
     * @throws \Doctrine\DBAL\Exception
745
     * @throws Exception
746
     */
747
    public function showAllCategoryInfo(int $categoryId): array
748
    {
749
        if (empty($categoryId)) {
750
            return [];
751
        }
752
753
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
754
        $sql = "SELECT * FROM $table
755
                WHERE id = $categoryId";
756
        $result = Database::query($sql);
757
758
        return Database::fetch_array($result);
759
    }
760
761
    /**
762
     * Checks if the certificate is available for the given user in this category.
763
     *
764
     * @param int $user_id User ID
765
     *
766
     * @return bool True if conditions match, false if fails
767
     */
768
    public function is_certificate_available($user_id)
769
    {
770
        $score = $this->calc_score(
771
            $user_id,
772
            null,
773
            $this->courseId,
774
            $this->session_id
775
        );
776
777
        if (isset($score) && isset($score[0])) {
778
            // Get a percentage score to compare to minimum certificate score
779
            // $certification_score = $score[0] / $score[1] * 100;
780
            // Get real score not a percentage.
781
            $certification_score = $score[0];
782
            if ($certification_score >= $this->certificate_min_score) {
783
                return true;
784
            }
785
        }
786
787
        return false;
788
    }
789
790
    /**
791
     * Is this category the main one in a course ?
792
     * A category is a course if it has a course code and no parent category.
793
     */
794
    public function is_course(): bool
795
    {
796
        return !empty($this->getCourseId())
797
            && (!isset($this->parent) || 0 == $this->parent);
798
    }
799
800
    /**
801
     * Calculate the score of this category.
802
     */
803
    public function calc_score(
804
        ?int $studentId = null,
805
        ?string $type = null,
806
        ?int $courseId = 0,
807
        ?int $session_id = null
808
    ): ?array {
809
        $key = 'category:'.$this->id.'student:'.(int) $studentId.'type:'.$type.'course:'.$courseId.'session:'.(int) $session_id;
810
        $useCache = ('true' === api_get_setting('gradebook.gradebook_use_apcu_cache'));
811
        $cacheAvailable = api_get_configuration_value('apc') && $useCache;
812
813
        if ($cacheAvailable) {
814
            $cache = new \Symfony\Component\Cache\Adapter\ApcuAdapter();
815
            if ($cache->hasItem($key)) {
816
                return $cache->getItem($key)->get();
817
            }
818
        }
819
        // Classic
820
        if (!empty($studentId) && '' == $type) {
821
            if (!empty($courseId)) {
822
                $cats = $this->get_subcategories(
823
                    $studentId,
824
                    $courseId,
825
                    $session_id
826
                );
827
                $evals = $this->get_evaluations($studentId, false, $courseId);
828
                $links = $this->get_links($studentId, false, $courseId);
829
            } else {
830
                $cats = $this->get_subcategories($studentId);
831
                $evals = $this->get_evaluations($studentId);
832
                $links = $this->get_links($studentId);
833
            }
834
835
            // Calculate score
836
            $count = 0;
837
            $ressum = 0;
838
            $weightsum = 0;
839
            if (!empty($cats)) {
840
                /** @var Category $cat */
841
                foreach ($cats as $cat) {
842
                    $cat->set_session_id($session_id);
843
                    $cat->setCourseId($courseId);
844
                    $cat->setStudentList($this->getStudentList());
845
                    $score = $cat->calc_score(
846
                        $studentId,
847
                        null,
848
                        $courseId,
849
                        $session_id
850
                    );
851
852
                    $catweight = 0;
853
                    if (0 != $cat->get_weight()) {
854
                        $catweight = $cat->get_weight();
855
                        $weightsum += $catweight;
856
                    }
857
858
                    if (isset($score) && !empty($score[1]) && !empty($catweight)) {
859
                        $ressum += $score[0] / $score[1] * $catweight;
860
                    }
861
                }
862
            }
863
864
            if (!empty($evals)) {
865
                /** @var Evaluation $eval */
866
                foreach ($evals as $eval) {
867
                    $eval->setStudentList($this->getStudentList());
868
                    $evalres = $eval->calc_score($studentId);
869
                    if (isset($evalres) && 0 != $eval->get_weight()) {
870
                        $evalweight = $eval->get_weight();
871
                        $weightsum += $evalweight;
872
                        if (!empty($evalres[1])) {
873
                            $ressum += $evalres[0] / $evalres[1] * $evalweight;
874
                        }
875
                    } else {
876
                        if (0 != $eval->get_weight()) {
877
                            $evalweight = $eval->get_weight();
878
                            $weightsum += $evalweight;
879
                        }
880
                    }
881
                }
882
            }
883
884
            if (!empty($links)) {
885
                /** @var EvalLink|ExerciseLink $link */
886
                foreach ($links as $link) {
887
                    $link->setStudentList($this->getStudentList());
888
889
                    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...
890
                        $link->set_session_id($session_id);
891
                    }
892
893
                    $linkres = $link->calc_score($studentId, null);
894
                    if (!empty($linkres) && 0 != $link->get_weight()) {
895
                        $linkweight = $link->get_weight();
896
                        $link_res_denom = 0 == $linkres[1] ? 1 : $linkres[1];
897
                        $weightsum += $linkweight;
898
                        $ressum += $linkres[0] / $link_res_denom * $linkweight;
899
                    } else {
900
                        // Adding if result does not exists
901
                        if (0 != $link->get_weight()) {
902
                            $linkweight = $link->get_weight();
903
                            $weightsum += $linkweight;
904
                        }
905
                    }
906
                }
907
            }
908
        } else {
909
            if (!empty($courseId)) {
910
                $cats = $this->get_subcategories(
911
                    null,
912
                    $courseId,
913
                    $session_id
914
                );
915
                $evals = $this->get_evaluations(null, false, $courseId);
916
                $links = $this->get_links(null, false, $courseId);
917
            } else {
918
                $cats = $this->get_subcategories(null);
919
                $evals = $this->get_evaluations(null);
920
                $links = $this->get_links(null);
921
            }
922
923
            // Calculate score
924
            $ressum = 0;
925
            $weightsum = 0;
926
            $bestResult = 0;
927
            $totalScorePerStudent = [];
928
929
            if (!empty($cats)) {
930
                /** @var Category $cat */
931
                foreach ($cats as $cat) {
932
                    $cat->setStudentList($this->getStudentList());
933
                    $score = $cat->calc_score(
934
                        null,
935
                        $type,
936
                        $courseId,
937
                        $session_id
938
                    );
939
940
                    $catweight = 0;
941
                    if (0 != $cat->get_weight()) {
942
                        $catweight = $cat->get_weight();
943
                        $weightsum += $catweight;
944
                    }
945
946
                    if (isset($score) && !empty($score[1]) && !empty($catweight)) {
947
                        $ressum += $score[0] / $score[1] * $catweight;
948
949
                        if ($ressum > $bestResult) {
950
                            $bestResult = $ressum;
951
                        }
952
                    }
953
                }
954
            }
955
956
            if (!empty($evals)) {
957
                if ('best' === $type) {
958
                    $studentList = $this->getStudentList();
959
                    foreach ($studentList as $student) {
960
                        $studentId = $student['user_id'];
961
                        foreach ($evals as $eval) {
962
                            $linkres = $eval->calc_score($studentId, null);
963
                            $linkweight = $eval->get_weight();
964
                            $link_res_denom = 0 == $linkres[1] ? 1 : $linkres[1];
965
                            $ressum = $linkres[0] / $link_res_denom * $linkweight;
966
967
                            if (!isset($totalScorePerStudent[$studentId])) {
968
                                $totalScorePerStudent[$studentId] = 0;
969
                            }
970
                            $totalScorePerStudent[$studentId] += $ressum;
971
                        }
972
                    }
973
                } else {
974
                    /** @var Evaluation $eval */
975
                    foreach ($evals as $eval) {
976
                        $evalres = $eval->calc_score(null, $type);
977
                        $eval->setStudentList($this->getStudentList());
978
979
                        if (isset($evalres) && 0 != $eval->get_weight()) {
980
                            $evalweight = $eval->get_weight();
981
                            $weightsum += $evalweight;
982
                            if (!empty($evalres[1])) {
983
                                $ressum += $evalres[0] / $evalres[1] * $evalweight;
984
                            }
985
986
                            if ($ressum > $bestResult) {
987
                                $bestResult = $ressum;
988
                            }
989
                        } else {
990
                            if (0 != $eval->get_weight()) {
991
                                $evalweight = $eval->get_weight();
992
                                $weightsum += $evalweight;
993
                            }
994
                        }
995
                    }
996
                }
997
            }
998
999
            if (!empty($links)) {
1000
                $studentList = $this->getStudentList();
1001
                if ('best' === $type) {
1002
                    foreach ($studentList as $student) {
1003
                        $studentId = $student['user_id'];
1004
                        foreach ($links as $link) {
1005
                            $linkres = $link->calc_score($studentId, null);
1006
                            $linkweight = $link->get_weight();
1007
                            if ($linkres) {
1008
                                $link_res_denom = 0 == $linkres[1] ? 1 : $linkres[1];
1009
                                $ressum = $linkres[0] / $link_res_denom * $linkweight;
1010
                            }
1011
1012
                            if (!isset($totalScorePerStudent[$studentId])) {
1013
                                $totalScorePerStudent[$studentId] = 0;
1014
                            }
1015
                            $totalScorePerStudent[$studentId] += $ressum;
1016
                        }
1017
                    }
1018
                } else {
1019
                    /** @var EvalLink|ExerciseLink $link */
1020
                    foreach ($links as $link) {
1021
                        $link->setStudentList($this->getStudentList());
1022
1023
                        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...
1024
                            $link->set_session_id($session_id);
1025
                        }
1026
1027
                        $linkres = $link->calc_score($studentId, $type);
1028
1029
                        if (!empty($linkres) && 0 != $link->get_weight()) {
1030
                            $linkweight = $link->get_weight();
1031
                            $link_res_denom = 0 == $linkres[1] ? 1 : $linkres[1];
1032
1033
                            $weightsum += $linkweight;
1034
                            $ressum += $linkres[0] / $link_res_denom * $linkweight;
1035
                            if ($ressum > $bestResult) {
1036
                                $bestResult = $ressum;
1037
                            }
1038
                        } else {
1039
                            // Adding if result does not exist
1040
                            if (0 != $link->get_weight()) {
1041
                                $linkweight = $link->get_weight();
1042
                                $weightsum += $linkweight;
1043
                            }
1044
                        }
1045
                    }
1046
                }
1047
            }
1048
        }
1049
1050
        switch ($type) {
1051
            case 'best':
1052
                arsort($totalScorePerStudent);
1053
                $maxScore = current($totalScorePerStudent);
1054
1055
                return [$maxScore, $this->get_weight()];
1056
                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...
1057
            case 'average':
1058
                if (empty($ressum)) {
1059
                    if ($cacheAvailable) {
1060
                        $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...
1061
                        $cacheItem->set(null);
1062
1063
                        $cache->save($cacheItem);
1064
                    }
1065
1066
                    return null;
1067
                }
1068
1069
                if ($cacheAvailable) {
1070
                    $cacheItem = $cache->getItem($key);
1071
                    $cacheItem->set([$ressum, $weightsum]);
1072
1073
                    $cache->save($cacheItem);
1074
                }
1075
1076
                return [$ressum, $weightsum];
1077
                //break;
1078
            case 'ranking':
1079
                // category ranking is calculated in gradebook_data_generator.class.php
1080
                // function get_data
1081
                return null;
1082
1083
                //return AbstractLink::getCurrentUserRanking($studentId, []);
1084
                //break;
1085
            default:
1086
                if ($cacheAvailable) {
1087
                    $cacheItem = $cache->getItem($key);
1088
                    $cacheItem->set([$ressum, $weightsum]);
1089
1090
                    $cache->save($cacheItem);
1091
                }
1092
1093
                return [$ressum, $weightsum];
1094
        }
1095
    }
1096
1097
    /**
1098
     * Delete this category and every subcategory, evaluation and result inside.
1099
     */
1100
    public function delete_all()
1101
    {
1102
        $cats = self::load(null, null, $this->getCourseId(), $this->id, null);
1103
        $evals = Evaluation::load(
1104
            null,
1105
            null,
1106
            $this->getCourseId(),
1107
            $this->id,
1108
            null
1109
        );
1110
1111
        $links = LinkFactory::load(
1112
            null,
1113
            null,
1114
            null,
1115
            null,
1116
            $this->getCourseId(),
1117
            $this->id,
1118
            null
1119
        );
1120
1121
        if (!empty($cats)) {
1122
            /** @var Category $cat */
1123
            foreach ($cats as $cat) {
1124
                $cat->delete_all();
1125
                $cat->delete();
1126
            }
1127
        }
1128
1129
        if (!empty($evals)) {
1130
            /** @var Evaluation $eval */
1131
            foreach ($evals as $eval) {
1132
                $eval->delete_with_results();
1133
            }
1134
        }
1135
1136
        if (!empty($links)) {
1137
            /** @var AbstractLink $link */
1138
            foreach ($links as $link) {
1139
                $link->delete();
1140
            }
1141
        }
1142
1143
        $this->delete();
1144
    }
1145
1146
    /**
1147
     * Return array of Category objects where a student is subscribed to.
1148
     *
1149
     * @param int $stud_id
1150
     * @param int $courseId
1151
     * @param int $session_id
1152
     *
1153
     * @return array
1154
     * @throws \Doctrine\DBAL\Exception
1155
     */
1156
    public function get_root_categories_for_student(
1157
        int $stud_id,
1158
        int $courseId = 0,
1159
        int $session_id = 0
1160
    ) {
1161
        $main_course_user_table = Database::get_main_table(TABLE_MAIN_COURSE_USER);
1162
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
1163
1164
        $sql = "SELECT * FROM $table WHERE parent_id = 0";
1165
1166
        if (!api_is_allowed_to_edit()) {
1167
            $sql .= ' AND visible = 1';
1168
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
                $sql .= " AND c_id = $courseId";
1176
1177
                if (!empty($session_id)) {
1178
                    $sql .= " AND session_id = $session_id";
1179
                } else {
1180
                    $sql .= " AND (session_id IS NULL OR session_id = 0)";
1181
                }
1182
            } else {
1183
                // No optional parameter, proceed as usual
1184
                $sql .= ' AND c_id IN (
1185
                SELECT cu.c_id
1186
                FROM '.$main_course_user_table.' cu
1187
                WHERE cu.user_id = '.$stud_id.'
1188
                AND cu.status = '.STUDENT.'
1189
            )';
1190
            }
1191
        } elseif (!api_is_platform_admin()) {
1192
            // Proceed with checks on optional parameters course & session
1193
            if (!empty($courseId)) {
1194
                // TODO: considering it highly improbable that a user would get here
1195
                // if he doesn't have the rights to view this course and this
1196
                // session, we don't check his registration to these, but this
1197
                // could be an improvement
1198
                $sql .= " AND c_id = $courseId";
1199
                if (!empty($session_id)) {
1200
                    $sql .= " AND session_id = $session_id";
1201
                } else {
1202
                    $sql .= " AND (session_id IS NULL OR session_id = 0)";
1203
                }
1204
            } else {
1205
                $sql .= ' AND c_id IN (
1206
                SELECT cu.c_id
1207
                FROM '.$main_course_user_table.' cu
1208
                WHERE cu.user_id = '.(int) api_get_user_id().'
1209
                AND cu.status = '.COURSEMANAGER.'
1210
            )';
1211
            }
1212
        } else {
1213
            // Platform admin
1214
            if (!empty($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
        $user_id = (int) $user_id;
1258
        $courseId = isset($courseId) ? (int) $courseId : null;
1259
        $session_id = isset($session_id) ? (int) $session_id : null;
1260
1261
        $sql = 'SELECT * FROM '.$tbl_grade_categories.' WHERE parent_id = 0';
1262
1263
        if (!empty($courseId)) {
1264
            $sql .= " AND c_id = $courseId";
1265
            if (!empty($session_id)) {
1266
                $sql .= " AND session_id = $session_id";
1267
            } else {
1268
                $sql .= ' AND (session_id IS NULL OR session_id = 0)';
1269
            }
1270
        } else {
1271
            $sql .= ' AND c_id IN (
1272
            SELECT cu.c_id
1273
            FROM '.$main_course_user_table.' cu
1274
            WHERE cu.user_id = '.$user_id.'
1275
        )';
1276
        }
1277
        $result = Database::query($sql);
1278
        $cats = self::create_category_objects_from_sql_result($result);
1279
1280
        // Course independent categories
1281
        if (!empty($courseId)) {
1282
            $indcats = self::load(
1283
                null,
1284
                $user_id,
1285
                $courseId,
1286
                0,
1287
                null,
1288
                $session_id
1289
            );
1290
            $cats = array_merge($cats, $indcats);
1291
        }
1292
1293
        return $cats;
1294
    }
1295
1296
    /**
1297
     * Can this category be moved to somewhere else ?
1298
     * The root and courses cannot be moved.
1299
     *
1300
     * @return bool
1301
     */
1302
    public function is_movable()
1303
    {
1304
        return !(!isset($this->id) || 0 == $this->id || $this->is_course());
1305
    }
1306
1307
    /**
1308
     * Generate an array of possible categories where this category can be moved to.
1309
     * Notice: its own parent will be included in the list: it's up to the frontend
1310
     * to disable this element.
1311
     *
1312
     * @return array 2-dimensional array - every element contains 3 subelements (id, name, level)
1313
     */
1314
    public function get_target_categories()
1315
    {
1316
        // the root or a course -> not movable
1317
        if (!$this->is_movable()) {
1318
            return null;
1319
        } else {
1320
            // otherwise:
1321
            // - course independent category
1322
            //   -> movable to root or other independent categories
1323
            // - category inside a course
1324
            //   -> movable to root, independent categories or categories inside the course
1325
            $user = api_is_platform_admin() ? null : api_get_user_id();
1326
            $targets = [];
1327
            $level = 0;
1328
1329
            $root = [0, get_lang('Main folder'), $level];
1330
            $targets[] = $root;
1331
1332
            if (!empty($this->courseId)) {
1333
                $crscats = self::load(null, null, $this->courseId, 0);
1334
                foreach ($crscats as $cat) {
1335
                    if ($this->can_be_moved_to_cat($cat)) {
1336
                        $targets[] = [
1337
                            $cat->get_id(),
1338
                            $cat->get_name(),
1339
                            $level + 1,
1340
                        ];
1341
                        $targets = $this->addTargetSubcategories(
1342
                            $targets,
1343
                            $level + 1,
1344
                            $cat->get_id()
1345
                        );
1346
                    }
1347
                }
1348
            }
1349
1350
            $indcats = self::load(null, $user, 0, 0);
1351
            foreach ($indcats as $cat) {
1352
                if ($this->can_be_moved_to_cat($cat)) {
1353
                    $targets[] = [$cat->get_id(), $cat->get_name(), $level + 1];
1354
                    $targets = $this->addTargetSubcategories(
1355
                        $targets,
1356
                        $level + 1,
1357
                        $cat->get_id()
1358
                    );
1359
                }
1360
            }
1361
1362
            return $targets;
1363
        }
1364
    }
1365
1366
    /**
1367
     * Move this category to the given category.
1368
     * If this category moves from inside a course to outside,
1369
     * its course code must be changed, as well as the course code
1370
     * of all underlying categories and evaluations. All links will
1371
     * be deleted as well !
1372
     */
1373
    public function move_to_cat($cat)
1374
    {
1375
        $this->set_parent_id($cat->get_id());
1376
        if ($this->get_course_code() != $cat->get_course_code()) {
1377
            $this->setCourseId($cat->getCourseId());
1378
            $this->applyCourseCodeToChildren();
1379
        }
1380
        $this->save();
1381
    }
1382
1383
    /**
1384
     * Generate an array of all categories the user can navigate to.
1385
     */
1386
    public function get_tree()
1387
    {
1388
        $targets = [];
1389
        $level = 0;
1390
        $root = [0, get_lang('Main folder'), $level];
1391
        $targets[] = $root;
1392
1393
        // course or platform admin
1394
        if (api_is_allowed_to_edit()) {
1395
            $user = api_is_platform_admin() ? null : api_get_user_id();
1396
            $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

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

2008
            Event::/** @scrutinizer ignore-call */ 
2009
                   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...
2009
        }
2010
    }
2011
2012
    /**
2013
     * Generates a certificate for this user if everything matches.
2014
     */
2015
    public static function generateUserCertificate(
2016
        GradebookCategory $category,
2017
        int $user_id,
2018
        bool $sendNotification = false,
2019
        bool $skipGenerationIfExists = false
2020
    ) {
2021
        $categoryId = (int) $category->getId();
2022
        $sessionId  = $category->getSession() ? (int) $category->getSession()->getId() : 0;
2023
        $courseId   = (int) $category->getCourse()->getId();
2024
2025
        // Load legacy Category object to compute score safely.
2026
        $catArr = Category::load($categoryId);
2027
        $catObj = $catArr[0] ?? null;
2028
2029
        $scoreForCertificate = 0.0;
2030
        if ($catObj) {
2031
            $scoreForCertificate = (float) self::calculateFlatViewTotalPercent($catObj, $user_id);
2032
        }
2033
2034
        // Guard: never generate certificate OR award skills if the global certificate threshold is not met.
2035
        $minCertificationScore = (float) $category->getCertifMinScore();
2036
        if ($minCertificationScore > 0.0 && $scoreForCertificate < $minCertificationScore) {
2037
            return false;
2038
        }
2039
2040
        // Guard: per-item min_score requirements must be met.
2041
        if (!self::userMeetsMinimumScores($user_id, $category)) {
2042
            return false;
2043
        }
2044
2045
        // If certificate already exists and we should skip regeneration, return false.
2046
        $my_certificate = GradebookUtils::get_certificate_by_user_id($categoryId, $user_id);
2047
        if ($skipGenerationIfExists && !empty($my_certificate)) {
2048
            return false;
2049
        }
2050
2051
        // Award skills only after ALL requirements are met.
2052
        $skillToolEnabled = SkillModel::hasAccessToUserSkill(api_get_user_id(), $user_id);
2053
        $userHasSkills = false;
2054
        if ($skillToolEnabled) {
2055
            $skill = new SkillModel();
2056
            $skill->addSkillToUser($user_id, $category, $courseId, $sessionId);
2057
            $objSkillRelUser = new SkillRelUserModel();
2058
            $userSkills = $objSkillRelUser->getUserSkills($user_id, $courseId, $sessionId);
2059
            $userHasSkills = !empty($userSkills);
2060
        }
2061
2062
        // If certificate generation is disabled, return only badge link (if available).
2063
        if (empty($category->getGenerateCertificates())) {
2064
            if ($userHasSkills) {
2065
                return [
2066
                    'badge_link' => Display::toolbarButton(
2067
                        get_lang('Export badges'),
2068
                        api_get_path(WEB_CODE_PATH) . "gradebook/get_badges.php?user=$user_id",
2069
                        'open-in-new'
2070
                    ),
2071
                ];
2072
            }
2073
2074
            return false;
2075
        }
2076
2077
        // Store score info (used to display/track generation moment).
2078
        GradebookUtils::registerUserInfoAboutCertificate(
2079
            $categoryId,
2080
            $user_id,
2081
            $scoreForCertificate,
2082
            api_get_utc_datetime()
2083
        );
2084
2085
        // Now fetch the (possibly existing) certificate.
2086
        $my_certificate = GradebookUtils::get_certificate_by_user_id($categoryId, $user_id);
2087
2088
        if (!empty($my_certificate)) {
2089
            $pathToCertificate = $category
2090
                ->getDocument()
2091
                ->getResourceNode()
2092
                ->getResourceFiles()
2093
                ->first()
2094
                ->getFile()
2095
                ->getPathname();
2096
2097
            $certificate_obj = new Certificate(
2098
                $my_certificate['id'],
2099
                0,
2100
                $sendNotification,
2101
                true,
2102
                $pathToCertificate
2103
            );
2104
2105
            $fileWasGenerated = $certificate_obj->isHtmlFileGenerated();
2106
2107
            if ('true' === api_get_plugin_setting('customcertificate', 'enable_plugin_customcertificate')) {
2108
                $infoCertificate = CustomCertificatePlugin::getCertificateData($my_certificate['id'], $user_id);
2109
                if (!empty($infoCertificate)) {
2110
                    $fileWasGenerated = true;
2111
                }
2112
            }
2113
2114
            $isOwner = api_get_user_id() == $user_id;
2115
            $isPlatformAdmin = api_is_platform_admin();
2116
            $isCourseAdmin = api_is_course_admin($courseId);
2117
2118
            $canViewCertificate = $isOwner || $isPlatformAdmin || $isCourseAdmin || !empty($my_certificate['publish']);
2119
2120
            if (!empty($fileWasGenerated) && $canViewCertificate) {
2121
                $certificates = '';
2122
                $exportToPDF = null;
2123
                $pdfUrl = null;
2124
2125
                if (!empty($my_certificate['pathCertificate'])) {
2126
                    $hash = pathinfo($my_certificate['pathCertificate'], PATHINFO_FILENAME);
2127
2128
                    $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

2128
                    $url = api_get_path(WEB_PATH) . 'certificates/' . /** @scrutinizer ignore-type */ $hash . '.html';
Loading history...
2129
                    $pdfUrl = api_get_path(WEB_PATH) . 'certificates/' . $hash . '.pdf';
2130
2131
                    $certificates = Display::toolbarButton(
2132
                        get_lang('Display certificate'),
2133
                        $url,
2134
                        'eye',
2135
                        'primary',
2136
                        ['target' => '_blank']
2137
                    );
2138
2139
                    $exportToPDF = Display::url(
2140
                        Display::getMdiIcon(ActionIcon::EXPORT_PDF, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Export to PDF')),
2141
                        $pdfUrl,
2142
                        ['target' => '_blank']
2143
                    );
2144
                }
2145
2146
                $hideExportLink = api_get_setting('certificate.hide_certificate_export_link');
2147
                $hideExportLinkStudent = api_get_setting('gradebook.hide_certificate_export_link_students');
2148
                if ('true' === $hideExportLink || (api_is_student() && 'true' === $hideExportLinkStudent)) {
2149
                    $exportToPDF = null;
2150
                }
2151
2152
                $html = [
2153
                    'certificate_link' => $certificates,
2154
                    'pdf_link' => $exportToPDF,
2155
                    'pdf_url' => $pdfUrl,
2156
                ];
2157
2158
                if ($skillToolEnabled && $userHasSkills) {
2159
                    $html['badge_link'] = Display::toolbarButton(
2160
                        get_lang('Export badges'),
2161
                        api_get_path(WEB_CODE_PATH) . "gradebook/get_badges.php?user=$user_id",
2162
                        'open-in-new'
2163
                    );
2164
                }
2165
2166
                return $html;
2167
            }
2168
2169
            // If file not generated or cannot view, still allow badges if skills exist.
2170
            if ($skillToolEnabled && $userHasSkills) {
2171
                return [
2172
                    'badge_link' => Display::toolbarButton(
2173
                        get_lang('Export badges'),
2174
                        api_get_path(WEB_CODE_PATH) . "gradebook/get_badges.php?user=$user_id",
2175
                        'open-in-new'
2176
                    ),
2177
                ];
2178
            }
2179
        }
2180
2181
        return false;
2182
    }
2183
2184
    private static function calculateFlatViewTotalPercent(Category $cat, int $userId): float
2185
    {
2186
        $sessionId = api_get_session_id();
2187
        $courseId  = api_get_course_int_id();
2188
2189
        $parentId = (int) $cat->get_parent_id();
2190
        $allcat   = $cat->get_subcategories(null, $courseId, $sessionId, 'ORDER BY id');
2191
2192
        // Root category with visible subcategories.
2193
        if ($parentId === 0 && !empty($allcat)) {
2194
            $itemValueTotal = 0.0;
2195
            $itemTotal      = 0.0;
2196
2197
            foreach ($allcat as $subCat) {
2198
                $isVisible = true;
2199
                if (method_exists($subCat, 'is_visible')) {
2200
                    $isVisible = (bool) $subCat->is_visible();
2201
                } elseif (method_exists($subCat, 'get_visible')) {
2202
                    $isVisible = (bool) $subCat->get_visible();
2203
                }
2204
2205
                $subWeight = (float) $subCat->get_weight();
2206
                if (!$isVisible || $subWeight <= 0.0) {
2207
                    continue;
2208
                }
2209
2210
                $score = $subCat->calc_score($userId);
2211
                if (!is_array($score)) {
2212
                    $score = [0, 0];
2213
                }
2214
2215
                $den = (isset($score[1]) && (float) $score[1] > 0.0) ? (float) $score[1] : 0.0;
2216
                $num = isset($score[0]) ? (float) $score[0] : 0.0;
2217
2218
                $ratio = ($den > 0.0) ? ($num / $den) : 0.0;
2219
2220
                $itemValueTotal += $ratio * $subWeight;
2221
                $itemTotal      += $subWeight;
2222
            }
2223
2224
            $percent = ($itemTotal > 0.0) ? (($itemValueTotal / $itemTotal) * 100.0) : 0.0;
2225
2226
            return round($percent, 2);
2227
        }
2228
2229
        // Fallback: use category calc_score for non-root or no-subcat cases.
2230
        $total = $cat->calc_score($userId);
2231
        if (!is_array($total)) {
2232
            $total = [0, 0];
2233
        }
2234
2235
        $den = (isset($total[1]) && (float) $total[1] > 0.0) ? (float) $total[1] : 0.0;
2236
        $num = isset($total[0]) ? (float) $total[0] : 0.0;
2237
2238
        $percent = ($den > 0.0) ? (($num / $den) * 100.0) : 0.0;
2239
2240
        return round($percent, 2);
2241
    }
2242
2243
    /**
2244
     * Checks whether the user has met the minimum score (`min_score`) in all required evaluations.
2245
     */
2246
    public static function userMeetsMinimumScores(int $userId, GradebookCategory $category): bool
2247
    {
2248
        $evaluations = $category->getEvaluations();
2249
2250
        foreach ($evaluations as $evaluation) {
2251
            $minScore = $evaluation->getMinScore();
2252
            if ($minScore !== null) {
2253
                $userScore = self::getUserScoreForEvaluation($userId, $evaluation->getId());
2254
                if ($userScore === null || $userScore < $minScore) {
2255
                    return false; // If at least one evaluation is below `min_score`, return false
2256
                }
2257
            }
2258
        }
2259
2260
        return true;
2261
    }
2262
2263
    /**
2264
     * Retrieves the score of a user for a specific evaluation using the GradebookResult repository.
2265
     */
2266
    public static function getUserScoreForEvaluation(int $userId, int $evaluationId): ?float
2267
    {
2268
        $gradebookResultRepo = Container::getGradebookResultRepository();
2269
2270
        $gradebookResult = $gradebookResultRepo->findOneBy([
2271
            'user' => $userId,
2272
            'evaluation' => $evaluationId,
2273
        ]);
2274
2275
        return $gradebookResult ? $gradebookResult->getScore() : null;
2276
    }
2277
2278
    /**
2279
     * @param int   $catId
2280
     * @param array $userList
2281
     */
2282
    public static function exportAllCertificates($catId, $userList = [])
2283
    {
2284
        $orientation = api_get_setting('certificate.certificate_pdf_orientation');
2285
2286
        $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...
2287
        if (!empty($orientation)) {
2288
            $params['orientation'] = $orientation;
2289
        }
2290
2291
        $params['left'] = 0;
2292
        $params['right'] = 0;
2293
        $params['top'] = 0;
2294
        $params['bottom'] = 0;
2295
        $page_format = 'landscape' == $params['orientation'] ? 'A4-L' : 'A4';
2296
        $pdf = new PDF($page_format, $params['orientation'], $params);
2297
        if ('true' === api_get_setting('certificate.add_certificate_pdf_footer')) {
2298
            $pdf->setCertificateFooter();
2299
        }
2300
        $certificate_list = GradebookUtils::get_list_users_certificates($catId, $userList);
2301
        $certificate_path_list = [];
2302
2303
        if (!empty($certificate_list)) {
2304
            foreach ($certificate_list as $index => $value) {
2305
                $list_certificate = GradebookUtils::get_list_gradebook_certificates_by_user_id(
2306
                    $value['user_id'],
2307
                    $catId
2308
                );
2309
                foreach ($list_certificate as $value_certificate) {
2310
                    $certificate_obj = new Certificate($value_certificate['id']);
2311
                    $certificate_obj->generate(['hide_print_button' => true]);
2312
                    if ($certificate_obj->isHtmlFileGenerated()) {
2313
                        $certificate_path_list[] = $certificate_obj->html_file;
2314
                    }
2315
                }
2316
            }
2317
        }
2318
2319
        if (!empty($certificate_path_list)) {
2320
            // Print certificates (without the common header/footer/watermark
2321
            //  stuff) and return as one multiple-pages PDF
2322
            $pdf->html_to_pdf(
2323
                $certificate_path_list,
2324
                get_lang('Certificates'),
2325
                null,
2326
                false,
2327
                false
2328
            );
2329
        }
2330
    }
2331
2332
    /**
2333
     * @param int $catId
2334
     */
2335
    public static function deleteAllCertificates($catId)
2336
    {
2337
        $certificate_list = GradebookUtils::get_list_users_certificates($catId);
2338
        if (!empty($certificate_list)) {
2339
            foreach ($certificate_list as $index => $value) {
2340
                $list_certificate = GradebookUtils::get_list_gradebook_certificates_by_user_id(
2341
                    $value['user_id'],
2342
                    $catId
2343
                );
2344
                foreach ($list_certificate as $value_certificate) {
2345
                    $certificate_obj = new Certificate($value_certificate['id']);
2346
                    $certificate_obj->delete(true);
2347
                }
2348
            }
2349
        }
2350
    }
2351
2352
    /**
2353
     * Check whether a user has finished a course by its gradebook.
2354
     */
2355
    public static function userFinishedCourse(
2356
        int $userId,
2357
        GradebookCategory $category,
2358
        bool $recalculateScore = false,
2359
        ?int $courseId = null,
2360
        ?int $sessionId = null
2361
    ): bool {
2362
        $currentScore = self::getCurrentScore(
2363
            $userId,
2364
            $category,
2365
            $recalculateScore,
2366
            $courseId,
2367
            $sessionId
2368
        );
2369
2370
        $minCertificateScore = $category->getCertifMinScore();
2371
2372
        return $currentScore >= $minCertificateScore;
2373
    }
2374
2375
    /**
2376
     * Get the current score (as percentage) on a gradebook category for a user.
2377
     */
2378
    public static function getCurrentScore(
2379
        int               $userId,
2380
        GradebookCategory $category,
2381
        bool              $recalculate = false,
2382
        ?int              $courseId = null,
2383
        ?int              $sessionId = null
2384
    ): float|int {
2385
2386
        if ($recalculate) {
2387
            return self::calculateCurrentScore(
2388
                $userId,
2389
                $category,
2390
                $courseId,
2391
                $sessionId
2392
            );
2393
        }
2394
2395
        $resultData = Database::select(
2396
            '*',
2397
            Database::get_main_table(TABLE_MAIN_GRADEBOOK_SCORE_LOG),
2398
            [
2399
                'where' => [
2400
                    'category_id = ? AND user_id = ?' => [$category->getId(), $userId],
2401
                ],
2402
                'order' => 'registered_at DESC',
2403
                'limit' => '1',
2404
            ],
2405
            'first'
2406
        );
2407
2408
        if (empty($resultData)) {
2409
            return 0;
2410
        }
2411
2412
        return $resultData['score'];
2413
    }
2414
2415
    /**
2416
     * Register the current score for a user on a category gradebook.
2417
     *
2418
     * @param float $score      The achieved score
2419
     * @param int   $userId     The user id
2420
     * @param int   $categoryId The gradebook category
2421
     *
2422
     * @return int The insert id
2423
     */
2424
    public static function registerCurrentScore($score, $userId, $categoryId)
2425
    {
2426
        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...
2427
            Database::get_main_table(TABLE_MAIN_GRADEBOOK_SCORE_LOG),
2428
            [
2429
                'category_id' => intval($categoryId),
2430
                'user_id' => intval($userId),
2431
                'score' => api_float_val($score),
2432
                'registered_at' => api_get_utc_datetime(),
2433
            ]
2434
        );
2435
    }
2436
2437
    /**
2438
     * @return array
2439
     */
2440
    public function getStudentList()
2441
    {
2442
        return $this->studentList;
2443
    }
2444
2445
    /**
2446
     * @param array $list
2447
     */
2448
    public function setStudentList($list)
2449
    {
2450
        $this->studentList = $list;
2451
    }
2452
2453
    /**
2454
     * @return string
2455
     */
2456
    public static function getUrl()
2457
    {
2458
        $url = Session::read('gradebook_dest');
2459
        if (empty($url)) {
2460
            // We guess the link
2461
            $courseInfo = api_get_course_info();
2462
            if (!empty($courseInfo)) {
2463
                return api_get_path(WEB_CODE_PATH).'gradebook/index.php?'.api_get_cidreq().'&';
2464
            } else {
2465
                return api_get_path(WEB_CODE_PATH).'gradebook/gradebook.php?';
2466
            }
2467
        }
2468
2469
        return $url;
2470
    }
2471
2472
    /**
2473
     * Destination is index.php or gradebook.php.
2474
     *
2475
     * @param string $url
2476
     */
2477
    public static function setUrl($url)
2478
    {
2479
        switch ($url) {
2480
            case 'gradebook.php':
2481
                $url = api_get_path(WEB_CODE_PATH).'gradebook/gradebook.php?';
2482
                break;
2483
            case 'index.php':
2484
                $url = api_get_path(WEB_CODE_PATH).'gradebook/index.php?'.api_get_cidreq().'&';
2485
                break;
2486
        }
2487
        Session::write('gradebook_dest', $url);
2488
    }
2489
2490
    /**
2491
     * @return int
2492
     */
2493
    public function getGradeBooksToValidateInDependence()
2494
    {
2495
        return $this->gradeBooksToValidateInDependence;
2496
    }
2497
2498
    /**
2499
     * @param int $value
2500
     *
2501
     * @return Category
2502
     */
2503
    public function setGradeBooksToValidateInDependence($value)
2504
    {
2505
        $this->gradeBooksToValidateInDependence = $value;
2506
2507
        return $this;
2508
    }
2509
2510
    /**
2511
     * Return HTML code with links to download and view certificate.
2512
     *
2513
     * @return string
2514
     */
2515
    public static function getDownloadCertificateBlock(array $certificate)
2516
    {
2517
        if (!isset($certificate['pdf_url'])) {
2518
            return '';
2519
        }
2520
2521
        $pdfUrl = (string) $certificate['pdf_url'];
2522
        $viewLinkHtml = $certificate['certificate_link'] ?? '';
2523
2524
        $downloadLink = "
2525
        <a
2526
            href='".htmlspecialchars($pdfUrl, ENT_QUOTES)."'
2527
            target='_blank'
2528
            rel='noopener noreferrer'
2529
            class='inline-flex items-center justify-center rounded-lg bg-gray-50 px-4 py-2 text-sm font-semibold text-white hover:bg-gray-50'
2530
        >
2531
            ".get_lang('Download certificate in PDF')."
2532
        </a>
2533
    ";
2534
2535
        return "
2536
        <section class='mx-auto max-w-5xl p-4'>
2537
            <div class='rounded-2xl bg-white ring-1 ring-gray-20 shadow-sm p-6'>
2538
                <h3 class='text-center text-xl font-semibold text-gray-900'>
2539
                    ".get_lang('You can now download your certificate by clicking here')."
2540
                </h3>
2541
                <div class='mt-4 flex flex-wrap items-center justify-center gap-3'>
2542
                    $downloadLink
2543
                    $viewLinkHtml
2544
                </div>
2545
            </div>
2546
        </section>
2547
    ";
2548
    }
2549
2550
    /**
2551
     * Find a gradebook category by the certificate ID.
2552
     *
2553
     * @param int $id certificate id
2554
     *
2555
     * @throws \Doctrine\ORM\NonUniqueResultException
2556
     *
2557
     * @return Category|null
2558
     */
2559
    public static function findByCertificate($id)
2560
    {
2561
        $category = Database::getManager()
2562
            ->createQuery('SELECT c.catId FROM ChamiloCoreBundle:GradebookCertificate c WHERE c.id = :id')
2563
            ->setParameters(['id' => $id])
2564
            ->getOneOrNullResult();
2565
2566
        if (empty($category)) {
2567
            return null;
2568
        }
2569
2570
        $category = self::load($category['catId']);
2571
2572
        if (empty($category)) {
2573
            return null;
2574
        }
2575
2576
        return $category[0];
2577
    }
2578
2579
    /**
2580
     * @param int $value
2581
     */
2582
    public function setDocumentId($value)
2583
    {
2584
        $this->documentId = (int) $value;
2585
    }
2586
2587
    /**
2588
     * @return int
2589
     */
2590
    public function getDocumentId()
2591
    {
2592
        return $this->documentId;
2593
    }
2594
2595
    /**
2596
     * Get the remaining weight in root category.
2597
     *
2598
     * @return int
2599
     */
2600
    public function getRemainingWeight()
2601
    {
2602
        $subCategories = $this->get_subcategories();
2603
2604
        $subWeight = 0;
2605
2606
        /** @var Category $subCategory */
2607
        foreach ($subCategories as $subCategory) {
2608
            $subWeight += $subCategory->get_weight();
2609
        }
2610
2611
        return $this->weight - $subWeight;
2612
    }
2613
2614
    /**
2615
     * @return int
2616
     */
2617
    public function getCourseId()
2618
    {
2619
        return $this->courseId;
2620
    }
2621
2622
    /**
2623
     * Sets both the course ID and course code. If course ID is empty, set both to null;
2624
     * @param ?int $courseId
2625
     *
2626
     * @return Category
2627
     */
2628
    public function setCourseId(?int $courseId = null): Category
2629
    {
2630
        $courseInfo = api_get_course_info_by_id($courseId);
2631
        if (!empty($courseInfo)) {
2632
            $this->course_code = $courseInfo['code'];
2633
            $this->courseId = $courseId;
2634
        } else {
2635
            $this->course_code = null;
2636
            $this->courseId = null;
2637
        }
2638
2639
        return $this;
2640
    }
2641
2642
    /**
2643
     * @return Category
2644
     */
2645
    private static function create_root_category()
2646
    {
2647
        $cat = new Category();
2648
        $cat->set_id(0);
2649
        $cat->set_name(get_lang('Main folder'));
2650
        $cat->set_description('');
2651
        $cat->set_user_id(0);
2652
        $cat->setCourseId(0);
2653
        $cat->set_parent_id(0);
2654
        $cat->set_weight(0);
2655
        $cat->set_visible(1);
2656
        $cat->setGenerateCertificates(0);
2657
        $cat->setIsRequirement(0);
2658
2659
        return $cat;
2660
    }
2661
2662
    /**
2663
     * @param ?Doctrine\DBAL\Result $result
2664
     *
2665
     * @return array
2666
     * @throws \Doctrine\DBAL\Exception
2667
     */
2668
    private static function create_category_objects_from_sql_result(?Doctrine\DBAL\Result $result)
2669
    {
2670
        $categories = [];
2671
        $allow = ('true' === api_get_setting('gradebook.allow_gradebook_stats'));
2672
        if ($allow) {
2673
            $em = Database::getManager();
2674
            $repo = $em->getRepository(GradebookCategory::class);
2675
        }
2676
2677
        if (!empty($result)) {
2678
            while ($data = Database::fetch_array($result)) {
2679
                $cat = new Category();
2680
                $cat->set_id($data['id']);
2681
                $cat->set_name($data['title']);
2682
                $cat->set_description($data['description']);
2683
                $cat->set_user_id($data['user_id']);
2684
                $cat->setCourseId($data['c_id']);
2685
                $cat->set_parent_id($data['parent_id']);
2686
                $cat->set_weight($data['weight']);
2687
                $cat->set_visible($data['visible']);
2688
                $cat->set_session_id($data['session_id']);
2689
                $cat->set_certificate_min_score($data['certif_min_score']);
2690
                $cat->set_grade_model_id((int) $data['grade_model_id']);
2691
                $cat->set_locked($data['locked']);
2692
                $cat->setGenerateCertificates($data['generate_certificates']);
2693
                $cat->setIsRequirement($data['is_requirement']);
2694
                //$cat->setCourseListDependency(isset($data['depends']) ? $data['depends'] : []);
2695
                $cat->setMinimumToValidate(isset($data['minimum_to_validate']) ? $data['minimum_to_validate'] : null);
2696
                $cat->setGradeBooksToValidateInDependence(isset($data['gradebooks_to_validate_in_dependence']) ? $data['gradebooks_to_validate_in_dependence'] : null);
2697
                $cat->setDocumentId($data['document_id']);
2698
                if ($allow) {
2699
                    $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...
2700
                }
2701
2702
                $categories[] = $cat;
2703
            }
2704
        }
2705
2706
        return $categories;
2707
    }
2708
2709
    /**
2710
     * Internal function used by get_target_categories().
2711
     *
2712
     * @param array $targets
2713
     * @param int   $level
2714
     * @param int   $catid
2715
     *
2716
     * @return array
2717
     */
2718
    private function addTargetSubcategories($targets, $level, $catid)
2719
    {
2720
        $subcats = self::load(null, null, 0, $catid);
2721
        foreach ($subcats as $cat) {
2722
            if ($this->can_be_moved_to_cat($cat)) {
2723
                $targets[] = [
2724
                    $cat->get_id(),
2725
                    $cat->get_name(),
2726
                    $level + 1,
2727
                ];
2728
                $targets = $this->addTargetSubcategories(
2729
                    $targets,
2730
                    $level + 1,
2731
                    $cat->get_id()
2732
                );
2733
            }
2734
        }
2735
2736
        return $targets;
2737
    }
2738
2739
    /**
2740
     * Internal function used by get_target_categories() and addTargetSubcategories()
2741
     * Can this category be moved to the given category ?
2742
     * Impossible when origin and target are the same... children won't be processed
2743
     * either. (a category can't be moved to one of its own children).
2744
     */
2745
    private function can_be_moved_to_cat($cat)
2746
    {
2747
        return $cat->get_id() != $this->get_id();
2748
    }
2749
2750
    /**
2751
     * Internal function used by move_to_cat().
2752
     */
2753
    private function applyCourseCodeToChildren()
2754
    {
2755
        $cats = self::load(null, null, 0, $this->id, null);
2756
        $evals = Evaluation::load(null, null, 0, $this->id, null);
2757
        $links = LinkFactory::load(
2758
            null,
2759
            null,
2760
            null,
2761
            null,
2762
            0,
2763
            $this->id,
2764
            null
2765
        );
2766
        /** @var Category $cat */
2767
        foreach ($cats as $cat) {
2768
            $cat->setCourseId($this->getCourseId());
2769
            $cat->save();
2770
            $cat->applyCourseCodeToChildren();
2771
        }
2772
2773
        foreach ($evals as $eval) {
2774
            $eval->setCourseId($this->getCourseId());
2775
            $eval->save();
2776
        }
2777
2778
        foreach ($links as $link) {
2779
            $link->delete();
2780
        }
2781
    }
2782
2783
    /**
2784
     * Internal function used by get_tree().
2785
     *
2786
     * @param int      $level
2787
     * @param int|null $visible
2788
     *
2789
     * @return array
2790
     */
2791
    private function add_subtree($targets, $level, $catid, $visible)
2792
    {
2793
        $subcats = self::load(null, null, 0, $catid, $visible);
2794
2795
        if (!empty($subcats)) {
2796
            foreach ($subcats as $cat) {
2797
                $targets[] = [
2798
                    $cat->get_id(),
2799
                    $cat->get_name(),
2800
                    $level + 1,
2801
                ];
2802
                $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

2802
                /** @scrutinizer ignore-call */ 
2803
                $targets = self::add_subtree(
Loading history...
2803
                    $targets,
2804
                    $level + 1,
2805
                    $cat->get_id(),
2806
                    $visible
2807
                );
2808
            }
2809
        }
2810
2811
        return $targets;
2812
    }
2813
2814
    /**
2815
     * Calculate the current score on a gradebook category for a user.
2816
     *
2817
     * @return float The score
2818
     */
2819
    private static function calculateCurrentScore(
2820
        int $userId,
2821
        ?GradebookCategory $category = null,
2822
        ?int $courseId = null,
2823
        ?int $sessionId = null,
2824
    ): float|int {
2825
        if (null === $category) {
2826
            return 0;
2827
        }
2828
2829
        $categoryList = self::load((int) $category->getId(), null, 0, null, null, null, null);
2830
2831
        /** @var Category|null $category */
2832
        $category = $categoryList[0] ?? null;
2833
2834
        if (null === $category) {
2835
            return 0;
2836
        }
2837
2838
        $courseEvaluations = $category->get_evaluations($userId, true);
2839
        $courseLinks = $category->get_links($userId, true);
2840
        $evaluationsAndLinks = array_merge($courseEvaluations, $courseLinks);
2841
        $count = count($evaluationsAndLinks);
2842
2843
        if ($count === 0) {
2844
            return 0;
2845
        }
2846
2847
        $categoryScore = 0.0;
2848
2849
        for ($i = 0; $i < $count; $i++) {
2850
            /** @var AbstractLink $item */
2851
            $item = $evaluationsAndLinks[$i];
2852
2853
            // Keep existing behavior: session comes from the legacy Category object.
2854
            $item->set_session_id($category->get_session_id());
2855
2856
            $weight = (float) $item->get_weight();
2857
            if ($weight <= 0.0) {
2858
                continue;
2859
            }
2860
2861
            $score = $item->calc_score($userId);
2862
2863
            // calc_score() is expected to return [score, max], but can be null/false or incomplete.
2864
            if (!is_array($score) || !array_key_exists(0, $score) || !array_key_exists(1, $score)) {
2865
                continue;
2866
            }
2867
2868
            $num = (float) $score[0];
2869
            $den = (float) $score[1];
2870
2871
            // Critical guard: if max/denominator is 0, the item must NOT contribute.
2872
            // This prevents "free" eligibility when the learner hasn't done anything.
2873
            if ($den <= 0.0) {
2874
                continue;
2875
            }
2876
2877
            $itemValue = ($num / $den) * $weight;
2878
            $categoryScore += $itemValue;
2879
        }
2880
2881
        return api_float_val($categoryScore);
2882
    }
2883
}
2884