3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
6 * Galette members controller
10 * Copyright © 2019-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 Controllers
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2019-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.9.4dev - 2019-12-02
37 namespace Galette\Controllers\Crud
;
39 use Galette\Controllers\CrudController
;
40 use Galette\DynamicFields\Boolean
;
41 use Slim\Psr7\Request
;
42 use Slim\Psr7\Response
;
43 use Galette\Core\GaletteMail
;
44 use Galette\Core\Gaptcha
;
45 use Galette\Entity\Adherent
;
46 use Galette\Entity\Contribution
;
47 use Galette\Entity\ContributionsTypes
;
48 use Galette\Entity\DynamicFieldsHandle
;
49 use Galette\Entity\Group
;
50 use Galette\Entity\Status
;
51 use Galette\Entity\FieldsConfig
;
52 use Galette\Entity\Social
;
53 use Galette\Filters\AdvancedMembersList
;
54 use Galette\Filters\MembersList
;
56 use Galette\Repository\Groups
;
57 use Galette\Repository\Members
;
58 use Galette\Repository\PaymentTypes
;
59 use Galette\Repository\Titles
;
63 * Galette members controller
65 * @category Controllers
66 * @name GaletteController
68 * @author Johan Cwiklinski <johan@x-tnd.be>
69 * @copyright 2019-2023 The Galette Team
70 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
71 * @link http://galette.tuxfamily.org
72 * @since Available since 0.9.4dev - 2019-12-02
75 class MembersController
extends CrudController
78 private $is_self_membership = false;
85 * @param Request $request PSR Request
86 * @param Response $response PSR Response
90 public function add(Request
$request, Response
$response): Response
92 return $this->edit($request, $response, null, 'add');
98 * @param Request $request PSR Request
99 * @param Response $response PSR Response
103 public function addChild(Request
$request, Response
$response): Response
105 return $this->edit($request, $response, null, 'addchild');
109 * Self subscription page
111 * @param Request $request PSR Request
112 * @param Response $response PSR Response
116 public function selfSubscribe(Request
$request, Response
$response): Response
118 if (!$this->preferences
->pref_bool_selfsubscribe ||
$this->login
->isLogged()) {
121 ->withHeader('Location', $this->routeparser
->urlFor('slash'));
124 if ($this->session
->member
!== null) {
125 $member = $this->session
->member
;
126 $this->session
->member
= null;
128 $member = new Adherent($this->zdb
);
129 $member->enableDep('dynamics');
132 //mark as self membership
133 $member->setSelfMembership();
135 // flagging required fields
136 $fc = $this->fields_config
;
137 $form_elements = $fc->getFormElements($this->login
, true, true);
141 $members = $m->getDropdownMembers(
144 $member->hasParent() ?
$member->parent
->id
: null
149 'filters' => $m->getFilters(),
150 'count' => $m->getCount()
154 if (count($members)) {
155 $params['members']['list'] = $members;
158 $gaptcha = new Gaptcha($this->i18n
);
159 $this->session
->gaptcha
= $gaptcha;
163 'pages/member_form.html.twig',
165 'page_title' => _T("Subscription"),
166 'parent_tpl' => 'public_page.html.twig',
169 'autocomplete' => true,
170 'osocials' => new Social($this->zdb
),
173 'titles_list' => Titles
::getList($this->zdb
),
174 'fieldsets' => $form_elements['fieldsets'],
175 'hidden_elements' => $form_elements['hiddens'],
177 'gaptcha' => $gaptcha
186 * @param Request $request PSR Request
187 * @param Response $response PSR Response
191 public function doAdd(Request
$request, Response
$response): Response
193 return $this->store($request, $response);
197 * Self subscription add action
199 * @param Request $request PSR Request
200 * @param Response $response PSR Response
204 public function doSelfSubscribe(Request
$request, Response
$response): Response
206 $this->setSelfMembership();
207 return $this->doAdd($request, $response);
214 * @param Request $request PSR Request
215 * @param Response $response PSR Response
216 * @param integer $id_adh Member ID to duplicate
220 public function duplicate(Request
$request, Response
$response, int $id_adh): Response
222 $adh = new Adherent($this->zdb
, $id_adh, ['dynamics' => true, 'parent' => true]);
223 $adh->setDuplicate();
225 //store entity in session
226 $this->session
->member
= $adh;
230 ->withHeader('Location', $this->routeparser
->urlFor('addMember'));
237 * Display member card
239 * @param Request $request PSR Request
240 * @param Response $response PSR Response
241 * @param integer $id Member ID
245 public function show(Request
$request, Response
$response, int $id): Response
247 $member = new Adherent($this->zdb
);
252 if (!$member->canShow($this->login
)) {
253 $this->flash
->addMessage(
255 _T("You do not have permission for requested URL.")
262 $this->routeparser
->urlFor('me')
266 if ($member->id
== null) {
267 //member does not exists!
268 $this->flash
->addMessage(
270 str_replace('%id', $id, _T("No member #%id."))
276 $this->routeparser
->urlFor('slash')
280 // flagging fields visibility
281 $fc = $this->fields_config
;
282 $display_elements = $fc->getDisplayElements($this->login
);
287 'pages/member_show.html.twig',
289 'page_title' => _T("Member Profile"),
291 'pref_lang' => $this->i18n
->getNameFromId($member->language
),
292 'pref_card_self' => $this->preferences
->pref_card_self
,
293 'groups' => Groups
::getSimpleList(),
295 'display_elements' => $display_elements,
296 'osocials' => new Social($this->zdb
),
297 'navigate' => $this->handleNavigationLinks($member->id
)
306 * @param Request $request PSR Request
307 * @param Response $response PSR Response
311 public function showMe(Request
$request, Response
$response): Response
313 if ($this->login
->isSuperAdmin()) {
316 ->withHeader('Location', $this->routeparser
->urlFor('slash'));
318 return $this->show($request, $response, $this->login
->id
);
322 * Public pages (trombinoscope, public list)
324 * @param Request $request PSR Request
325 * @param Response $response PSR Response
326 * @param string $option One of 'page' or 'order'
327 * @param string|integer $value Value of the option
328 * @param string $type List type (either list or trombi)
332 public function publicList(
339 $varname = 'public_filter_' . $type;
340 if (isset($this->session
->$varname)) {
341 $filters = $this->session
->$varname;
343 $filters = new MembersList();
346 if ($option !== null) {
349 $filters->current_page
= (int)$value;
352 $filters->orderby
= $value;
357 $m = new Members($filters);
358 $members = $m->getPublicList($type === 'trombi');
360 $this->session
->$varname = $filters;
362 //assign pagination variables to the template and add pagination links
363 $filters->setViewPagination($this->routeparser
, $this->view
, false);
368 ($type === 'list' ?
'pages/members_public_list' : 'pages/members_public_gallery') . '.html.twig',
370 'page_title' => ($type === 'list' ?
_T("Members list") : _T('Trombinoscope')),
371 'additionnal_html_class' => ($type === 'list' ?
'' : 'trombinoscope'),
373 'members' => $members,
374 'nb_members' => $m->getCount(),
375 'filters' => $filters,
384 * Public pages (trombinoscope, public list)
386 * @param Request $request PSR Request
387 * @param Response $response PSR Response
388 * @param string $type Type
392 public function filterPublicList(Request
$request, Response
$response, string $type): Response
394 $post = $request->getParsedBody();
396 $varname = 'public_filter_' . $type;
397 if (isset($this->session
->$varname)) {
398 $filters = $this->session
->$varname;
400 $filters = new MembersList();
403 //reintialize filters
404 if (isset($post['clear_filter'])) {
407 //number of rows to show
408 if (isset($post['nbshow'])) {
409 $filters->show
= (int)$post['nbshow'];
413 $this->session
->$varname = $filters;
417 ->withHeader('Location', $this->routeparser
->urlFor('publicList', ['type' => $type]));
423 * @param Request $request PSR Request
424 * @param Response $response PSR Response
425 * @param integer $id Member ID
426 * @param integer $fid Dynamic fields ID
427 * @param integer $pos Dynamic field position
428 * @param string $name File name
432 public function getDynamicFile(
440 $member = new Adherent($this->zdb
);
443 ->enableDep('dynamics')
447 if (!$member->canShow($this->login
)) {
448 $fields = $member->getDynamicFields()->getFields();
449 if (!isset($fields[$fid])) {
450 //field does not exist or access is forbidden
457 if ($denied === true) {
458 $this->flash
->addMessage(
460 _T("You do not have permission for requested URL.")
466 $this->routeparser
->urlFor(
473 $filename = str_replace(
484 'member_%mid_field_%fid_value_%pos'
487 if (file_exists(GALETTE_FILES_PATH
. $filename)) {
488 $type = File
::getMimeType(GALETTE_FILES_PATH
. $filename);
490 $response = $response->withHeader('Content-Description', 'File Transfer')
491 ->withHeader('Content-Type', $type)
492 ->withHeader('Content-Disposition', 'attachment;filename="' . $name . '"')
493 ->withHeader('Pragma', 'no-cache')
494 ->withHeader('Content-Transfer-Encoding', 'binary')
495 ->withHeader('Expires', '0')
496 ->withHeader('Cache-Control', 'must-revalidate')
497 ->withHeader('Pragma', 'public');
499 $stream = fopen('php://memory', 'r+');
500 fwrite($stream, file_get_contents(GALETTE_FILES_PATH
. $filename));
503 return $response->withBody(new \Slim\Psr7\
Stream($stream));
506 'A request has been made to get a dynamic file named `' .
507 $filename . '` that does not exists.',
511 $this->flash
->addMessage(
513 _T("The file does not exists or cannot be read :(")
519 $this->routeparser
->urlFor('member', ['id' => $id])
527 * @param Request $request PSR Request
528 * @param Response $response PSR Response
529 * @param string $option One of 'page' or 'order'
530 * @param string|integer $value Value of the option
534 public function list(Request
$request, Response
$response, $option = null, $value = null): Response
536 if (isset($this->session
->filter_members
)) {
537 $filters = $this->session
->filter_members
;
539 $filters = new MembersList();
542 if ($option !== null) {
545 $filters->current_page
= (int)$value;
548 $filters->orderby
= $value;
553 $members = new Members($filters);
555 $members_list = array();
556 if ($this->login
->isAdmin() ||
$this->login
->isStaff()) {
557 $members_list = $members->getMembersList(true);
559 $members_list = $members->getManagedMembersList(true);
562 $groups = new Groups($this->zdb
, $this->login
);
563 $groups_list = $groups->getList();
565 //assign pagination variables to the template and add pagination links
566 $filters->setViewPagination($this->routeparser
, $this->view
, false);
567 $filters->setViewCommonsFilters($this->preferences
, $this->view
);
569 $this->session
->filter_members
= $filters;
574 'pages/members_list.html.twig',
576 'page_title' => _T("Members management"),
577 'require_mass' => true,
578 'members' => $members_list,
579 'filter_groups_options' => $groups_list,
580 'nb_members' => $members->getCount(),
581 'filters' => $filters,
582 'adv_filters' => $filters instanceof AdvancedMembersList
,
583 'galette_list' => $this->lists_config
->getDisplayElements($this->login
)
592 * @param Request $request PSR Request
593 * @param Response $response PSR Response
597 public function filter(Request
$request, Response
$response): Response
599 $post = $request->getParsedBody();
600 if (isset($this->session
->filter_members
)) {
601 //CAUTION: this one may be simple or advanced, display must change
602 $filters = $this->session
->filter_members
;
604 $filters = new MembersList();
607 //reintialize filters
608 if (isset($post['clear_filter'])) {
609 $filters = new MembersList();
610 } elseif (isset($post['clear_adv_filter'])) {
611 $this->session
->filter_members
= null;
612 unset($this->session
->filter_members
);
616 ->withHeader('Location', $this->routeparser
->urlFor('advanced-search'));
617 } elseif (isset($post['adv_criteria'])) {
620 ->withHeader('Location', $this->routeparser
->urlFor('advanced-search'));
623 if (isset($post['filter_str'])) { //filter search string
624 $filters->filter_str
= stripslashes(
625 htmlspecialchars($post['filter_str'], ENT_QUOTES
)
629 if (isset($post['field_filter'])) {
630 if (is_numeric($post['field_filter'])) {
631 $filters->field_filter
= $post['field_filter'];
634 //membership to filter
635 if (isset($post['membership_filter'])) {
636 if (is_numeric($post['membership_filter'])) {
637 $filters->membership_filter
638 = $post['membership_filter'];
641 //account status to filter
642 if (isset($post['filter_account'])) {
643 if (is_numeric($post['filter_account'])) {
644 $filters->filter_account
= $post['filter_account'];
648 if (isset($post['email_filter'])) {
649 $filters->email_filter
= (int)$post['email_filter'];
653 isset($post['group_filter'])
654 && $post['group_filter'] > 0
656 $filters->group_filter
= (int)$post['group_filter'];
658 //number of rows to show
659 if (isset($post['nbshow'])) {
660 $filters->show
= (int)$post['nbshow'];
663 if (isset($post['advanced_filtering'])) {
664 if (!$filters instanceof AdvancedMembersList
) {
665 $filters = new AdvancedMembersList($filters);
669 unset($post['advanced_filtering']);
671 foreach ($post as $k => $v) {
672 if (strpos($k, 'free_', 0) === 0) {
675 foreach ($post['free_field'] as $f) {
678 && trim($post['free_text'][$i]) !== ''
680 $fs_search = htmlspecialchars($post['free_text'][$i], ENT_QUOTES
);
682 = (int)$post['free_logical_operator'][$i];
684 = (int)$post['free_query_operator'][$i];
685 $type = (int)$post['free_type'][$i];
690 'search' => $fs_search,
694 $filters->free_search
= $fs;
700 } elseif ($k == 'groups_search') {
702 $filters->groups_search_log_op
= (int)$post['groups_logical_operator'];
703 foreach ($post['groups_search'] as $g) {
704 if (trim($g) !== '') {
709 $filters->groups_search
= $gs;
715 case 'contrib_min_amount':
716 case 'contrib_max_amount':
717 if (trim($v) !== '') {
730 if (isset($post['savesearch'])) {
735 $this->routeparser
->urlFor(
742 $this->session
->filter_members
= $filters;
746 ->withHeader('Location', $this->routeparser
->urlFor('members'));
750 * Advanced search page
752 * @param Request $request PSR Request
753 * @param Response $response PSR Response
757 public function advancedSearch(Request
$request, Response
$response): Response
759 if (isset($this->session
->filter_members
)) {
760 $filters = $this->session
->filter_members
;
761 if (!$filters instanceof AdvancedMembersList
) {
762 $filters = new AdvancedMembersList($filters);
765 $filters = new AdvancedMembersList();
768 $groups = new Groups($this->zdb
, $this->login
);
769 $groups_list = $groups->getList();
771 //we want only visible fields
772 $fields = $this->members_fields
;
773 $fc = $this->fields_config
;
774 $fc->filterVisible($this->login
, $fields);
776 //add status label search
777 if ($pos = array_search(Status
::PK
, array_keys($fields))) {
778 $fields = array_slice($fields, 0, $pos, true) +
779 ['status_label' => ['label' => _T('Status label')]] +
780 array_slice($fields, $pos, count($fields) - 1, true);
784 $member = new Adherent($this->zdb
);
787 ->enableDep('dynamics')
788 ->loadFromLoginOrMail($this->login
->login
);
789 $adh_dynamics = new DynamicFieldsHandle($this->zdb
, $this->login
, $member);
791 $contrib = new Contribution($this->zdb
, $this->login
);
792 $contrib_dynamics = new DynamicFieldsHandle($this->zdb
, $this->login
, $contrib);
795 $statuts = new Status($this->zdb
);
797 //Contributions types
798 $ct = new ContributionsTypes($this->zdb
);
801 $ptypes = new PaymentTypes(
806 $ptlist = $ptypes->getList();
808 $filters->setViewCommonsFilters($this->preferences
, $this->view
);
810 $social = new Social($this->zdb
);
811 $types = $member->getMemberRegisteredTypes();
813 foreach ($types as $type) {
814 $social_types[$type] = $social->getSystemType($type);
820 'pages/advanced_search.html.twig',
822 'page_title' => _T("Advanced search"),
823 'filter_groups_options' => $groups_list,
824 'search_fields' => $fields,
825 'adh_dynamics' => $adh_dynamics->getFields(),
826 'contrib_dynamics' => $contrib_dynamics->getFields(),
827 'adh_socials' => $social_types,
828 'statuts' => $statuts->getList(),
829 'contributions_types' => $ct->getList(),
830 'filters' => $filters,
831 'payments_types' => $ptlist
838 * Members list for ajax
840 * @param Request $request PSR Request
841 * @param Response $response PSR Response
842 * @param string|null $option One of 'page' or 'order'
843 * @param string|integer $value Value of the option
847 public function ajaxList(Request
$request, Response
$response, string $option = null, $value = null): Response
849 $post = $request->getParsedBody();
851 $filters = $this->session
->ajax_members_filters ??
new MembersList();
853 if ($option == 'page') {
854 $filters->current_page
= (int)$value;
857 //numbers of rows to display
858 if (isset($post['nbshow']) && is_numeric($post['nbshow'])) {
859 $filters->show
= (int)$post['nbshow'];
862 $members = new Members($filters);
863 if (!$this->login
->isAdmin() && !$this->login
->isStaff()) {
864 if ($this->login
->isGroupManager()) {
865 $members_list = $members->getManagedMembersList(true);
870 [$this->login
->id
, $this->login
->login
],
871 'Trying to list group members without access from #%id (%login)'
875 throw new \
Exception('Access denied.');
878 $members_list = $members->getMembersList(true);
881 //assign pagination variables to the template and add pagination links
882 $filters->setViewPagination($this->routeparser
, $this->view
, false);
884 $this->session
->ajax_members_filters
= $filters;
886 $selected_members = null;
887 $unreachables_members = null;
888 if (!isset($post['from'])) {
889 $mailing = $this->session
->mailing
;
890 if (!isset($post['members'])) {
891 $selected_members = $mailing->recipients
;
892 $unreachables_members = $mailing->unreachables
;
895 $selected_members = $m->getArrayList($post['members']);
896 if (isset($post['unreachables']) && is_array($post['unreachables'])) {
897 $unreachables_members = $m->getArrayList($post['unreachables']);
901 switch ($post['from']) {
903 if (!isset($post['gid'])) {
905 'Trying to list group members with no group id provided',
908 throw new \
Exception('A group id is required.');
910 if (!isset($post['members'])) {
911 $group = new Group((int)$post['gid']);
912 $selected_members = array();
913 if (!isset($post['mode']) ||
$post['mode'] == 'members') {
914 $selected_members = $group->getMembers();
915 } elseif ($post['mode'] == 'managers') {
916 $selected_members = $group->getManagers();
919 'Trying to list group members with unknown mode',
922 throw new \
Exception('Unknown mode.');
926 $selected_members = $m->getArrayList($post['members']);
927 if (isset($post['unreachables']) && is_array($post['unreachables'])) {
928 $unreachables_members = $m->getArrayList($post['unreachables']);
933 if (!isset($post['id_adh'])) {
934 throw new \
RuntimeException(
935 'Current selected member must be excluded while attaching!'
943 'filters' => $filters,
944 'members_list' => $members_list,
945 'selected_members' => $selected_members,
946 'unreachables_members' => $unreachables_members
949 if (isset($post['multiple'])) {
950 $params['multiple'] = true;
953 if (isset($post['gid'])) {
954 $params['the_id'] = (int)$post['gid'];
957 if (isset($post['id_adh'])) {
958 $params['excluded'] = (int)$post['id_adh'];
964 'elements/ajax_members.html.twig',
971 * Batch actions handler
973 * @param Request $request PSR Request
974 * @param Response $response PSR Response
978 public function handleBatch(Request
$request, Response
$response): Response
980 $post = $request->getParsedBody();
982 if (isset($post['entries_sel'])) {
983 if (isset($this->session
->filter_members
)) {
984 $filters = $this->session
->filter_members
;
986 $filters = new MembersList();
989 $filters->selected
= $post['entries_sel'];
990 $this->session
->filter_members
= $filters;
992 if (isset($post['cards'])) {
995 ->withHeader('Location', $this->routeparser
->urlFor('pdf-members-cards'));
998 if (isset($post['labels'])) {
1001 ->withHeader('Location', $this->routeparser
->urlFor('pdf-members-labels'));
1004 if (isset($post['sendmail'])) {
1007 ->withHeader('Location', $this->routeparser
->urlFor('mailing') . '?mailing_new=new');
1010 if (isset($post['attendance_sheet'])) {
1013 ->withHeader('Location', $this->routeparser
->urlFor('attendance_sheet_details'));
1016 if (isset($post['csv'])) {
1019 ->withHeader('Location', $this->routeparser
->urlFor('csv-memberslist'));
1022 if (isset($post['delete'])) {
1025 ->withHeader('Location', $this->routeparser
->urlFor('removeMembers'));
1028 if (isset($post['masschange'])) {
1031 ->withHeader('Location', $this->routeparser
->urlFor('masschangeMembers'));
1034 if (isset($post['masscontributions'])) {
1037 ->withHeader('Location', $this->routeparser
->urlFor('massAddContributionsChooseType'));
1040 throw new \
RuntimeException('Does not know what to batch :(');
1042 $this->flash
->addMessage(
1044 _T("No member was selected, please check at least one name.")
1049 ->withHeader('Location', $this->routeparser
->urlFor('members'));
1059 * @param Request $request PSR Request
1060 * @param Response $response PSR Response
1061 * @param integer $id Member id/array of members id
1062 * @param string $action null or 'add'
1066 public function edit(
1070 string $action = 'edit'
1072 //instantiate member object
1073 $member = new Adherent($this->zdb
);
1075 if ($this->session
->member
!== null) {
1076 //retrieve from session, in add or edit
1077 $member = $this->session
->member
;
1078 $this->session
->member
= null;
1081 $member->enableAllDeps();
1084 //load requested member
1086 $can = $member->canEdit($this->login
);
1088 $can = $member->canCreate($this->login
);
1092 $this->flash
->addMessage(
1094 _T("You do not have permission for requested URL.")
1100 $this->routeparser
->urlFor('me')
1104 //if adding a child, force parent here
1105 if ($action === 'addchild') {
1106 $member->setParent((int)$this->login
->id
);
1109 // flagging required fields
1110 $fc = $this->fields_config
;
1112 // password required if we create a new member
1114 $fc->setNotRequired('mdp_adh');
1117 //handle requirements for parent fields
1118 $parent_fields = $member->getParentFields();
1119 $tpl_parent_fields = []; //for JS when detaching
1120 foreach ($parent_fields as $field) {
1121 if ($fc->isRequired($field)) {
1122 $tpl_parent_fields[] = $field;
1123 if ($member->hasParent()) {
1124 $fc->setNotRequired($field);
1129 // flagging required fields invisible to members
1130 if ($this->login
->isAdmin() ||
$this->login
->isStaff()) {
1131 $fc->setNotRequired('activite_adh');
1132 $fc->setNotRequired('id_statut');
1135 // template variable declaration
1136 $title = _T("Member Profile");
1137 if ($member->id
!= '') {
1138 $title .= ' (' . _T("modification") . ')';
1140 $title .= ' (' . _T("creation") . ')';
1144 $statuts = new Status($this->zdb
);
1147 $groups = new Groups($this->zdb
, $this->login
);
1148 $groups_list = $groups->getSimpleList(true);
1150 $form_elements = $fc->getFormElements(
1158 if ($member->hasParent()) {
1159 $pid = ($member->parent
instanceof Adherent ?
$member->parent
->id
: $member->parent
);
1161 $members = $m->getDropdownMembers(
1167 $route_params['members'] = [
1168 'filters' => $m->getFilters(),
1169 'count' => $m->getCount()
1172 if (count($members)) {
1173 $route_params['members']['list'] = $members;
1176 if ($action === 'edit') {
1177 $route_params['navigate'] = $this->handleNavigationLinks($member->id
);
1181 $this->view
->render(
1183 'pages/member_form.html.twig',
1185 'parent_tpl' => 'page.html.twig',
1186 'autocomplete' => true,
1187 'page_title' => $title,
1188 'member' => $member,
1189 'self_adh' => false,
1190 // pseudo random int
1192 'titles_list' => Titles
::getList($this->zdb
),
1193 'statuts' => $statuts->getList(),
1194 'groups' => $groups_list,
1195 'fieldsets' => $form_elements['fieldsets'],
1196 'hidden_elements' => $form_elements['hiddens'],
1197 'parent_fields' => $tpl_parent_fields,
1198 'addchild' => ($action === 'addchild'),
1199 'osocials' => new Social($this->zdb
)
1208 * @param Request $request PSR Request
1209 * @param Response $response PSR Response
1210 * @param integer $id Member id
1214 public function doEdit(Request
$request, Response
$response, int $id): Response
1216 return $this->store($request, $response);
1220 * Massive change page
1222 * @param Request $request PSR Request
1223 * @param Response $response PSR Response
1227 public function massChange(Request
$request, Response
$response): Response
1229 $filters = $this->session
->filter_members
;
1232 'id' => $filters->selected
,
1233 'redirect_uri' => $this->routeparser
->urlFor('members')
1236 $fc = $this->fields_config
;
1237 $form_elements = $fc->getMassiveFormElements($this->members_fields
, $this->login
);
1240 $member = new Adherent($this->zdb
);
1241 $member->disableAllDeps()->enableDep('dynamics');
1244 $statuts = new Status($this->zdb
);
1247 $this->view
->render(
1249 'modals/mass_change_members.html.twig',
1251 'mode' => ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') ?
'ajax' : '',
1252 'page_title' => str_replace(
1255 _T('Mass change %count members')
1257 'form_url' => $this->routeparser
->urlFor('masschangeMembersReview'),
1258 'cancel_uri' => $this->routeparser
->urlFor('members'),
1260 'member' => $member,
1261 'fieldsets' => $form_elements['fieldsets'],
1262 'titles_list' => Titles
::getList($this->zdb
),
1263 'statuts' => $statuts->getList(),
1264 'require_mass' => true
1271 * Massive changes validation page
1273 * @param Request $request PSR Request
1274 * @param Response $response PSR Response
1278 public function validateMassChange(Request
$request, Response
$response): Response
1280 $post = $request->getParsedBody();
1283 if (!isset($post['confirm'])) {
1284 $this->flash
->addMessage(
1286 _T("Mass changes has not been confirmed!")
1289 //we want only visibles fields
1290 $fc = $this->fields_config
;
1291 $form_elements = $fc->getMassiveFormElements($this->members_fields
, $this->login
);
1293 foreach ($form_elements['fieldsets'] as $form_element) {
1294 foreach ($form_element->elements
as $field) {
1296 isset($post['mass_' . $field->field_id
])
1297 && (isset($post[$field->field_id
]) ||
$field->type
=== FieldsConfig
::TYPE_BOOL
)
1299 $changes[$field->field_id
] = [
1300 'label' => $field->label
,
1301 'value' => $post[$field->field_id
] ??
0
1307 //handle dynamic fields
1308 $member = new Adherent($this->zdb
);
1313 $this->members_fields
,
1316 $dynamic_fields = $member->getDynamicFields()->getFields();
1317 foreach ($dynamic_fields as $field) {
1318 $mass_id = 'mass_info_field_' . $field->getId();
1319 $field_id = 'info_field_' . $field->getId() . '_1';
1321 isset($post[$mass_id])
1322 && (isset($post[$field_id]) ||
$field instanceof Boolean
)
1324 $changes[$field_id] = [
1325 'label' => $field->getName(),
1326 'value' => $post[$field_id] ??
0
1332 $filters = $this->session
->filter_members
;
1334 'id' => $filters->selected
,
1335 'redirect_uri' => $this->routeparser
->urlFor('members')
1339 $statuts = new Status($this->zdb
);
1342 $this->view
->render(
1344 'modals/mass_change_members.html.twig',
1346 'mode' => ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') ?
'ajax' : '',
1347 'page_title' => str_replace(
1350 _T('Review mass change %count members')
1352 'form_url' => $this->routeparser
->urlFor('massstoremembers'),
1353 'cancel_uri' => $this->routeparser
->urlFor('members'),
1355 'titles_list' => Titles
::getList($this->zdb
),
1356 'statuts' => $statuts->getList(),
1357 'changes' => $changes
1364 * Do massive changes
1366 * @param Request $request PSR Request
1367 * @param Response $response PSR Response
1371 public function doMassChange(Request
$request, Response
$response): Response
1373 $post = $request->getParsedBody();
1374 $redirect_url = $post['redirect_uri'];
1375 $error_detected = [];
1377 $dynamic_fields = null;
1379 unset($post['redirect_uri']);
1380 if (!isset($post['confirm'])) {
1381 $error_detected[] = _T("Mass changes has not been confirmed!");
1383 unset($post['confirm']);
1387 $fc = $this->fields_config
;
1388 $form_elements = $fc->getMassiveFormElements($this->members_fields
, $this->login
);
1389 $disabled = $this->members_fields
;
1390 foreach (array_keys($post) as $key) {
1392 foreach ($form_elements['fieldsets'] as $fieldset) {
1393 if (isset($fieldset->elements
[$key])) {
1400 //try on dynamic fields
1401 if ($dynamic_fields === null) {
1402 //handle dynamic fields
1403 $member = new Adherent($this->zdb
);
1408 $this->members_fields
,
1411 $dynamic_fields = $member->getDynamicFields()->getFields();
1413 foreach ($dynamic_fields as $field) {
1414 $field_id = 'info_field_' . $field->getId() . '_1';
1415 if ($key == $field_id) {
1424 'Permission issue mass editing field ' . $key,
1429 unset($disabled[$key]);
1433 if (!count($post)) {
1434 $error_detected[] = _T("Nothing to do!");
1436 foreach ($ids as $id) {
1437 $is_manager = !$this->login
->isAdmin()
1438 && !$this->login
->isStaff()
1439 && $this->login
->isGroupManager();
1440 $member = new Adherent($this->zdb
);
1441 $member->disableAllDeps();
1443 $member->enableDep('groups');
1445 $member->load((int)$id);
1446 $member->setDependencies(
1448 $this->members_fields
,
1451 if (!$member->canEdit($this->login
)) {
1455 $valid = $member->check($post, [], $disabled);
1456 if ($valid === true) {
1457 $done = $member->store();
1459 $error_detected[] = _T("An error occurred while storing the member.");
1464 $error_detected = array_merge($error_detected, $valid);
1470 if ($mass == 0 && !count($error_detected)) {
1471 $error_detected[] = _T('Something went wront during mass edition!');
1473 $this->flash
->addMessage(
1478 _T('%count members has been changed successfully!')
1483 if (count($error_detected) > 0) {
1484 foreach ($error_detected as $error) {
1485 $this->flash
->addMessage(
1492 if (!($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest')) {
1495 ->withHeader('Location', $redirect_url);
1497 return $this->withJson(
1500 'success' => count($error_detected) === 0
1509 * @param Request $request PSR Request
1510 * @param Response $response PSR Response
1514 public function store(Request
$request, Response
$response): Response
1516 if (!$this->preferences
->pref_bool_selfsubscribe
&& !$this->login
->isLogged()) {
1519 ->withHeader('Location', $this->routeparser
->urlFor('slash'));
1522 $post = $request->getParsedBody();
1523 $member = new Adherent($this->zdb
);
1528 $this->members_fields
,
1532 $success_detected = [];
1533 $warning_detected = [];
1534 $error_detected = [];
1536 if ($this->isSelfMembership() && !isset($post[Adherent
::PK
])) {
1537 //mark as self membership
1538 $member->setSelfMembership();
1541 $gaptcha = $this->session
->gaptcha
;
1542 if (!$gaptcha->check($post['gaptcha'])) {
1543 $error_detected[] = _T('Invalid captcha');
1548 if (isset($post['id_adh'])) {
1549 $member->load((int)$post['id_adh']);
1550 if (!$member->canEdit($this->login
)) {
1551 //redirection should have been done before. Just throw an Exception.
1552 throw new \
RuntimeException(
1556 'No right to store member #%id'
1561 if ($member->id
!= '') {
1562 $member->load($this->login
->id
);
1566 // flagging required fields
1567 $fc = $this->fields_config
;
1569 // password required if we create a new member but not from self subscription
1570 if ($member->id
!= '' ||
$this->isSelfMembership()) {
1571 $fc->setNotRequired('mdp_adh');
1575 $member->hasParent() && !isset($post['detach_parent'])
1576 ||
isset($post['parent_id']) && !empty($post['parent_id'])
1578 $parent_fields = $member->getParentFields();
1579 foreach ($parent_fields as $field) {
1580 if ($fc->isRequired($field)) {
1581 $fc->setNotRequired($field);
1586 // flagging required fields invisible to members
1587 if ($this->login
->isAdmin() ||
$this->login
->isStaff()) {
1588 $fc->setNotRequired('activite_adh');
1589 $fc->setNotRequired('id_statut');
1592 $form_elements = $fc->getFormElements(
1595 $this->isSelfMembership()
1597 $fieldsets = $form_elements['fieldsets'];
1598 $required = array();
1599 $disabled = array();
1601 foreach ($fieldsets as $category) {
1602 foreach ($category->elements
as $field) {
1603 if ($field->required
== true) {
1604 $required[$field->field_id
] = true;
1606 if ($field->disabled
== true) {
1607 $disabled[$field->field_id
] = true;
1608 } elseif (!isset($post[$field->field_id
])) {
1609 switch ($field->field_id
) {
1610 //unchecked booleans are not sent from form
1611 case 'bool_admin_adh':
1612 case 'bool_exempt_adh':
1613 case 'bool_display_info':
1614 $post[$field->field_id
] = 0;
1621 $real_requireds = array_diff(array_keys($required), array_keys($disabled));
1623 // send email to member
1624 if ($this->isSelfMembership() ||
isset($post['mail_confirm']) && $post['mail_confirm'] == '1') {
1625 $member->setSendmail(); //flag to send creation email
1629 $redirect_url = $this->routeparser
->urlFor('member', ['id' => $member->id
]);
1630 if (!count($real_requireds) ||
isset($post[array_shift($real_requireds)])) {
1632 $valid = $member->check($post, $required, $disabled);
1633 if ($valid !== true) {
1634 $error_detected = array_merge($error_detected, $valid);
1637 if (count($error_detected) == 0) {
1638 //all goes well, we can proceed
1641 if ($member->id
== '') {
1645 $store = $member->store();
1646 if ($store === true) {
1647 //member has been stored :)
1649 if ($this->isSelfMembership()) {
1650 $success_detected[] = _T("Your account has been created!");
1652 $this->preferences
->pref_mail_method
> GaletteMail
::METHOD_DISABLED
1653 && $member->getEmail() != ''
1655 $success_detected[] = _T("An email has been sent to you, check your inbox.");
1658 $success_detected[] = _T("New member has been successfully added.");
1661 $success_detected[] = _T("Member account has been modified.");
1664 if ($this->login
->isGroupManager()) {
1665 //add/remove user from groups
1666 $groups_adh = $post['groups_adh'] ??
null;
1667 $add_groups = Groups
::addMemberToGroups(
1672 if ($add_groups === false) {
1673 $error_detected[] = _T("An error occurred adding member to its groups.");
1676 if ($this->login
->isSuperAdmin() ||
$this->login
->isAdmin() ||
$this->login
->isStaff()) {
1677 //add/remove manager from groups
1678 $managed_groups_adh = $post['groups_managed_adh'] ??
null;
1679 $add_groups = Groups
::addMemberToGroups(
1681 $managed_groups_adh,
1684 $member->loadGroups();
1686 if ($add_groups === false) {
1687 $error_detected[] = _T("An error occurred adding member to its groups as manager.");
1691 //something went wrong :'(
1692 $error_detected[] = _T("An error occurred while storing the member.");
1696 if (count($error_detected) === 0) {
1697 $files_res = $member->handleFiles($_FILES);
1698 if (is_array($files_res)) {
1699 $error_detected = array_merge($error_detected, $files_res);
1702 if (isset($post['del_photo'])) {
1703 if (!$member->picture
->delete($member->id
)) {
1704 $error_detected[] = _T("Delete failed");
1705 $str_adh = $member->id
. ' (' . $member->sname
. ' ' . ')';
1707 'Unable to delete picture for member ' . $str_adh,
1714 if (count($error_detected) > 0) {
1715 foreach ($error_detected as $error) {
1716 if (strpos($error, '%member_url_') !== false) {
1717 preg_match('/%member_url_(\d+)/', $error, $matches);
1718 $url = $this->routeparser
->urlFor('member', ['id' => $matches[1]]);
1719 $error = str_replace(
1720 '%member_url_' . $matches[1],
1725 $this->flash
->addMessage(
1732 if (count($warning_detected) > 0) {
1733 foreach ($warning_detected as $warning) {
1734 $this->flash
->addMessage(
1740 if (count($success_detected) > 0) {
1741 foreach ($success_detected as $success) {
1742 $this->flash
->addMessage(
1749 if (count($error_detected) === 0) {
1750 if ($this->isSelfMembership()) {
1751 $redirect_url = $this->routeparser
->urlFor('login');
1753 isset($post['redirect_on_create'])
1754 && $post['redirect_on_create'] > Adherent
::AFTER_ADD_DEFAULT
1756 switch ($post['redirect_on_create']) {
1757 case Adherent
::AFTER_ADD_TRANS
:
1758 $redirect_url = $this->routeparser
->urlFor('addTransaction');
1760 case Adherent
::AFTER_ADD_NEW
:
1761 $redirect_url = $this->routeparser
->urlFor('addMember');
1763 case Adherent
::AFTER_ADD_SHOW
:
1764 $redirect_url = $this->routeparser
->urlFor('member', ['id' => $member->id
]);
1766 case Adherent
::AFTER_ADD_LIST
:
1767 $redirect_url = $this->routeparser
->urlFor('members');
1769 case Adherent
::AFTER_ADD_HOME
:
1770 $redirect_url = $this->routeparser
->urlFor('slash');
1773 } elseif (!isset($post['id_adh']) && !$member->isDueFree()) {
1774 $redirect_url = $this->routeparser
->urlFor(
1777 ) . '?id_adh=' . $member->id
;
1779 $redirect_url = $this->routeparser
->urlFor('member', ['id' => $member->id
]);
1782 //store entity in session
1783 $this->session
->member
= $member;
1785 if ($this->isSelfMembership()) {
1786 $redirect_url = $this->routeparser
->urlFor('subscribe');
1789 $redirect_url = $this->routeparser
->urlFor(
1791 ['id' => $member->id
]
1794 $redirect_url = $this->routeparser
->urlFor((isset($post['addchild']) ?
'addMemberChild' : 'addMember'));
1802 ->withHeader('Location', $redirect_url);
1810 * Get redirection URI
1812 * @param array $args Route arguments
1816 public function redirectUri(array $args)
1818 return $this->routeparser
->urlFor('members');
1824 * @param array $args Route arguments
1828 public function formUri(array $args)
1830 return $this->routeparser
->urlFor(
1840 * In simple cases, we get the ID in the route arguments; but for
1841 * batchs, it should be found elsewhere.
1842 * In post values, we look for id key, as well as all entries_sel keys
1844 * @param array $args Request arguments
1845 * @param array $post POST values
1847 * @return null|integer|integer[]
1849 protected function getIdsToRemove(&$args, $post)
1851 if (isset($args['id'])) {
1854 $filters = $this->session
->filter_members
;
1855 return $filters->selected
;
1860 * Get confirmation removal page title
1862 * @param array $args Route arguments
1866 public function confirmRemoveTitle(array $args)
1868 if (isset($args['id_adh']) ||
isset($args['id'])) {
1869 //one member removal
1870 $id_adh = $args['id_adh'] ??
$args['id'];
1871 $adh = new Adherent($this->zdb
, (int)$id_adh);
1873 _T('Remove member %1$s'),
1877 //batch members removal
1878 $filters = $this->session
->filter_members
;
1881 count($filters->selected
),
1882 _T('You are about to remove %count members.')
1890 * @param array $args Route arguments
1891 * @param array $post POST values
1895 protected function doDelete(array $args, array $post)
1897 if (isset($this->session
->filter_members
)) {
1898 $filters = $this->session
->filter_members
;
1900 $filters = new MembersList();
1902 $members = new Members($filters);
1904 if (!is_array($post['id'])) {
1905 $ids = (array)$post['id'];
1910 return $members->removeMembers($ids);
1916 * Set self memebrship flag
1918 * @return MembersController
1920 private function setSelfMembership(): MembersController
1922 $this->is_self_membership
= true;
1927 * Is self membership?
1931 private function isSelfMembership(): bool
1933 return $this->is_self_membership
;
1937 * Handle navigation links
1939 * @param int $id_adh Current member ID
1943 private function handleNavigationLinks(int $id_adh): array
1945 $navigate = array();
1947 if (isset($this->session
->filter_members
)) {
1948 $filters = $this->session
->filter_members
;
1950 $filters = new MembersList();
1952 //we must navigate between all members
1956 $this->login
->isAdmin()
1957 ||
$this->login
->isStaff()
1958 ||
$this->login
->isGroupManager()
1960 $m = new Members($filters);
1963 $fields = [Adherent
::PK
, 'nom_adh', 'prenom_adh'];
1964 if ($this->login
->isAdmin() ||
$this->login
->isStaff()) {
1965 $ids = $m->getMembersList(false, $fields);
1967 $ids = $m->getManagedMembersList(false, $fields);
1970 $ids = $ids->toArray();
1971 foreach ($ids as $k => $m) {
1972 if ($m['id_adh'] == $id_adh) {
1974 'cur' => $m['id_adh'],
1975 'count' => $filters->counter
,
1979 $navigate['prev'] = $ids[$k - 1]['id_adh'];
1981 if ($k < count($ids) - 1) {
1982 $navigate['next'] = $ids[$k +
1]['id_adh'];