3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
10 * Copyright © 2009-2023 The Galette Team
12 * This file is part of Galette (http://galette.tuxfamily.org).
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.
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.
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/>.
27 * @category Repository
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2009-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 - 2009-02-28
37 namespace Galette\Repository
;
39 use Galette\Core\Login
;
40 use Galette\Entity\Social
;
41 use Galette\Events\GaletteEvent
;
42 use Laminas\Db\ResultSet\ResultSet
;
43 use Laminas\Db\Sql\Predicate\IsNull
;
45 use Galette\DynamicFields\DynamicField
;
46 use Galette\Entity\DynamicFieldsHandle
;
48 use Laminas\Db\Sql\Expression
;
49 use Laminas\Db\Sql\Select
;
50 use Laminas\Db\Sql\Predicate\PredicateSet
;
51 use Laminas\Db\Sql\Predicate\Operator
;
52 use Galette\Entity\Adherent
;
53 use Galette\Entity\Contribution
;
54 use Galette\Entity\Transaction
;
55 use Galette\Entity\Reminder
;
56 use Galette\Filters\MembersList
;
57 use Galette\Filters\AdvancedMembersList
;
58 use Galette\Core\Picture
;
59 use Galette\Entity\Group
;
60 use Galette\Entity\Status
;
64 * Members class for galette
67 * @category Repository
70 * @author Johan Cwiklinski <johan@x-tnd.be>
71 * @copyright 2009-2023 The Galette Team
72 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
73 * @link http://galette.tuxfamily.org
77 public const TABLE
= Adherent
::TABLE
;
78 public const PK
= Adherent
::PK
;
80 public const ALL_ACCOUNTS
= 0;
81 public const ACTIVE_ACCOUNT
= 1;
82 public const INACTIVE_ACCOUNT
= 2;
84 public const SHOW_LIST
= 0;
85 public const SHOW_PUBLIC_LIST
= 1;
86 public const SHOW_ARRAY_LIST
= 2;
87 public const SHOW_STAFF
= 3;
88 public const SHOW_MANAGED
= 4;
89 public const SHOW_EXPORT
= 5;
91 public const FILTER_NAME
= 0;
92 public const FILTER_ADDRESS
= 1;
93 public const FILTER_MAIL
= 2;
94 public const FILTER_JOB
= 3;
95 public const FILTER_INFOS
= 4;
96 public const FILTER_DC_EMAIL
= 5;
97 public const FILTER_W_EMAIL
= 6;
98 public const FILTER_WO_EMAIL
= 7;
99 public const FILTER_COMPANY_NAME
= 8;
100 public const FILTER_DC_PUBINFOS
= 9;
101 public const FILTER_W_PUBINFOS
= 10;
102 public const FILTER_WO_PUBINFOS
= 11;
103 public const FILTER_ID
= 12;
104 public const FILTER_NUMBER
= 13;
106 public const MEMBERSHIP_ALL
= 0;
107 public const MEMBERSHIP_UP2DATE
= 3;
108 public const MEMBERSHIP_NEARLY
= 1;
109 public const MEMBERSHIP_LATE
= 2;
110 public const MEMBERSHIP_NEVER
= 4;
111 public const MEMBERSHIP_STAFF
= 5;
112 public const MEMBERSHIP_ADMIN
= 6;
113 public const MEMBERSHIP_NONE
= 7;
115 public const ORDERBY_NAME
= 'name';
116 public const ORDERBY_NICKNAME
= 'nickname';
117 public const ORDERBY_STATUS
= 'status';
118 public const ORDERBY_FEE_STATUS
= 'fee_status';
119 public const ORDERBY_MODIFDATE
= 'modif_date';
120 public const ORDERBY_ID
= 'id';
122 public const NON_STAFF_MEMBERS
= 30;
124 private $filters = false;
125 private $count = null;
126 private $errors = array();
129 * Default constructor
131 * @param MembersList $filters Filtering
133 public function __construct($filters = null)
135 if ($filters === null) {
136 $this->filters
= new MembersList();
138 $this->filters
= $filters;
143 * Get staff members list
145 * @param bool $as_members return the results as an array of
147 * @param array $fields field(s) name(s) to get. Should be a string or
148 * an array. If null, all fields will be
150 * @param boolean $count true if we want to count members
151 * @param boolean $limit true to LIMIT query
153 * @return Adherent[]|ResultSet
155 public function getStaffMembersList(
161 return $this->getMembersList(
172 * Get managed members list (for groups managers)
174 * @param bool $as_members return the results as an array of
176 * @param array $fields field(s) name(s) to get. Should be a string or
177 * an array. If null, all fields will be
179 * @param boolean $count true if we want to count members
180 * @param boolean $limit true to LIMIT query
182 * @return Adherent[]|ResultSet
184 public function getManagedMembersList(
190 return $this->getMembersList(
203 * @param bool $as_members return the results as an array of
205 * @param array $fields field(s) name(s) to get. Should be a string or
206 * an array. If null, all fields will be
208 * @param boolean $count true if we want to count members
209 * @param boolean $staff true if we want only staff members
210 * @param boolean $managed true if we want only managed groups
211 * @param boolean $limit true if we want records pagination
212 * @param boolean $export true if we are exporting
214 * @return Adherent[]|ResultSet
216 public function getMembersList(
227 if ($limit === true) {
228 //force count if limit is active
233 $_mode = self
::SHOW_LIST
;
234 if ($staff !== false) {
235 $_mode = self
::SHOW_STAFF
;
237 if ($managed !== false) {
238 $_mode = self
::SHOW_MANAGED
;
240 if ($export !== false) {
241 $_mode = self
::SHOW_EXPORT
;
244 $select = $this->buildSelect(
251 //add limits to retrieve only relavant rows
252 if ($limit === true) {
253 $this->filters
->setLimits($select);
256 $rows = $zdb->execute($select);
257 $this->filters
->query
= $zdb->query_string
;
265 foreach ($rows as $row) {
266 $members[] = new Adherent($zdb, $row, $deps);
272 } catch (Throwable
$e) {
274 'Cannot list members | ' . $e->getMessage(),
282 * Remove specified members
284 * @param integer|array $ids Members identifiers to delete
288 public function removeMembers($ids)
290 global $zdb, $hist, $emitter;
292 $processed = array();
294 if (is_array($ids)) {
296 } elseif (is_numeric($ids)) {
303 $zdb->connection
->beginTransaction();
305 //Retrieve some information
306 $select = $zdb->select(self
::TABLE
);
308 array(self
::PK
, 'nom_adh', 'prenom_adh', 'email_adh')
309 )->where
->in(self
::PK
, $list);
311 $results = $zdb->execute($select);
314 foreach ($results as $member) {
315 $str_adh = $member->id_adh
. ' (' . $member->nom_adh
. ' ' .
316 $member->prenom_adh
. ')';
317 $infos .= $str_adh . "\n";
319 $p = new Picture($member->id_adh
);
320 if ($p->hasPicture()) {
321 if (!$p->delete(false)) {
323 'Unable to delete picture for member ' . $str_adh,
326 throw new \
Exception(
327 'Unable to delete picture for member ' .
332 _T("Member Picture deleted"),
339 'id_adh' => $member->id_adh
,
340 'nom_adh' => $member->nom_adh
,
341 'prenom_adh' => $member->prenom_adh
,
342 'email_adh' => $member->email_adh
346 //delete contributions
347 $del_qry = $zdb->delete(Contribution
::TABLE
);
352 $zdb->execute($del_qry);
355 $select = $zdb->select(Transaction
::TABLE
);
356 $select->where
->in(self
::PK
, $list);
357 $results = $zdb->execute($select);
359 //if members has transactions;
360 //reset link with other contributions
362 if ($results->count() > 0) {
364 foreach ($results as $transaction) {
365 $transactions[] = $transaction[Transaction
::PK
];
368 $update = $zdb->update(Contribution
::TABLE
);
370 Transaction
::PK
=> new Expression('NULL')
375 $zdb->execute($update);
378 //delete transactions
379 $del_qry = $zdb->delete(Transaction
::TABLE
);
380 $del_qry->where
->in(self
::PK
, $list);
381 $zdb->execute($del_qry);
383 //delete groups membership/mamagmentship
384 Groups
::removeMembersFromGroups($list);
387 $del_qry = $zdb->delete(Reminder
::TABLE
);
392 $zdb->execute($del_qry);
394 //delete dynamic fields values
395 $del_qry = $zdb->delete(DynamicFieldsHandle
::TABLE
);
396 $del_qry->where(['field_form' => 'adh']);
397 $del_qry->where
->in('item_id', $list);
398 $zdb->execute($del_qry);
401 $del_qry = $zdb->delete(self
::TABLE
);
406 $zdb->execute($del_qry);
409 $zdb->connection
->commit();
411 foreach ($processed as $p) {
412 $emitter->dispatch(new GaletteEvent('member.remove', $p));
415 //add a history entry
417 _T("Delete members cards, transactions and dues"),
422 } catch (Throwable
$e) {
423 if ($zdb->connection
->inTransaction()) {
424 $zdb->connection
->rollBack();
426 if ($e->getCode() == 23000) {
428 'Member still have existing dependencies in the ' .
429 'database, maybe a mailing or some content from a ' .
430 'plugin. Please remove dependencies before trying ' .
434 $this->errors
[] = _T("Cannot remove a member who still have dependencies (mailings, ...)");
437 'Unable to delete selected member(s) |' .
450 * @param boolean $as_members return the results as an array of
452 * @param array $fields field(s) name(s) to get. Should be a string or
453 * an array. If null, all fields will be
456 * @return Adherent[]|ResultSet
458 public function getList($as_members = false, $fields = null)
460 return $this->getMembersList(
472 * Get members list with public information available
474 * @param boolean $with_photos get only members which have uploaded a
475 * photo (for trombinoscope)
479 public function getPublicList($with_photos)
484 $select = $this->buildSelect(
485 self
::SHOW_PUBLIC_LIST
,
491 $this->filters
->setLimits($select);
493 $results = $zdb->execute($select);
498 'picture' => $with_photos
500 foreach ($results as $row) {
501 $members[] = new Adherent($zdb, $row, $deps);
504 } catch (Throwable
$e) {
506 'Cannot list members with public information (photos: '
507 . $with_photos . ') | ' . $e->getMessage(),
515 * Get list of members that has been selected
517 * @param array $ids an array of members id that has been selected
518 * @param array $orderby SQL order clause (optional)
519 * @param boolean $with_photos Should photos be loaded?
520 * @param boolean $as_members Return Adherent[] or simple ResultSet
521 * @param array $fields Fields to use
522 * @param boolean $export True if we are exporting
523 * @param boolean $dues True if load dues as Adherent dependency
524 * @param boolean $parent True if load parent as Adherent dependency
526 * @return Adherent[]|false
528 public function getArrayList(
531 $with_photos = false,
540 if (!is_array($ids) ||
count($ids) < 1) {
541 Analog
::log('No member selected for labels.', Analog
::INFO
);
546 $damode = self
::SHOW_ARRAY_LIST
;
547 if ($export === true) {
548 $damode = self
::SHOW_EXPORT
;
550 $select = $this->buildSelect(
556 $select->where
->in('a.' . self
::PK
, $ids);
557 if (is_array($orderby) && count($orderby) > 0) {
558 foreach ($orderby as $o) {
563 $results = $zdb->execute($select);
567 'picture' => $with_photos,
572 foreach ($results as $o) {
573 if ($as_members === true) {
574 $members[] = new Adherent($zdb, $o, $deps);
580 } catch (Throwable
$e) {
582 'Cannot load members form ids array | ' . $e->getMessage(),
590 * Builds the SELECT statement
592 * @param int $mode the current mode (see self::SHOW_*)
593 * @param array $fields fields list to retrieve
594 * @param bool $photos true if we want to get only members with photos
595 * Default to false, only relevant for SHOW_PUBLIC_LIST
596 * @param bool $count true if we want to count members, defaults to false
598 * @return Select SELECT statement
600 private function buildSelect($mode, $fields, $photos, $count = false): Select
605 if ($fields != null && is_array($fields) && !in_array('id_adh', $fields)) {
606 $fields[] = 'id_adh';
608 $fieldsList = ($fields != null)
609 ?
((!is_array($fields) ||
count($fields) < 1) ?
(array)'*'
610 : $fields) : (array)'*';
612 $select = $zdb->select(self
::TABLE
, 'a');
614 $select->columns($fieldsList);
616 $select->quantifier('DISTINCT');
619 array('so' => PREFIX_DB
. Social
::TABLE
),
620 'a.' . Adherent
::PK
. '=so.' . Adherent
::PK
,
626 array('parent' => PREFIX_DB
. self
::TABLE
),
627 'a.parent_id=parent.' . self
::PK
,
633 case self
::SHOW_STAFF
:
634 case self
::SHOW_LIST
:
635 case self
::SHOW_ARRAY_LIST
:
636 case self
::SHOW_EXPORT
:
638 array('status' => PREFIX_DB
. Status
::TABLE
),
639 'a.' . Status
::PK
. '=status.' . Status
::PK
,
640 array('priorite_statut')
643 case self
::SHOW_MANAGED
:
645 array('status' => PREFIX_DB
. Status
::TABLE
),
646 'a.' . Status
::PK
. '=status.' . Status
::PK
648 array('gr' => PREFIX_DB
. Group
::GROUPSUSERS_TABLE
),
649 'a.' . Adherent
::PK
. '=gr.' . Adherent
::PK
,
652 array('m' => PREFIX_DB
. Group
::GROUPSMANAGERS_TABLE
),
653 'gr.' . Group
::PK
. '=m.' . Group
::PK
,
655 )->where(['m.' . Adherent
::PK
=> $login->id
]);
657 case self
::SHOW_PUBLIC_LIST
:
660 array('picture' => PREFIX_DB
. Picture
::TABLE
),
661 'a.' . self
::PK
. '= picture.' . self
::PK
,
668 //check for contributions filtering
670 $this->filters
instanceof AdvancedMembersList
671 && $this->filters
->withinContributions()
674 array('ct' => PREFIX_DB
. Contribution
::TABLE
),
675 'ct.' . self
::PK
. '=a.' . self
::PK
,
681 //check if there are dynamic fields in filter
684 if ($this->filters
instanceof AdvancedMembersList
) {
686 (bool)count($this->filters
->free_search
)
687 && !isset($this->filters
->free_search
['empty'])
689 $free_searches = $this->filters
->free_search
;
690 foreach ($free_searches as $fs) {
691 if (strpos($fs['field'], 'dyn_') === 0) {
692 // simple dynamic fields
694 $dfs[] = str_replace('dyn_', '', $fs['field']);
700 //check if there are dynamic fields for contributions in filter
706 $this->filters
instanceof AdvancedMembersList
707 && $this->filters
->withinContributions()
710 count($this->filters
->contrib_dynamic
) > 0
711 && !isset($this->filters
->contrib_dynamic
['empty'])
715 //check if there are dynamic fields in the filter
716 foreach ($this->filters
->contrib_dynamic
as $k => $cd) {
717 $dyn_field = DynamicField
::loadFieldType($zdb, (int)$k);
718 if ($dyn_field instanceof \Galette\DynamicFields\Choice
) {
726 if ($hasDfc === true ||
$hasCdfc === true) {
728 array('dfc' => PREFIX_DB
. DynamicFieldsHandle
::TABLE
),
729 'dfc.item_id=ct.' . Contribution
::PK
,
735 // simple dynamic fields
736 if ($hasDf === true) {
737 foreach ($dfs as $df) {
738 $subselect = $zdb->select(DynamicFieldsHandle
::TABLE
, 'df');
741 'item_id' => 'item_id',
745 $subselect->where(['df.field_form' => 'adh']);
746 $subselect->where(['df.field_id' => $df]);
748 array('df' . $df => $subselect),
749 'a.id_adh = df' . $df . '.item_id',
756 // choice dynamic fields
757 if ($hasCdfc === true) {
758 foreach ($cdfcs as $cdf) {
759 $rcdf_field = sprintf(
761 $zdb->platform
->quoteIdentifier('cdfc' . $cdf),
762 $zdb->platform
->quoteIdentifier('id')
764 if (TYPE_DB
=== 'pgsql') {
765 $rcdf_field = $rcdf_field . '::text';
769 array('cdfc' . $cdf => DynamicField
::getFixedValuesTableName($cdf, true)),
774 $zdb->platform
->quoteIdentifier('dfc'),
775 $zdb->platform
->quoteIdentifier('field_val')
784 if ($mode == self
::SHOW_LIST ||
$mode == self
::SHOW_MANAGED
) {
785 if ($this->filters
!== false) {
786 $this->buildWhereClause($select);
788 } elseif ($mode == self
::SHOW_PUBLIC_LIST
) {
801 new Expression('true')
809 'a.bool_display_info',
811 new Expression('true')
816 new Expression('true')
825 if ($mode === self
::SHOW_STAFF
) {
826 $select->where
->lessThan(
827 'status.priorite_statut',
828 self
::NON_STAFF_MEMBERS
833 $this->proceedCount($select);
836 $this->buildOrderClause($select, $fields);
839 } catch (Throwable
$e) {
841 'Cannot build SELECT clause for members | ' . $e->getMessage(),
849 * Count members from the query
851 * @param Select $select Original select
855 private function proceedCount(Select
$select)
860 $countSelect = clone $select;
861 $countSelect->reset($countSelect::COLUMNS
);
862 $countSelect->reset($countSelect::ORDER
);
863 $countSelect->reset($countSelect::HAVING
);
864 $joins = $countSelect->joins
;
865 $countSelect->reset($countSelect::JOINS
);
866 foreach ($joins as $join) {
873 unset($join['columns']);
875 $countSelect->columns(
877 'count' => new Expression('count(DISTINCT a.' . self
::PK
. ')')
881 $have = $select->having
;
882 if ($have->count() > 0) {
883 foreach ($have->getPredicates() as $h) {
884 $countSelect->where($h);
888 $results = $zdb->execute($countSelect);
890 $this->count
= (int)$results->current()->count
;
891 if (isset($this->filters
) && $this->count
> 0) {
892 $this->filters
->setCounter($this->count
);
894 } catch (Throwable
$e) {
896 'Cannot count members | ' . $e->getMessage(),
904 * Builds the order clause
906 * @param Select $select Original select
907 * @param array $fields Fields list to ensure ORDER clause
908 * references selected fields. Optional.
912 private function buildOrderClause(Select
$select, $fields = null): Select
916 switch ($this->filters
->orderby
) {
917 case self
::ORDERBY_NICKNAME
:
918 if ($this->canOrderBy('pseudo_adh', $fields)) {
919 $order[] = 'pseudo_adh ' . $this->filters
->getDirection();
922 case self
::ORDERBY_STATUS
:
923 if ($this->canOrderBy('priorite_statut', $fields)) {
924 $order[] = 'priorite_statut ' . $this->filters
->getDirection();
927 case self
::ORDERBY_MODIFDATE
:
928 if ($this->canOrderBy('date_modif_adh', $fields)) {
929 $order[] = 'date_modif_adh ' . $this->filters
->getDirection();
932 case 'list_adh_contribstatus':
933 case self
::ORDERBY_FEE_STATUS
:
934 if ($this->canOrderBy('bool_exempt_adh', $fields)) {
935 $order[] = 'bool_exempt_adh ' . $this->filters
->getDirection();
938 if ($this->canOrderBy('date_echeance', $fields)) {
939 $order[] = 'date_echeance ' . $this->filters
->getDirection();
942 case self
::ORDERBY_ID
:
943 if ($this->canOrderBy('id_adh', $fields)) {
944 $order[] = 'id_adh ' . $this->filters
->getDirection();
947 case 'list_adh_name':
950 case self
::ORDERBY_NAME
:
954 if ($this->canOrderBy($this->filters
->orderby
, $fields)) {
955 $order[] = 'a.' . $this->filters
->orderby
. ' ' . $this->filters
->getDirection();
960 //anyways, we want to order by firstname, lastname
961 if ($this->canOrderBy('nom_adh', $fields)) {
962 $order[] = 'nom_adh ' . $this->filters
->getDirection();
964 if ($this->canOrderBy('prenom_adh', $fields)) {
965 $order[] = 'prenom_adh ' . $this->filters
->getDirection();
968 $select->order($order);
973 * Is field allowed to order? it should be present in
974 * provided fields list (those that are SELECT'ed).
976 * @param string $field_name Field name to order by
977 * @param array $fields SELECTE'ed fields
981 private function canOrderBy($field_name, $fields)
983 if ($fields === null) {
985 } elseif (!is_array($fields)) {
987 } elseif (in_array($field_name, $fields)) {
991 'Trying to order by ' . $field_name . ' while it is not in ' .
1000 * Builds where clause, for filtering on simple list mode
1002 * @param Select $select Original select
1006 private function buildWhereClause(Select
$select)
1008 global $zdb, $login;
1011 if ($this->filters
->email_filter
== self
::FILTER_W_EMAIL
) {
1012 $select->where('(a.email_adh != \'\' OR a.parent_id IS NOT NULL AND parent.email_adh != \'\')');
1014 if ($this->filters
->email_filter
== self
::FILTER_WO_EMAIL
) {
1015 $select->where('(a.email_adh = \'\' OR a.email_adh IS NULL) AND (parent.email_adh = \'\' OR parent.email_adh IS NULL)');
1018 if ($this->filters
->filter_str
!= '') {
1019 $token = $zdb->platform
->quoteValue(
1020 '%' . strtolower($this->filters
->filter_str
) . '%'
1022 switch ($this->filters
->field_filter
) {
1023 case self
::FILTER_NAME
:
1024 if (TYPE_DB
=== 'pgsql') {
1025 $sep = " || ' ' || ";
1036 $pre . 'LOWER(a.nom_adh)' . $sep .
1037 'LOWER(a.prenom_adh)' . $sep .
1038 'LOWER(a.pseudo_adh)' . $post . ' LIKE ' .
1041 $pre . 'LOWER(a.prenom_adh)' . $sep .
1042 'LOWER(a.nom_adh)' . $sep .
1043 'LOWER(a.pseudo_adh)' . $post . ' LIKE ' .
1048 case self
::FILTER_COMPANY_NAME
:
1050 'LOWER(a.societe_adh) LIKE ' .
1054 case self
::FILTER_ADDRESS
:
1057 'LOWER(a.adresse_adh) LIKE ' . $token
1059 'a.cp_adh LIKE ' . $token
1061 'LOWER(a.ville_adh) LIKE ' . $token
1063 'LOWER(a.pays_adh) LIKE ' . $token
1067 case self
::FILTER_MAIL
:
1070 'LOWER(a.email_adh) LIKE ' . $token
1072 'LOWER(so.url) LIKE ' . $token
1076 case self
::FILTER_JOB
:
1078 'LOWER(a.prof_adh) LIKE ' . $token
1081 case self
::FILTER_INFOS
:
1083 if ($login->isAdmin() ||
$login->isStaff()) {
1084 $more = ' OR LOWER(a.info_adh) LIKE ' . $token;
1087 '(LOWER(a.info_public_adh) LIKE ' .
1088 $token . $more . ')'
1091 case self
::FILTER_NUMBER
:
1092 $select->where
->equalTo('a.num_adh', $this->filters
->filter_str
);
1094 case self
::FILTER_ID
:
1095 $select->where
->equalTo('a.id_adh', $this->filters
->filter_str
);
1100 if ($this->filters
->membership_filter
) {
1101 switch ($this->filters
->membership_filter
) {
1102 case self
::MEMBERSHIP_NEARLY
:
1103 $now = new \
DateTime();
1104 $due_date = clone $now;
1105 $due_date->modify('+30 days');
1107 ->greaterThanOrEqualTo(
1109 $now->format('Y-m-d')
1110 )->lessThanOrEqualTo(
1112 $due_date->format('Y-m-d')
1113 )->equalTo('a.bool_exempt_adh', new Expression('false'));
1115 case self
::MEMBERSHIP_LATE
:
1119 date('Y-m-d', time())
1120 )->equalTo('a.bool_exempt_adh', new Expression('false'));
1122 case self
::MEMBERSHIP_UP2DATE
:
1124 '(' . 'a.date_echeance >= \'' . date('Y-m-d', time())
1125 . '\' OR a.bool_exempt_adh=true)'
1128 case self
::MEMBERSHIP_NEVER
:
1129 $select->where('a.date_echeance IS NULL')
1130 ->where('a.bool_exempt_adh = false');
1132 case self
::MEMBERSHIP_STAFF
:
1133 $select->where
->lessThan(
1134 'status.priorite_statut',
1135 self
::NON_STAFF_MEMBERS
1138 case self
::MEMBERSHIP_ADMIN
:
1139 $select->where
->equalTo('a.bool_admin_adh', true);
1141 case self
::MEMBERSHIP_NONE
:
1142 $select->where
->equalTo('a.id_statut', Status
::DEFAULT_STATUS
);
1147 if ($this->filters
->filter_account
) {
1148 switch ($this->filters
->filter_account
) {
1149 case self
::ACTIVE_ACCOUNT
:
1150 $select->where('a.activite_adh=true');
1152 case self
::INACTIVE_ACCOUNT
:
1153 $select->where('a.activite_adh=false');
1158 if ($this->filters
->group_filter
) {
1160 array('g' => PREFIX_DB
. Group
::GROUPSUSERS_TABLE
),
1161 'a.' . Adherent
::PK
. '=g.' . Adherent
::PK
,
1165 array('gs' => PREFIX_DB
. Group
::TABLE
),
1166 'gs.' . Group
::PK
. '=g.' . Group
::PK
,
1170 '(g.' . Group
::PK
. ' = ' . $zdb->platform
->quoteValue($this->filters
->group_filter
) .
1171 ' OR gs.parent_group = NULL OR gs.parent_group = ' .
1172 $this->filters
->group_filter
. ')'
1176 if ($this->filters
instanceof AdvancedMembersList
) {
1177 $this->buildAdvancedWhereClause($select);
1179 } catch (Throwable
$e) {
1181 __METHOD__
. ' | ' . $e->getMessage(),
1189 * Builds where clause, for advanced filtering on simple list mode
1191 * @param Select $select Original select
1195 private function buildAdvancedWhereClause(Select
$select)
1199 // Search members who belong to any (OR) or all (AND) listed groups.
1200 // Idea is to build an array of members ID that fits groups selection
1201 // we will use in the final query.
1202 // The OR case is quite simple, AND is a bit more complex; since we must
1203 // check each member do belongs to all listed groups.
1205 count($this->filters
->groups_search
) > 0
1206 && !isset($this->filters
->groups_search
['empty'])
1210 foreach ($this->filters
->groups_search
as $gs) { // then add a row for each group
1211 $wheregroups[] = $gs['group'];
1214 $gselect = $zdb->select(Group
::GROUPSUSERS_TABLE
, 'gu');
1218 array('g' => PREFIX_DB
. Group
::TABLE
),
1219 'gu.id_group=g.' . Group
::PK
,
1224 'g.id_group' => ':group',
1225 'g.parent_group' => ':pgroup'
1229 $gselect->group(['gu.id_adh']);
1231 $stmt = $zdb->sql
->prepareStatementForSqlObject($gselect);
1235 foreach ($this->filters
->groups_search
as $gs) { // then add a row for each ig/searched group pair
1236 $gresults = $stmt->execute(
1238 'group' => $gs['group'],
1239 'pgroup' => $gs['group']
1243 switch ($this->filters
->groups_search_log_op
) {
1244 case AdvancedMembersList
::OP_AND
:
1245 foreach ($gresults as $gresult) {
1246 if (!isset($ids[$gresult['id_adh']])) {
1247 $ids[$gresult['id_adh']] = 0;
1249 $ids[$gresult['id_adh']] +
= 1;
1252 case AdvancedMembersList
::OP_OR
:
1253 foreach ($gresults as $gresult) {
1254 $mids[$gresult['id_adh']] = $gresult['id_adh'];
1261 foreach ($ids as $id_adh => $count) {
1262 if ($count == count($wheregroups)) {
1263 $mids[$id_adh] = $id_adh;
1269 //limit on found members
1270 $select->where
->in('a.id_adh', $mids);
1272 //no match in groups, end of game.
1273 $select->where('false = true');
1277 //FIXME: should be retrieved from members_fields
1279 'a.ddn_adh' => 'birth_date',
1280 'a.date_crea_adh' => 'creation_date',
1281 'a.date_modif_adh' => 'modif_date',
1282 'a.date_echeance' => 'due_date',
1283 'ct.date_enreg' => 'contrib_creation_date',
1284 'ct.date_debut_cotis' => 'contrib_begin_date',
1285 'ct.date_fin_cotis' => 'contrib_end_date'
1288 foreach ($dates as $field => $property) {
1289 $bprop = "r{$property}_begin";
1290 if ($this->filters
->$bprop) {
1291 $d = new \
DateTime($this->filters
->$bprop);
1292 $select->where
->greaterThanOrEqualTo(
1297 $eprop = "r{$property}_end";
1298 if ($this->filters
->$eprop) {
1299 $d = new \
DateTime($this->filters
->$eprop);
1300 $select->where
->lessThanOrEqualTo(
1307 if ($this->filters
->show_public_infos
) {
1308 switch ($this->filters
->show_public_infos
) {
1309 case self
::FILTER_W_PUBINFOS
:
1310 $select->where('a.bool_display_info = true');
1312 case self
::FILTER_WO_PUBINFOS
:
1313 $select->where('a.bool_display_info = false');
1315 case self
::FILTER_DC_PUBINFOS
:
1316 //nothing to do here.
1321 if ($this->filters
->status
) {
1324 $this->filters
->status
1329 $this->filters
->contrib_min_amount
1330 ||
$this->filters
->contrib_max_amount
1332 if ($this->filters
->contrib_min_amount
) {
1333 $select->where
->greaterThanOrEqualTo(
1335 $this->filters
->contrib_min_amount
1338 if ($this->filters
->contrib_max_amount
) {
1339 $select->where
->lessThanOrEqualTo(
1341 $this->filters
->contrib_max_amount
1346 if ($this->filters
->contributions_types
) {
1349 $this->filters
->contributions_types
1353 if ($this->filters
->payments_types
) {
1355 'ct.type_paiement_cotis',
1356 $this->filters
->payments_types
1361 count($this->filters
->contrib_dynamic
) > 0
1362 && !isset($this->filters
->contrib_dynamic
['empty'])
1364 foreach ($this->filters
->contrib_dynamic
as $k => $cd) {
1367 if (is_array($cd)) {
1368 //dynamic choice spotted!
1369 $prefix = 'cdfc' . $k . '.';
1370 $qry = 'dfc.field_form = \'contrib\' AND ' .
1371 'dfc.field_id = ' . $k;
1373 $select->where($qry);
1374 $select->where
->in($prefix . $field, $cd);
1376 //dynamic field spotted!
1378 $qry = 'dfc.field_form = \'contrib\' AND ' .
1379 'dfc.field_id = ' . $k . ' AND ';
1380 $field = 'field_val';
1382 $dyn_field = DynamicField
::loadFieldType($zdb, (int)$k);
1384 if ($dyn_field instanceof \Galette\DynamicFields\Boolean
) {
1386 $qry .= $field . ' = ' . (int)$cd;
1388 $select->where($qry);
1389 } elseif ($dyn_field instanceof \Galette\DynamicFields\Date
) {
1390 //dynamic dates are stored in their localized format :/
1391 //use current lang format to query for now
1392 //FIXME works with french formatted date only -_-
1393 if ($zdb->isPostgres()) {
1395 $store_fmt = __("Y-m-d") === 'Y-m-d' ?
'YYYY-MM-DD' : 'DD/MM/YYYY';
1396 $cd = "to_date('" . $cd . "', '" . $store_fmt . "')";
1397 $qry .= "to_date(" . $prefix . $field . ", '$store_fmt')";
1399 $store_fmt = __("Y-m-d") === 'Y-m-d' ?
'%Y-%m-%d' : '%d/%m/%Y';
1400 $cd = "STR_TO_DATE('" . $cd . "', '" . $store_fmt . "')";
1401 $qry .= 'STR_TO_DATE(' . $prefix . $field . ', \'' . $store_fmt . '\') ';
1403 $qry .= $qop . ' ' . $cd;
1404 $select->where($qry);
1406 $qry .= 'LOWER(' . $prefix . $field . ') ' . $qop . ' ';
1407 $select->where($qry . $zdb->platform
->quoteValue('%' . strtolower($cd) . '%'));
1414 count($this->filters
->free_search
) > 0
1415 && !isset($this->filters
->free_search
['empty'])
1417 foreach ($this->filters
->free_search
as $fs) {
1418 $fs['search'] = mb_strtolower($fs['search']);
1420 switch ($fs['qry_op']) {
1421 case AdvancedMembersList
::OP_EQUALS
:
1424 case AdvancedMembersList
::OP_CONTAINS
:
1426 $fs['search'] = '%' . $fs['search'] . '%';
1428 case AdvancedMembersList
::OP_NOT_EQUALS
:
1431 case AdvancedMembersList
::OP_NOT_CONTAINS
:
1433 $fs['search'] = '%' . $fs['search'] . '%';
1435 case AdvancedMembersList
::OP_STARTS_WITH
:
1437 $fs['search'] = $fs['search'] . '%';
1439 case AdvancedMembersList
::OP_ENDS_WITH
:
1441 $fs['search'] = '%' . $fs['search'];
1443 case AdvancedMembersList
::OP_BEFORE
:
1446 case AdvancedMembersList
::OP_AFTER
:
1451 'Unknown query operator: ' . $fs['qry_op'] .
1452 ' (will fallback to equals)',
1462 if (strpos($fs['field'], 'dyn_') === 0) {
1463 // simple dynamic field spotted!
1464 $index = str_replace('dyn_', '', $fs['field']);
1465 $dyn_field = DynamicField
::loadFieldType($zdb, (int)$index);
1466 $prefix = 'df' . $index . '.';
1467 $fs['field'] = 'val';
1470 //handle socials networks
1471 if (strpos($fs['field'], 'socials_') === 0) {
1473 $type = str_replace('socials_', '', $fs['field']);
1475 $fs['field'] = 'url';
1476 $select->where(['so.type' => $type]);
1479 if ($dyn_field && $dyn_field instanceof \Galette\DynamicFields\Boolean
) {
1480 if ($fs['search'] != 0) {
1481 $qry .= $prefix . $fs['field'] . $qop . ' ' .
1484 $qry .= $prefix . $fs['field'] . ' IS NULL';
1486 } elseif (!strncmp($fs['field'], 'bool_', strlen('bool_'))) {
1487 $qry .= $prefix . $fs['field'] . $qop . ' ' .
1490 $fs['qry_op'] === AdvancedMembersList
::OP_BEFORE
1491 ||
$fs['qry_op'] === AdvancedMembersList
::OP_AFTER
1493 if ($prefix === 'a.') {
1494 //dates are OK in the main fields. no cast, just query!
1495 $qry .= $prefix . $fs['field'] . $qop . ' ' .
1496 $zdb->platform
->quoteValue($fs['search']);
1498 //dynamic dates are stored in their localized format :/
1499 //use current lang format to query for now
1500 //FIXME works with french formatted date only -_-
1501 if ($zdb->isPostgres()) {
1502 $store_fmt = __("Y-m-d") === 'Y-m-d' ?
'YYYY-MM-DD' : 'DD/MM/YYYY';
1503 $fs['search'] = "to_date('" . $fs['search'] . "', '" . $store_fmt . "')";
1504 $qry .= "to_date('" . $prefix . $fs['field'] . "', '$store_fmt')";
1506 $store_fmt = __("Y-m-d") === 'Y-m-d' ?
'%Y-%m-%d' : '%d/%m/%Y';
1507 $fs['search'] = "STR_TO_DATE('" . $fs['search'] . "', '" . $store_fmt . "')";
1508 $qry .= 'STR_TO_DATE(' . $prefix . $fs['field'] . ', \'' . $store_fmt . '\') ';
1511 $qry .= $qop . ' ' . $fs['search'];
1513 } elseif ($fs['field'] == 'status_label') {
1514 $qry_pattern = '%p%field %op %value';
1515 $qry .= str_replace(
1526 $zdb->platform
->quoteValue($fs['search'])
1531 $field = $prefix . $fs['field'];
1532 if ($zdb->isPostgres()) {
1533 $field = 'CAST(' . $field . ' AS TEXT)';
1535 $qry .= 'LOWER(' . $field . ') ' .
1536 $qop . ' ' . $zdb->platform
->quoteValue($fs['search']);
1539 if ($fs['log_op'] === AdvancedMembersList
::OP_AND
) {
1540 $select->where($qry);
1541 } elseif ($fs['log_op'] === AdvancedMembersList
::OP_OR
) {
1542 $select->where($qry, PredicateSet
::OP_OR
);
1549 * Login and password field cannot be empty.
1551 * If those are not required, or if a file has been imported
1552 * (from a CSV file for example), we fill here random values.
1556 public function emptyLogins()
1561 $zdb->connection
->beginTransaction();
1562 $select = $zdb->select(Adherent
::TABLE
);
1564 array('id_adh', 'login_adh', 'mdp_adh')
1573 new IsNull('login_adh'),
1579 new IsNull('mdp_adh'),
1585 $results = $zdb->execute($select);
1588 if ($results->count() > 0) {
1589 $update = $zdb->update(Adherent
::TABLE
);
1592 'login_adh' => ':login',
1593 'mdp_adh' => ':pass'
1595 )->where
->equalTo(Adherent
::PK
, ':id');
1597 $stmt = $zdb->sql
->prepareStatementForSqlObject($update);
1599 $p = new \Galette\Core\
Password($zdb);
1601 foreach ($results as $m) {
1605 ||
!isset($m->login_adh
)
1606 ||
$m->login_adh
== 'NULL'
1608 $m->login_adh
= $p->makeRandomPassword(15);
1614 ||
!isset($m->mdp_adh
)
1615 ||
$m->mdp_adh
== 'NULL'
1617 $randomp = $p->makeRandomPassword(15);
1618 $m->mdp_adh
= password_hash(
1625 if ($dirty === true) {
1628 'login' => $m->login_adh
,
1629 'pass' => $m->mdp_adh
,
1637 $zdb->connection
->commit();
1638 $this->count
= $processed;
1640 } catch (Throwable
$e) {
1641 $zdb->connection
->rollBack();
1643 'An error occurred trying to retrieve members with ' .
1644 'empty logins/passwords (' . $e->getMessage(),
1652 * Get reminders count based on members state of dues
1656 public function getRemindersCount()
1660 $reminders = array();
1662 // Count close to be expired reminders
1663 $select = $zdb->select(Adherent
::TABLE
, 'a');
1666 'cnt' => new Expression('count(a.' . Adherent
::PK
. ')')
1671 array('parent' => PREFIX_DB
. self
::TABLE
),
1672 'a.parent_id=parent.' . self
::PK
,
1678 ->where('a.activite_adh=true')
1679 ->where('a.bool_exempt_adh=false');
1681 $now = new \
DateTime();
1682 $due_date = clone $now;
1683 $due_date->modify('+30 days');
1686 ->greaterThanOrEqualTo('a.date_echeance', $now->format('Y-m-d'))
1687 ->lessThanOrEqualTo('a.date_echeance', $due_date->format('Y-m-d'));
1689 $select_wo_mail = clone $select;
1690 //per default, limit to members who have an email address
1692 '(a.email_adh != \'\' OR a.parent_id IS NOT NULL AND parent.email_adh != \'\')'
1694 $select_wo_mail->where(
1695 '(a.email_adh = \'\' OR a.email_adh IS NULL) AND (parent.email_adh = \'\' OR parent.email_adh IS NULL)'
1698 $results = $zdb->execute($select);
1699 $res = $results->current();
1700 $reminders['impending'] = $res->cnt
;
1702 $results_wo_mail = $zdb->execute($select_wo_mail);
1703 $res_wo_mail = $results_wo_mail->current();
1704 $reminders['nomail']['impending'] = $res_wo_mail->cnt
;
1706 // Count late reminders
1707 $select = $zdb->select(Adherent
::TABLE
, 'a');
1710 'cnt' => new Expression('count(a.' . Adherent
::PK
. ')')
1715 array('parent' => PREFIX_DB
. self
::TABLE
),
1716 'a.parent_id=parent.' . self
::PK
,
1722 ->where('a.activite_adh=true')
1723 ->where('a.bool_exempt_adh=false');
1726 ->lessThan('a.date_echeance', $now->format('Y-m-d'));
1728 $select_wo_mail = clone $select;
1729 //per default, limit to members who have an email address
1731 '(a.email_adh != \'\' OR a.parent_id IS NOT NULL AND parent.email_adh != \'\')'
1733 $select_wo_mail->where(
1734 '(a.email_adh = \'\' OR a.email_adh IS NULL) AND (parent.email_adh = \'\' OR parent.email_adh IS NULL)'
1737 $results = $zdb->execute($select);
1738 $res = $results->current();
1739 $reminders['late'] = $res->cnt
;
1741 $results_wo_mail = $zdb->execute($select_wo_mail);
1742 $res_wo_mail = $results_wo_mail->current();
1743 $reminders['nomail']['late'] = $res_wo_mail->cnt
;
1749 * Get count for current query
1753 public function getCount()
1755 return $this->count
;
1759 * Get registered errors
1763 public function getErrors()
1765 return $this->errors
;
1769 * Get all existing emails
1771 * @param Db $zdb Database instance
1773 * @return array ['email' => 'id_adh']
1775 public static function getEmails(Db
$zdb)
1778 $select = $zdb->select(self
::TABLE
);
1783 $select->where('email_adh != \'\' AND email_adh IS NOT NULL');
1784 $rows = $zdb->execute($select);
1785 foreach ($rows as $row) {
1786 $emails[$row->email_adh
] = $row->{self
::PK
};
1792 * Get current filters
1794 * @return MembersList
1796 public function getFilters()
1798 return $this->filters
;
1802 * Get members list to instanciate dropdowns
1804 * @param Db $zdb Database instance
1805 * @param Login $login Login instance
1806 * @param integer $current Current member
1810 public function getDropdownMembers(Db
$zdb, Login
$login, $current = null)
1813 $required_fields = array(
1821 if ($login->isAdmin() ||
$login->isStaff()) {
1822 $list_members = $this->getList(false, $required_fields);
1823 } elseif ($login->isGroupManager()) {
1824 $list_members = $this->getManagedMembersList(false, $required_fields);
1827 if (count($list_members) > 0) {
1828 foreach ($list_members as $member) {
1831 $members[$member->$pk] = Adherent
::getNameWithCase(
1833 $member->prenom_adh
,
1841 //check if current attached member is part of the list
1842 if ($current !== null && !isset($members[$current])) {
1844 [$current => Adherent
::getSName($zdb, $current, true, true)] +