aboutsummaryrefslogtreecommitdiff
path: root/lib/models/Statusgruppen.php
blob: c8789a6900e4b494b398c668039189daf04aa239 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
<?php

/**
 * Statusgruppen.php
 * model class for statusgroups.
 * The statusgrouphierarchy is represented by the attributes
 * children and parent
 *
 * Statusgroupmembers are saved as in <code>$this->members</code>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * @author      Florian Bieringer <florian.bieringer@uni-passau.de>
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
 * @category    Stud.IP
 *
 * @property string statusgruppe_id database column
 * @property string id alias column for statusgruppe_id
 * @property string name database column
 * @property string range_id database column
 * @property string position database column
 * @property string size database column
 * @property string selfassign database column
 * @property string selfassign_start database column
 * @property string mkdate database column
 * @property string chdate database column
 * @property string calendar_group database column
 * @property string name_w database column
 * @property string name_m database column
 * @property string children computed column
 * @property SimpleORMapCollection members has_many StatusgruppeUser
 * @property Statusgruppen parent belongs_to Statusgruppen
 * @property Course course belongs_to course
 * @property Institute institute belongs_to institute
 * @property User user belongs_to user
 *
 * @property ConsultationBlock[]|SimpleORMapCollection $consultation_blocks
 * @property ConsultationResponsibility[]|SimpleORMapCollection $consultation_responsibilities
 * @property-read Range|null $range
 */
class Statusgruppen extends SimpleORMap implements PrivacyObject
{
    protected static function configure($config = [])
    {
        $config['db_table'] = 'statusgruppen';
        $config['has_many']['members'] = [
            'class_name'        => StatusgruppeUser::class,
            'assoc_foreign_key' => 'statusgruppe_id',
            'on_delete'         => 'delete',
            'order_by'          => 'ORDER BY position ASC',
        ];
        $config['has_and_belongs_to_many']['dates'] = [
            'class_name' => CourseDate::class,
            'thru_table' => 'termin_related_groups',
            'order_by'   => 'ORDER BY date',
            'on_delete'  => 'delete', // TODO: This might cause trouble
            'on_store'   => 'store'
        ];
        $config['belongs_to']['parent'] = [
            'class_name'  => Statusgruppen::class,
            'foreign_key' => 'range_id',
        ];
        $config['belongs_to']['course'] = [
            'class_name'  => Course::class,
            'foreign_key' => 'range_id',
        ];
        $config['belongs_to']['institute'] = [
            'class_name'  => Institute::class,
            'foreign_key' => 'range_id',
        ];
        $config['belongs_to']['user'] = [
            'class_name'  => User::class,
            'foreign_key' => 'range_id',
        ];
        $config['has_one']['blubberthread'] = [
            'class_name' => BlubberStatusgruppeThread::class,
            'assoc_func' => 'findByStatusgruppe_id',
            'on_store' => 'store',
            'on_delete' => 'delete'
        ];
        $config['has_many']['consultation_blocks'] = [
            'class_name'        => ConsultationBlock::class,
            'assoc_foreign_key' => 'range_id',
            'on_delete'         => 'delete',
        ];
        $config['has_many']['consultation_responsibilities'] = [
            'class_name'        => ConsultationResponsibility::class,
            'assoc_func'        => 'findByStatusgroupId',
            'on_delete'         => 'delete',
        ];

        $config['additional_fields']['children'] = true;

        $config['additional_fields']['range'] = [
            'get' => function (Statusgruppen $group): ?Range {
                return RangeFactory::find($group->range_id);
            },
        ];

        $config['default_values']['position'] = null;

        $config['registered_callbacks']['before_store'][] = 'cbAddPosition';
        $config['registered_callbacks']['after_delete'][] = 'cbReorderPositions';

        $config['i18n_fields']['name'] = true;
        $config['i18n_fields']['name_w'] = true;
        $config['i18n_fields']['name_m'] = true;

        parent::configure($config);
    }

    public $keep_children = false;

    public static function findAllByRangeId($range_id, $as_collection = false)
    {
        $groups = self::findBySQL('range_id IN (?)', [$range_id]);
        if (count($groups) > 0) {
            $ids = array_map(function ($group) { return $group->id; }, $groups);
            $groups = array_merge($groups, self::findAllByRangeId($ids, false));
        }

        return $as_collection
             ? SimpleCollection::createFromArray($groups)
             : $groups;
    }

