]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Entity/Group.php
a85086f781b3493b1ca887a3009f13fae193f20f
[galette.git] / galette / lib / Galette / Entity / Group.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * Group entity
7 *
8 * PHP version 5
9 *
10 * Copyright © 2012-2023 The Galette Team
11 *
12 * This file is part of Galette (http://galette.tuxfamily.org).
13 *
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
18 *
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
26 *
27 * @category Entity
28 * @package Galette
29 *
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2012-2023 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.tuxfamily.org
34 * @since Available since 0.7dev - 2012-01-17
35 */
36
37 namespace Galette\Entity;
38
39 use ArrayObject;
40 use Galette\Repository\Groups;
41 use Throwable;
42 use Galette\Core\Login;
43 use Analog\Analog;
44 use Laminas\Db\Sql\Expression;
45
46 /**
47 * Group entity
48 *
49 * @category Entity
50 * @name Group
51 * @package Galette
52 * @author Johan Cwiklinski <johan@x-tnd.be>
53 * @copyright 2012-2023 The Galette Team
54 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
55 * @link http://galette.tuxfamily.org
56 * @since Available since 0.7dev - 2012-01-17
57 */
58 class Group
59 {
60 public const TABLE = 'groups';
61 public const PK = 'id_group';
62 //relations tables
63 public const GROUPSUSERS_TABLE = 'groups_members';
64 public const GROUPSMANAGERS_TABLE = 'groups_managers';
65
66 public const MEMBER_TYPE = 0;
67 public const MANAGER_TYPE = 1;
68
69 private $id;
70 private $group_name;
71 private $parent_group;
72 private $managers;
73 private $members;
74 private $groups;
75 private $creation_date;
76 private $count_members;
77 private $isempty;
78 private $login;
79
80 /**
81 * Default constructor
82 *
83 * @param null|int|ArrayObject $args Either a ResultSet row or its id for to load
84 * a specific group, or null to just
85 * instanciate object
86 */
87 public function __construct($args = null)
88 {
89 if ($args === null || is_int($args)) {
90 if (is_int($args) && $args > 0) {
91 $this->load($args);
92 }
93 } elseif (is_object($args)) {
94 $this->loadFromRS($args);
95 }
96 }
97
98 /**
99 * Loads a group from its id
100 *
101 * @param int $id the identifiant for the group to load
102 *
103 * @return bool true if query succeed, false otherwise
104 */
105 public function load($id)
106 {
107 global $zdb;
108
109 try {
110 $select = $zdb->select(self::TABLE);
111 $select->where(array(self::PK => $id));
112
113 $results = $zdb->execute($select);
114
115 if ($results->count() > 0) {
116 $this->loadFromRS($results->current());
117 return true;
118 } else {
119 return false;
120 }
121 } catch (Throwable $e) {
122 Analog::log(
123 'Cannot load group form id `' . $id . '` | ' . $e->getMessage(),
124 Analog::WARNING
125 );
126 throw $e;
127 }
128 }
129
130 /**
131 * Populate object from a resultset row
132 *
133 * @param ArrayObject $r the resultset row
134 *
135 * @return void
136 */
137 private function loadFromRS(ArrayObject $r)
138 {
139 $this->id = (int)$r->id_group;
140 $this->group_name = $r->group_name;
141 $this->creation_date = $r->creation_date;
142 if ($r->parent_group) {
143 $this->parent_group = new Group((int)$r->parent_group);
144 }
145 $adhpk = Adherent::PK;
146 if (isset($r->members)) {
147 //we're from a list, we just want members count
148 $this->count_members = $r->members;
149 } else {
150 //we're probably from a single group, let's load sub entities
151 //$this->loadPersons(self::MEMBER_TYPE);
152 //$this->loadPersons(self::MANAGER_TYPE);
153 //$this->loadSubGroups();
154 }
155 }
156
157 /**
158 * Loads members for the current group
159 *
160 * @param int $type Either self::MEMBER_TYPE or self::MANAGER_TYPE
161 *
162 * @return void
163 */
164 private function loadPersons($type)
165 {
166 global $zdb;
167
168 if ($this->id) {
169 try {
170 $join = null;
171 switch ($type) {
172 case self::MEMBER_TYPE:
173 $join = PREFIX_DB . self::GROUPSUSERS_TABLE;
174 break;
175 case self::MANAGER_TYPE:
176 $join = PREFIX_DB . self::GROUPSMANAGERS_TABLE;
177 break;
178 }
179
180 $select = $zdb->select(Adherent::TABLE, 'a');
181 $select->join(
182 array('g' => $join),
183 'g.' . Adherent::PK . '=a.' . Adherent::PK,
184 array()
185 )->where([
186 'g.' . self::PK => $this->id
187 ])->order(
188 'nom_adh ASC',
189 'prenom_adh ASC'
190 );
191
192 $results = $zdb->execute($select);
193 $members = array();
194
195 $deps = array(
196 'picture' => false,
197 'groups' => false,
198 'dues' => false
199 );
200
201 foreach ($results as $m) {
202 $members[] = new Adherent($zdb, $m, $deps);
203 }
204
205 if ($type === self::MEMBER_TYPE) {
206 $this->members = $members;
207 } else {
208 $this->managers = $members;
209 }
210 } catch (Throwable $e) {
211 Analog::log(
212 'Cannot get group persons | ' . $e->getMessage(),
213 Analog::WARNING
214 );
215 throw $e;
216 }
217 }
218 }
219
220 /**
221 * Load sub-groups
222 *
223 * @return void
224 */
225 private function loadSubGroups()
226 {
227 global $zdb;
228
229 if (!isset($this->login) || !$this->login->isLogged()) {
230 $this->groups = [];
231 return;
232 }
233
234 try {
235 $select = $zdb->select(self::TABLE, 'a');
236
237 if (!$this->login->isAdmin() && !$this->login->isStaff()) {
238 $select->join(
239 array('b' => PREFIX_DB . self::GROUPSMANAGERS_TABLE),
240 'a.' . self::PK . '=b.' . self::PK,
241 array()
242 )->where(['b.' . Adherent::PK => $this->login->id]);
243 }
244
245 $select->where(['parent_group' => $this->id])
246 ->order('group_name ASC');
247
248 $results = $zdb->execute($select);
249 $groups = array();
250 $grppk = self::PK;
251 foreach ($results as $m) {
252 $group = new Group((int)$m->$grppk);
253 $group->setLogin($this->login);
254 $groups[] = $group;
255 }
256 $this->groups = $groups;
257 } catch (Throwable $e) {
258 Analog::log(
259 'Cannot get subgroup for group ' . $this->group_name .
260 ' (' . $this->id . ')| ' . $e->getMessage(),
261 Analog::WARNING
262 );
263 throw $e;
264 }
265 }
266
267 /**
268 * Remove specified group
269 *
270 * @param boolean $cascade Also remove members and managers
271 *
272 * @return boolean
273 */
274 public function remove($cascade = false)
275 {
276 global $zdb;
277 $transaction = false;
278
279 try {
280 if (!$zdb->connection->inTransaction()) {
281 $zdb->connection->beginTransaction();
282 $transaction = true;
283 }
284
285 if ($cascade === true) {
286 $subgroups = $this->getGroups();
287 if (count($subgroups) > 0) {
288 Analog::log(
289 'Cascading remove ' . $this->group_name .
290 '. Subgroups, their members and managers will be detached.',
291 Analog::INFO
292 );
293 foreach ($subgroups as $subgroup) {
294 $subgroup->remove(true);
295 }
296 }
297
298 Analog::log(
299 'Cascading remove ' . $this->group_name .
300 '. Members and managers will be detached.',
301 Analog::INFO
302 );
303
304 //delete members
305 $delete = $zdb->delete(self::GROUPSUSERS_TABLE);
306 $delete->where([self::PK => $this->id]);
307 $zdb->execute($delete);
308
309 //delete managers
310 $delete = $zdb->delete(self::GROUPSMANAGERS_TABLE);
311 $delete->where([self::PK => $this->id]);
312 $zdb->execute($delete);
313 }
314
315 //delete group itself
316 $delete = $zdb->delete(self::TABLE);
317 $delete->where([self::PK => $this->id]);
318 $zdb->execute($delete);
319
320 //commit all changes
321 if ($transaction) {
322 $zdb->connection->commit();
323 }
324
325 return true;
326 } catch (Throwable $e) {
327 if ($transaction) {
328 $zdb->connection->rollBack();
329 }
330 if (!$zdb->isPostgres() && $e->getCode() == 23000 || $zdb->isPostgres() && $e->getCode() == 23503) {
331 Analog::log(
332 str_replace(
333 '%group',
334 $this->group_name,
335 'Group "%group" still have members!'
336 ),
337 Analog::WARNING
338 );
339 $this->isempty = false;
340 } else {
341 Analog::log(
342 'Unable to delete group ' . $this->group_name .
343 ' (' . $this->id . ') |' . $e->getMessage(),
344 Analog::ERROR
345 );
346 throw $e;
347 }
348 return false;
349 }
350 }
351
352 /**
353 * Is group empty? (after first deletion try)
354 *
355 * @return boolean
356 */
357 public function isEmpty()
358 {
359 return $this->isempty;
360 }
361
362 /**
363 * Detach a group from its parent
364 *
365 * @return boolean
366 */
367 public function detach()
368 {
369 global $zdb, $hist;
370
371 try {
372 $update = $zdb->update(self::TABLE);
373 $update->set(
374 array('parent_group' => new Expression('NULL'))
375 )->where(
376 [self::PK => $this->id]
377 );
378
379 $edit = $zdb->execute($update);
380
381 //edit == 0 does not mean there were an error, but that there
382 //were nothing to change
383 if ($edit->count() > 0) {
384 $this->parent_group = null;
385 $hist->add(
386 _T("Group has been detached from its parent"),
387 $this->group_name
388 );
389 }
390
391 return true;
392 } catch (Throwable $e) {
393 Analog::log(
394 'Something went wrong detaching group `' . $this->group_name .
395 '` (' . $this->id . ') from its parent:\'( | ' .
396 $e->getMessage() . "\n" .
397 $e->getTraceAsString(),
398 Analog::ERROR
399 );
400 throw $e;
401 }
402 }
403
404 /**
405 * Store the group
406 *
407 * @return boolean
408 */
409 public function store()
410 {
411 global $zdb, $hist;
412
413 $parent_group = null;
414 if ($this->parent_group) {
415 $parent_group = $this->parent_group->getId();
416 }
417 if (!Groups::isUnique($zdb, $this->getName(), $parent_group, $this->getId())) {
418 Analog::log(
419 'Group name is not unique at requested level',
420 Analog::WARNING
421 );
422 throw new \RuntimeException(
423 _T("The group name you have requested already exists in the database.")
424 );
425 }
426
427 try {
428 $values = array(
429 self::PK => $this->id,
430 'group_name' => $this->group_name
431 );
432
433 if ($this->parent_group) {
434 $values['parent_group'] = $parent_group;
435 }
436
437 if (!isset($this->id) || $this->id == '') {
438 //we're inserting a new group
439 unset($values[self::PK]);
440 $this->creation_date = date("Y-m-d H:i:s");
441 $values['creation_date'] = $this->creation_date;
442
443 $insert = $zdb->insert(self::TABLE);
444 $insert->values($values);
445 $add = $zdb->execute($insert);
446 if ($add->count() > 0) {
447 $this->id = $zdb->getLastGeneratedValue($this);
448
449 // logging
450 $hist->add(
451 _T("Group added"),
452 $this->group_name
453 );
454 return true;
455 } else {
456 $hist->add(_T("Fail to add new group."));
457 throw new \Exception(
458 'An error occurred inserting new group!'
459 );
460 }
461 } else {
462 //we're editing an existing group
463 $update = $zdb->update(self::TABLE);
464 $update
465 ->set($values)
466 ->where([self::PK => $this->id]);
467
468 $edit = $zdb->execute($update);
469
470 //edit == 0 does not mean there were an error, but that there
471 //were nothing to change
472 if ($edit->count() > 0) {
473 $hist->add(
474 _T("Group updated"),
475 $this->group_name
476 );
477 }
478 return true;
479 }
480 /** FIXME: also store members and managers? */
481 } catch (Throwable $e) {
482 Analog::log(
483 'Something went wrong :\'( | ' . $e->getMessage() . "\n" .
484 $e->getTraceAsString(),
485 Analog::ERROR
486 );
487 throw $e;
488 }
489 }
490
491 /**
492 * Is current logged-in user manager of the group?
493 *
494 * @param Login $login Login instance
495 *
496 * @return boolean
497 */
498 public function isManager(Login $login)
499 {
500 if ($login->isAdmin() || $login->isStaff()) {
501 //admins as well as staff members are managers for all groups!
502 return true;
503 } else {
504 //let's check if current logged-in user is part of group managers
505 if (!is_array($this->managers)) {
506 $this->loadPersons(self::MANAGER_TYPE);
507 }
508
509 foreach ($this->managers as $manager) {
510 if ($login->login == $manager->login) {
511 return true;
512 }
513 }
514 return false;
515 }
516 }
517
518 /**
519 * Get group id
520 *
521 * @return integer
522 */
523 public function getId()
524 {
525 return $this->id;
526 }
527
528 /**
529 * Get Level of the group
530 *
531 * @return integer
532 */
533 public function getLevel()
534 {
535 if ($this->parent_group) {
536 return $this->parent_group->getLevel() + 1;
537 }
538 return 0;
539 }
540
541 /**
542 * Get the full name of the group "foo / bar"
543 *
544 * @return string
545 */
546 public function getFullName()
547 {
548 if ($this->parent_group) {
549 return $this->parent_group->getFullName() . ' / ' . $this->group_name;
550 }
551 return $this->group_name;
552 }
553
554 /**
555 * Get the indented short name of the group " >> bar"
556 *
557 * @return string
558 */
559 public function getIndentName()
560 {
561 if (($level = $this->getLevel())) {
562 return str_repeat("&nbsp;", 3 * $level) . '&raquo; ' . $this->group_name;
563 }
564 return $this->group_name;
565 }
566
567 /**
568 * Get group name
569 *
570 * @return string
571 */
572 public function getName()
573 {
574 return $this->group_name;
575 }
576
577 /**
578 * Get group members
579 *
580 * @return Adherent[]
581 */
582 public function getMembers()
583 {
584 if (!is_array($this->members)) {
585 $this->loadPersons(self::MEMBER_TYPE);
586 }
587 return $this->members;
588 }
589
590 /**
591 * Get groups managers
592 *
593 * @return Adherent[]
594 */
595 public function getManagers()
596 {
597 if (!is_array($this->managers)) {
598 $this->loadPersons(self::MANAGER_TYPE);
599 }
600 return $this->managers;
601 }
602
603 /**
604 * Get subgroups
605 *
606 * @return Group[]
607 */
608 public function getGroups()
609 {
610 if (!is_array($this->groups)) {
611 $this->loadSubGroups();
612 }
613 return $this->groups;
614 }
615
616 /**
617 * Get parent group
618 *
619 * @return Group
620 */
621 public function getParentGroup()
622 {
623 return $this->parent_group;
624 }
625
626 /**
627 * Get group creation date
628 *
629 * @param boolean $formatted Return date formatted, raw if false
630 *
631 * @return string
632 */
633 public function getCreationDate($formatted = true)
634 {
635 if ($formatted === true) {
636 $date = new \DateTime($this->creation_date);
637 return $date->format(__("Y-m-d"));
638 } else {
639 return $this->creation_date;
640 }
641 }
642
643 /**
644 * Get member count
645 *
646 * @param boolean $force Force members load, defaults to false
647 *
648 * @return int
649 */
650 public function getMemberCount($force = false)
651 {
652 if (isset($this->members) && is_array($this->members)) {
653 return count($this->members);
654 } elseif (isset($this->count_members)) {
655 return $this->count_members;
656 } else {
657 if ($force === true) {
658 return count($this->getMembers());
659 } else {
660 return 0;
661 }
662 }
663 }
664
665 /**
666 * Set name
667 *
668 * @param string $name Group name
669 *
670 * @return Group
671 */
672 public function setName($name)
673 {
674 $this->group_name = $name;
675 return $this;
676 }
677
678 /**
679 * check if can Set parent group
680 *
681 * @param Group $group Parent group
682 *
683 * @return boolean
684 */
685 public function canSetParentGroup(Group $group)
686 {
687 do {
688 if ($group->getId() == $this->getId()) {
689 return false;
690 }
691 } while ($group = $group->getParentGroup());
692
693 return true;
694 }
695
696 /**
697 * Set parent group
698 *
699 * @param int $id Parent group identifier
700 *
701 * @return Group
702 */
703 public function setParentGroup($id)
704 {
705 $group = new Group((int)$id);
706
707 if (!$this->canSetParentGroup($group)) {
708 //does not seem to work :/
709 throw new \Exception(
710 sprintf(
711 _T('Group `%1$s` cannot be set as parent!'),
712 $group->getName()
713 )
714 );
715 }
716
717 $this->parent_group = $group;
718 return $this;
719 }
720
721 /**
722 * Set members
723 *
724 * @param Adherent[] $members Members list
725 *
726 * @return bool
727 * @throws Throwable
728 */
729 public function setMembers(array $members = []): bool
730 {
731 global $zdb;
732
733 try {
734 $zdb->connection->beginTransaction();
735
736 //first, remove current groups members
737 $delete = $zdb->delete(self::GROUPSUSERS_TABLE);
738 $delete->where([self::PK => $this->id]);
739 $zdb->execute($delete);
740
741 Analog::log(
742 'Group members has been removed for `' . $this->group_name .
743 '`, we can now store new ones.',
744 Analog::INFO
745 );
746
747 $insert = $zdb->insert(self::GROUPSUSERS_TABLE);
748 $insert->values(
749 array(
750 self::PK => ':group',
751 Adherent::PK => ':adh'
752 )
753 );
754
755 $stmt = $zdb->sql->prepareStatementForSqlObject($insert);
756
757 foreach ($members as $m) {
758 $result = $stmt->execute(
759 array(
760 'group' => $this->id,
761 'adh' => $m->id
762 )
763 );
764
765 if ($result) {
766 Analog::log(
767 'Member `' . $m->sname . '` attached to group `' .
768 $this->group_name . '`.',
769 Analog::DEBUG
770 );
771 } else {
772 Analog::log(
773 'An error occurred trying to attach member `' .
774 $m->sname . '` to group `' . $this->group_name .
775 '` (' . $this->id . ').',
776 Analog::ERROR
777 );
778 throw new \Exception(
779 'Unable to attach `' . $m->sname . '` ' .
780 'to ' . $this->group_name . '(' . $this->id . ')'
781 );
782 }
783 }
784
785 //commit all changes
786 $zdb->connection->commit();
787
788 Analog::log(
789 'Group members updated successfully.',
790 Analog::INFO
791 );
792
793 return true;
794 } catch (Throwable $e) {
795 $te = clone $e;
796 $zdb->connection->rollBack();
797 $messages = array();
798 do {
799 $messages[] = $e->getMessage();
800 } while ($e = $e->getPrevious());
801 Analog::log(
802 'Unable to attach members to group `' . $this->group_name .
803 '` (' . $this->id . ')|' . implode("\n", $messages),
804 Analog::ERROR
805 );
806 throw $te;
807 }
808 }
809
810 /**
811 * Set managers
812 *
813 * @param Adherent[] $members Managers list
814 *
815 * @return bool
816 * @throws Throwable
817 */
818 public function setManagers(array $members = []): bool
819 {
820 global $zdb;
821
822 try {
823 $zdb->connection->beginTransaction();
824
825 //first, remove current groups managers
826 $delete = $zdb->delete(self::GROUPSMANAGERS_TABLE);
827 $delete->where([self::PK => $this->id]);
828 $zdb->execute($delete);
829
830 Analog::log(
831 'Group managers has been removed for `' . $this->group_name .
832 '`, we can now store new ones.',
833 Analog::INFO
834 );
835
836 $insert = $zdb->insert(self::GROUPSMANAGERS_TABLE);
837 $insert->values(
838 array(
839 self::PK => ':group',
840 Adherent::PK => ':adh'
841 )
842 );
843
844 $stmt = $zdb->sql->prepareStatementForSqlObject($insert);
845
846 foreach ($members as $m) {
847 $result = $stmt->execute(
848 array(
849 'group' => $this->id,
850 'adh' => $m->id
851 )
852 );
853
854 if ($result) {
855 Analog::log(
856 'Manager `' . $m->sname . '` attached to group `' .
857 $this->group_name . '`.',
858 Analog::DEBUG
859 );
860 } else {
861 Analog::log(
862 'An error occurred trying to attach manager `' .
863 $m->sname . '` to group `' . $this->group_name .
864 '` (' . $this->id . ').',
865 Analog::ERROR
866 );
867 throw new \Exception(
868 'Unable to attach `' . $m->sname . '` ' .
869 'to ' . $this->group_name . '(' . $this->id . ')'
870 );
871 }
872 }
873
874 //commit all changes
875 $zdb->connection->commit();
876
877 Analog::log(
878 'Groups managers updated successfully.',
879 Analog::INFO
880 );
881
882 return true;
883 } catch (Throwable $e) {
884 $te = clone $e;
885 $zdb->connection->rollBack();
886 $messages = array();
887 do {
888 $messages[] = $e->getMessage();
889 } while ($e = $e->getPrevious());
890 Analog::log(
891 'Unable to attach managers to group `' . $this->group_name .
892 '` (' . $this->id . ')|' . implode("\n", $messages),
893 Analog::ERROR
894 );
895 throw $te;
896 }
897 }
898
899 /**
900 * Set login instance
901 *
902 * @param Login $login Login instance
903 *
904 * @return Group
905 */
906 public function setLogin(Login $login)
907 {
908 $this->login = $login;
909 return $this;
910 }
911
912 /**
913 * Can current logged-in user edit group
914 *
915 * @param Login $login Login instance
916 *
917 * @return boolean
918 */
919 public function canEdit(Login $login): bool
920 {
921 global $preferences;
922
923 //admin and staff users can edit
924 if ($login->isAdmin() || $login->isStaff()) {
925 return true;
926 }
927
928 //group managers can edit groups they manage when pref is on
929 if ($preferences->pref_bool_groupsmanagers_edit_member && $this->isManager($login)) {
930 return true;
931 }
932
933 return false;
934 }
935 }