Category::setMinimumToValidate()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2705
                /** @scrutinizer ignore-call */ 
2706
                $targets = self::add_subtree(
Loading history...
2706
                    $targets,
2707
                    $level + 1,
2708
                    $cat->get_id(),
2709
                    $visible
2710
                );
2711
            }
2712
        }
2713
2714
        return $targets;
2715
    }
2716
2717
    /**
2718
     * Calculate the current score on a gradebook category for a user.
2719
     *
2720
     * @return float The score
2721
     */
2722
    private static function calculateCurrentScore(
2723
        int $userId,
2724
        ?GradebookCategory $category = null,
2725
        ?int $courseId = null,
2726
        ?int $sessionId = null,
2727
    ): float|int {
2728
2729
        if (null === $category) {
2730
            return 0;
2731
        }
2732
2733
        $categoryList = self::load(
2734
            null,
2735
            null,
2736
            $courseId,
2737
            null,
2738
            null,
2739
            $sessionId
2740
        );
2741
2742
        /* @var Category $category */
2743
        $category = $categoryList[0] ?? null;
2744
2745
        if (null === $category) {
2746
            return 0;
2747
        }
2748
2749
        $courseEvaluations = $category->get_evaluations($userId, true);
2750
        $courseLinks = $category->get_links($userId, true);
2751
        $evaluationsAndLinks = array_merge($courseEvaluations, $courseLinks);
2752
        $count = count($evaluationsAndLinks);
2753
        if (empty($count)) {
2754
            return 0;
2755
        }
2756
2757
        $categoryScore = 0;
2758
        for ($i = 0; $i < $count; $i++) {
2759
            /** @var AbstractLink $item */
2760
            $item = $evaluationsAndLinks[$i];
2761
            // Set session id from category
2762
            $item->set_session_id($category->get_session_id());
2763
            $score = $item->calc_score($userId);
2764
            $itemValue = 0;
2765
            if (!empty($score)) {
2766
                $divider = 0 == $score[1] ? 1 : $score[1];
2767
                $itemValue = $score[0] / $divider * $item->get_weight();
2768
            }
2769
2770
            $categoryScore += $itemValue;
2771
        }
2772
2773
        return api_float_val($categoryScore);
2774
    }
2775
}
2776