    /**
     * Creates or updates a statusgroup.
     *
     * @param string      $id                ID of an existing group or empty if new group
     * @param string      $name              group name
     * @param int         $position          position or null if automatic position after other groups
     * @param string      $range_id          ID of the object this group belongs to
     * @param int         $size              max number of members or 0 if unlimited
     * @param int         $selfassign        may users join this group by themselves?
     * @param int         $selfassign_start  group joining is possible starting at ...
     * @param int         $makefolder        create a document folder assigned to this group?
     * @param array|null  $dates             dates assigned to this group. Defaults to null which means already assigned
     *                                       dates are not changed.
     * @return Statusgruppen The saved statusgroup.
     * @throws Exception
     */
    public static function createOrUpdate(
        $id,
        $name,
        $position,
        $range_id,
        $size,
        $selfassign,
        $selfassign_start,
        $selfassign_end,
        $makefolder,
        $dates = null
    )
    {
        $group = new Statusgruppen($id);

        $group->name = $name;
        $group->position = $position;
        $group->range_id = $range_id;
        $group->size = $size;
        $group->selfassign = $selfassign;
        $group->selfassign_start = $selfassign ? $selfassign_start : 0;
        $group->selfassign_end = $selfassign ? $selfassign_end : 0;

        // Set assigned dates.
        if (isset($dates)) {
            $group->dates = CourseDate::findMany($dates);
        }

        $group->store();

        /*
         * Create document folder if requested (ID is needed here,
         * so we do that after store()).
         */
        $group->updateFolder($makefolder);

        return $group;
    }

    public function getChildren()
    {
        $result = Statusgruppen::findBySQL('range_id = ? ORDER BY position', [$this->id]);
        return $result ?: [];
    }

    public function getDatafields()
    {
        return DataFieldEntry::getDataFieldEntries([$this->range_id, $this->statusgruppe_id], 'roleinstdata');
    }

    public function setDatafields($data)
    {
        foreach ($this->getDatafields() as $field) {
            $field->setValueFromSubmit($data[$field->getId()]);
            $field->store();
        }
    }

    /**
     * Finds all statusgroups by a course id
     *
     * @param string The course id
     * @return array Statusgroups
     */
    public static function findBySeminar_id($course_id)
    {
        return self::findByRange_id($course_id, 'ORDER BY position asc, name asc');
    }

    public static function findByTermin_id($termin_id)
    {
        return self::findBySQL('INNER JOIN termin_related_groups USING (statusgruppe_id) WHERE termin_id = ?', [$termin_id]);
    }

    public static function findContactGroups($user_id = null)
    {
        return self::findByRange_id($user_id ?: $GLOBALS['user']->id);
    }

    /**
     * Find all groups belonging to the given range_id that may be joined
     * by the given user.
     *
     * @param String $range_id range_id the groups shall belong to
     * @param String $user_id user to check
     * @return array
     */
    public static function findJoinableGroups($range_id, $user_id)
    {
        $groups = self::findByRange_id($range_id);
        return array_filter($groups, function ($g) use ($user_id) { return $g->userMayJoin($user_id); });
    }

    /**
     * Reorders the positions in numeric order without gaps (e.g. after a delete).
     *
     * @param  string $range_id Id of range
     */
    public static function reorderPositionsForRange($range_id)
    {
        return self::findEachBySQL(
            function ($group, $index) {
                $group->position = $index;
                $group->store();
            },
            'range_id = ? ORDER BY position ASC, name ASC',
            [$range_id]
        );
    }

    /**
     * Produces an array of all statusgroups a user is in
     *
     * @param string $user_id The user_id
     * @param string $seperator The sign between the full paths
     * @param string $pre Preface of the outputted string (used for recursion)
     * @return array Stringarray of full gendered paths
     */
    public function getFullGenderedPaths($user_id, $seperator = " > ", $pre = "")
    {
        $result = [];
        $name = $pre
              ? $pre . $seperator . $this->getGenderedName($user_id)
              : $this->getGenderedName($user_id);
        if ($this->isMember($user_id)) {
            $result[] = $name;
        }
        if ($this->children) {
            foreach ($this->children as $child) {
                $result = array_merge($result, $child->getFullGenderedPaths($user_id, $seperator, $name));
            }
        }
        return $result;
    }

    /**
     * Produces string of all statusgroups a user is in (upwards from the
     * current group)
     *
     * @param string $user_id The user_id
     * @param string $seperator The sign between the full paths
     * @return array String of full gendered paths separated by given separator
     */
    public function getFullGenderedName($user_id, $seperator = ' > ')
    {
        $result = [$this->getGenderedName($user_id)];

        $item = $this;
        while ($item = $item->parent) {
            array_unshift($result, $item->getGenderedName($user_id));
        }

        return implode($seperator, $result);
    }

    /**
     * Returns the gendered name of a statusgroup
     *
     * @param string|User $user_id The user_id
     * @return string The gendered name
     */
    public function getGenderedName($user_or_id)
    {
        // We have to have at least 1 name gendered
        if ((string) $this->name_m || (string) $this->name_w) {
            $user = User::toObject($user_or_id);
            switch ($user->geschlecht) {
                case UserInfo::GENDER_FEMALE:
                    return (string) $this->name_w ?: $this->name;
                case UserInfo::GENDER_MALE:
                    return (string) $this->name_m ?: $this->name;
            }
        }
        return $this->name;
    }

    public function getName()
    {
        return $this->content['name'];
    }

    /**
     * Puts out an array of all gendered userroles for a user in a certain
     * context
     *
     * @param string $context The context
     * @param string $user The user id
     * @return array All roles
     */
    public static function getUserRoles($context, $user)
    {
        $roles = [];
        $groups = self::findByRange_id($context);
        foreach ($groups as $group) {
            $roles = array_merge($roles, $group->getFullGenderedPaths($user));
        }
        return $roles;
    }

    /**
     * Checks if a statusgroup has a folder.
     *
     * @return boolean <b>true</> if the statusgroup has a folder, else
     * <b>false</b>
     */
    public function hasFolder()
    {
        $query = "SELECT id FROM folders WHERE folder_type = 'CourseGroupFolder' AND range_id = ? AND data_content LIKE ? LIMIT 1";
        $statement = DBManager::get()->prepare($query);
        $statement->execute([$this->range_id, '%"group":"' . $this->id. '"%']);
        return $statement->fetchColumn();
    }

    /**
     * Gets the folder assigned to this statusgroup.
     *
     * @return CourseGroupFolder|null
     */
    public function getFolder()
    {
        $folder_id = $this->hasFolder();
        return $folder_id ? FileManager::getTypedFolder($folder_id) : null;
    }

    /**
     * Delete or create a folder
     * @param boolean $set <b>true</b> Create a folder
     * <b>false</b> Unlink the existing folder from the group
     */
    public function updateFolder($set)
    {
        // Keep existing folder, but disconnect it from group.
        if ($this->hasFolder() && !$set) {
            $folder = $this->getFolder();
            $folder->folder_type = 'StandardFolder';
            unset($folder->data_content['group']);
            return $folder->store();
        }

        // Update existing folder name
        if ($this->hasFolder() && $set) {
            $folder = $this->getFolder();
            $folder->name = _('Dateiordner der Gruppe:') . ' ' . $this->name;
            return $folder->store();
        }

        // Create new CourseGroupFolder under top folder.
        if (!$this->hasFolder() && $set) {
            $topFolder = Folder::findTopFolder($this->range_id);
            if ($topFolder) {
                $folderdata = [
                    'user_id' => $GLOBALS['user']->id,
                    'parent_id' => $topFolder->id,
                    'range_id' => $this->range_id,
                    'range_type' => 'course',
                    'folder_type' => 'CourseGroupFolder',
                    'name' => _('Dateiordner der Gruppe:') . ' ' . $this->name,
                    'data_content' => ['group' => $this->id],
                    'description' => _('Ablage für Ordner und Dokumente dieser Gruppe')
                ];
                $groupFolder = new CourseGroupFolder($folderdata);
                return $groupFolder->store();
            }
        }
    }

    /**
     * Returns whether the group has an associated blubber thread.
     */
    public function hasBlubber(): bool
    {
        return (bool) BlubberStatusgruppeThread::findByStatusgruppe_id($this->id);
    }

    /**
     * Finds CourseTopics assigned to this group via course dates.
     * @return array
     */
    public function findTopics()
    {
        $topics = [];
        foreach ($this->dates as $d) {
            foreach ($d->topics as $t) {
                // Assign topics with ID as key so we get unique entries.
                $topics[$t->id] = $t;
            }
        }
        return $topics;
    }

    /**
     * Finds Lecturers assigned to this group via course dates.
     * @return array
     */
    public function findLecturers()
    {
        $lecturers = [];
        foreach ($this->dates as $dates) {
            foreach ($dates->dozenten as $d) {
                // Assign topics with ID as key so we get unique entries.
                $lecturers[$d->id] = $d;
            }
        }
        return $lecturers;
    }

    /**
     * Checks if a user is a member of this group
     *
     * @param string $user_id The user id
     * @return boolean <b>true</b> if user is a member of this group
     */
    public function isMember($user_id = null)
    {
        if ($user_id == null) {
            $user_id = $GLOBALS['user']->id;
        }
        foreach ($this->members as $member) {
            if ($member->user_id == $user_id) {
                return true;
            }
        }
        return false;
    }

    /**
     * Displayfunction to show the places left in this group
     *
     * @return string displaystring
     */
    public function getPlaces()
    {
        return $this->size ? "( " . min(count($this->members), $this->size) . " / {$this->size} )" : "";
    }

    /**
     * Remove all users of this group
     */
    public function removeAllUsers()
    {
        StatusgruppeUser::deleteBySQL('statusgruppe_id = ?', [$this->id]);
    }

    /**
     * Remove one user from this group
     *
     * @param string $user_id The user id
     * @param bool   $deep    Remove user from children as well?
     * @return bool
     */
    public function removeUser($user_id, $deep = false)
    {
        // Delete user from statusgruppe
        $member = StatusgruppeUser::find([$this->id, $user_id]);
        $result = $member !== null && $member->delete();

        if ($deep) {
            foreach ($this->children as $child) {
                $child->removeUser($user_id, true);
            }
        }

        return $result;
    }

    /**
     * Adds a user to a group
     *
     * @param string $user_id The user id
     * @param boolean $check if <b>true</b> checks if there is space left in
     * this group
     * @return boolean <b>true</b> if user was added
     */
    public function addUser($user_id, $check = false)
    {
        if ($check && !$this->userMayJoin($user_id)) {
            return false;
        }
        $user = new StatusgruppeUser([$this->id, $user_id]);

        // set up default datafield values for institute groups
        if ($user->isNew() && !Course::find($this->range_id)) {
            $user->datafields->each(function ($datafield) {
                // note: $datafield->content does not work here
                $datafield['content'] = 'default_value';
                $datafield->store();
            });
        }
        return $user->store();
    }

    /**
     * Checks if a user could join this group
     *
     * @param string $user_id The user id
     * @return boolean <b>true</b> if user is allowed to join
     */
    public function userMayJoin($user_id)
    {
        return !$this->isMember($user_id)
            && $this->hasSpace()
            && ($this->selfassign != 2 || !$this->userHasExclusiveGroup($user_id));
    }

    /**
     * Checks if a user could leave this group
     *
     * @param string $user_id The user id
     * @return boolean <b>true</b> if user is allowed to leave
     */
    public function userMayLeave($user_id)
    {
        return $this->isMember($user_id)
            && ($this->selfassign && (!$this->selfassign_end || $this->selfassign_end > time()));
    }

    /**
     * Checks if the user is already in an exclusive group of this range
     *
     * @param string $user_id The user id
     * @return boolean <b>true</b> if user has already an exclusive group
     */
    public function userHasExclusiveGroup($user_id)
    {
        $sql = "SELECT 1 FROM statusgruppe_user JOIN statusgruppen USING (statusgruppe_id) WHERE selfassign = 2 AND range_id = ? AND user_id = ?";
        $stmt = DBManager::get()->prepare($sql);
        $stmt->execute([$this->range_id, $user_id]);
        return $stmt->fetchColumn();
    }

    /**
     * Sorts the member of a group alphabetic
     */
    public function sortMembersAlphabetic()
    {
        $i = 0;

        foreach ($this->members->orderBy('nachname, vorname') as $member) {
            $member->position = $i++;
            $member->store();
        }
    }

    /**
     * Sorts subgroups alphabetical
     */
    public function sortSubGroupsAlphabetic()
    {
        $groups = self::findBySQL('range_id = ? ORDER BY name', [$this->id]);

        foreach ($groups as $position => $group) {
            $group->position = $position;
            $group->store();
        }
    }

    /**
     * Checks if there is free space in this group
     *
     * @return <b>true</b> if there is free space
     */
    public function hasSpace()
    {
        return $this->selfassign &&
            ($this->selfassign_start <= time()) &&
            ($this->selfassign_end == 0 || $this->selfassign_end >= time()) &&
            ($this->size == 0 || count($this->members) < $this->size);
    }

    /**
     * Move a user to a position of a group
     *
     * @param string $user
     * @param type $pos
     */
    public function moveUser($user_id, $pos)
    {
        $statususer = new StatusgruppeUser([$this->id, $user_id]);
        if ($pos > $statususer->position) {
            $sql = "UPDATE statusgruppe_user SET position = position - 1 WHERE statusgruppe_id = ? AND position > ? AND position <= ?";
        } else {
            $sql = "UPDATE statusgruppe_user SET position = position + 1 WHERE statusgruppe_id = ? AND position < ? AND position >= ?";
        }
        $db = DBManager::get();
        $stmt = $db->prepare($sql);
        $stmt->execute([$this->id, $statususer->position, $pos]);

        $sql2 = "UPDATE statusgruppe_user SET position = ? WHERE statusgruppe_id = ? AND user_id = ?";
        $stmt2 = $db->prepare($sql2);
        $stmt2->execute([$pos, $this->id, $statususer->user_id]);
    }

    /**
     * Deletes a status group. Any associated child group will move upwards
     * in the tree.
     */
    public function remove()
    {
        // get all child-statusgroups and put them as a child of the father, so they don't hang around without a parent
        $children = $this->children->pluck('statusgruppe_id');
        if (!empty($children)) {
            $query = "UPDATE statusgruppen
                      SET range_id = ?
                      WHERE statusgruppe_id IN (?)";
            $statement = DBManager::get()->prepare($query);
            $statement->execute([$this->range_id, $children]);
        }

        $old = $this->keep_children;
        $this->keep_children = true;

        $result = $this->delete();

        $this->keep_children = $old;

        return $result;
    }

    /**
     * Deletes a status group and all it's child groups.
     *
     * @return int number of deleted groups
     */
    public function delete()
    {
        $result = 0;
        if (!$this->keep_children) {
            foreach($this->children as $child) {
                $result += $child->delete();
            }

        }

        // Remove datafields
        DatafieldEntryModel::deleteBySQL('range_id = ?', [$this->id]);
        $result += parent::delete();

        return $result;
    }

    /**
     * Adds the next free position if position is null.
     */
    public function cbAddPosition()
    {
        if ($this->position === null) {
            $sql = "SELECT MAX(`position`) FROM `statusgruppen` WHERE `range_id` = ?";
            $max_position = DBManager::get()->fetchColumn($sql, [$this->range_id]);
            $this->position = $max_position === null ? 0 : (int) $max_position + 1;
        }
    }

    /**
     * Reorders position after delete or for the assoicated range_id.
     */
    public function cbReorderPositions()
    {
        if (self::$performs_batch_operation) {
            return;
        }

        self::reorderPositionsForRange($this->range_id);
    }

    /**
     * This callback is called after deleting a User.
     * It removes courseware task entries that are associated with the group.
     */
    public function cbRemoveTask()
    {
        \Courseware\Task::deleteBySQL(
            '`solver_id` = ? AND `solver_type`= "group"',
            [$this->id]
        );
    }

    /**
     * Export available data of a given user into a storage object
     * (an instance of the StoredUserData class) for that user.
     *
     * @param StoredUserData $storage object to store data into
     */
    public static function exportUserData(StoredUserData $storage)
    {
        $sorm = self::findThru($storage->user_id, [
            'thru_table'        => 'statusgruppe_user',
            'thru_key'          => 'user_id',
            'thru_assoc_key'    => 'statusgruppe_id',
            'assoc_foreign_key' => 'statusgruppe_id',
        ]);
        if ($sorm) {
            $field_data = [];
            foreach ($sorm as $row) {
                $field_data[] = $row->toRawArray();
            }
            if ($field_data) {
                $storage->addTabularData(_('Statusgruppen'), 'statusgruppen', $field_data);
            }
        }
    }
}