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 Galette\Features\BatchList
;
42 use Slim\Psr7\Request
;
43 use Slim\Psr7\Response
;
44 use Galette\Core\GaletteMail
;
45 use Galette\Core\Gaptcha
;
46 use Galette\Entity\Adherent
;
47 use Galette\Entity\Contribution
;
48 use Galette\Entity\ContributionsTypes
;
49 use Galette\Entity\DynamicFieldsHandle
;
50 use Galette\Entity\Group
;
51 use Galette\Entity\Status
;
52 use Galette\Entity\FieldsConfig
;
53 use Galette\Entity\Social
;
54 use Galette\Filters\AdvancedMembersList
;
55 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
80 private $is_self_membership = false;
87 * @param Request $request PSR Request
88 * @param Response $response PSR Response
92 public function add(Request
$request, Response
$response): Response
94 return $this->edit($request, $response, null, 'add');
100 * @param Request $request PSR Request
101 * @param Response $response PSR Response
105 public function addChild(Request
$request, Response
$response): Response
107 return $this->edit($request, $response, null, 'addchild');
111 * Self subscription page
113 * @param Request $request PSR Request
114 * @param Response $response PSR Response
118 public function selfSubscribe(Request
$request, Response
$response): Response
120 if (!$this->preferences
->pref_bool_selfsubscribe ||
$this->login
->isLogged()) {
123 ->withHeader('Location', $this->routeparser
->urlFor('slash'));
126 if ($this->session
->member
!== null) {
127 $member = $this->session
->member
;
128 $this->session
->member
= null;
130 $member = new Adherent($this->zdb
);
131 $member->enableDep('dynamics');
134 //mark as self membership
135 $member->setSelfMembership();
137 // flagging required fields
138 $fc = $this->fields_config
;
139 $form_elements = $fc->getFormElements($this->login
, true, true);
143 $members = $m->getDropdownMembers(
146 $member->hasParent() ?
$member->parent
->id
: null
151 'filters' => $m->getFilters(),
152 'count' => $m->getCount()
156 if (count($members)) {
157 $params['members']['list'] = $members;
160 $gaptcha = new Gaptcha($this->i18n
);
161 $this->session
->gaptcha
= $gaptcha;
163 $titles = new Titles($this->zdb
);
168 'pages/member_form.html.twig',
170 'page_title' => _T("Subscription"),
171 'parent_tpl' => 'public_page.html.twig',
174 'autocomplete' => true,
175 'osocials' => new Social($this->zdb
),
178 'titles_list' => $titles->getList(),
179 'fieldsets' => $form_elements['fieldsets'],
180 'hidden_elements' => $form_elements['hiddens'],
182 'gaptcha' => $gaptcha
191 * @param Request $request PSR Request
192 * @param Response $response PSR Response
196 public function doAdd(Request
$request, Response
$response): Response
198 return $this->store($request, $response);
202 * Self subscription add action
204 * @param Request $request PSR Request
205 * @param Response $response PSR Response
209 public function doSelfSubscribe(Request
$request, Response
$response): Response
211 $this->setSelfMembership();
212 return $this->doAdd($request, $response);
219 * @param Request $request PSR Request
220 * @param Response $response PSR Response
221 * @param integer $id_adh Member ID to duplicate
225 public function duplicate(Request
$request, Response
$response, int $id_adh): Response
227 $adh = new Adherent($this->zdb
, $id_adh, ['dynamics' => true, 'parent' => true]);
228 $adh->setDuplicate();
230 //store entity in session
231 $this->session
->member
= $adh;
235 ->withHeader('Location', $this->routeparser
->urlFor('addMember'));
242 * Display member card
244 * @param Request $request PSR Request
245 * @param Response $response PSR Response
246 * @param integer $id Member ID
250 public function show(Request
$request, Response
$response, int $id): Response
252 $member = new Adherent($this->zdb
);
257 if (!$member->canShow($this->login
)) {
258 $this->flash
->addMessage(
260 _T("You do not have permission for requested URL.")
267 $this->routeparser
->urlFor('me')
271 if ($member->id
== null) {
272 //member does not exist!
273 $this->flash
->addMessage(
275 str_replace('%id', $id, _T("No member #%id."))
281 $this->routeparser
->urlFor('slash')
285 // flagging fields visibility
286 $fc = $this->fields_config
;
287 $display_elements = $fc->getDisplayElements($this->login
);
292 'pages/member_show.html.twig',
294 'page_title' => _T("Member Profile"),
296 'pref_lang' => $this->i18n
->getNameFromId($member->language
),
297 'pref_card_self' => $this->preferences
->pref_card_self
,
298 'groups' => Groups
::getSimpleList(),
300 'display_elements' => $display_elements,
301 'osocials' => new Social($this->zdb
),
302 'navigate' => $this->handleNavigationLinks($member->id
)
311 * @param Request $request PSR Request
312 * @param Response $response PSR Response
316 public function showMe(Request
$request, Response
$response): Response
318 if ($this->login
->isSuperAdmin()) {
321 ->withHeader('Location', $this->routeparser
->urlFor('slash'));
323 return $this->show($request, $response, $this->login
->id
);
327 * Public pages (trombinoscope, public list)
329 * @param Request $request PSR Request
330 * @param Response $response PSR Response
331 * @param string $option One of 'page' or 'order'
332 * @param string|integer $value Value of the option
333 * @param string $type List type (either list or trombi)
337 public function publicList(
344 $varname = $this->getFilterName(['prefix' => 'public', 'suffix' => $type]);
345 if (isset($this->session
->$varname)) {
346 $filters = $this->session
->$varname;
348 $filters = new MembersList();
351 if ($option !== null) {
354 $filters->current_page
= (int)$value;
357 $filters->orderby
= $value;
362 $m = new Members($filters);
363 $members = $m->getPublicList($type === 'trombi');
365 $this->session
->$varname = $filters;
367 //assign pagination variables to the template and add pagination links
368 $filters->setViewPagination($this->routeparser
, $this->view
, false);
373 ($type === 'list' ?
'pages/members_public_list' : 'pages/members_public_gallery') . '.html.twig',
375 'page_title' => ($type === 'list' ?
_T("Members list") : _T('Trombinoscope')),
376 'additionnal_html_class' => ($type === 'list' ?
'' : 'trombinoscope'),
378 'members' => $members,
379 'nb_members' => $m->getCount(),
380 'filters' => $filters,
389 * Public pages (trombinoscope, public list)
391 * @param Request $request PSR Request
392 * @param Response $response PSR Response
393 * @param string $type Type
397 public function filterPublicList(Request
$request, Response
$response, string $type): Response
399 $post = $request->getParsedBody();
401 $varname = $this->getFilterName(['prefix' => 'public', 'suffix' => $type]);
402 if (isset($this->session
->$varname)) {
403 $filters = $this->session
->$varname;
405 $filters = new MembersList();
408 //reintialize filters
409 if (isset($post['clear_filter'])) {
412 //number of rows to show
413 if (isset($post['nbshow'])) {
414 $filters->show
= (int)$post['nbshow'];
418 $this->session
->$varname = $filters;
422 ->withHeader('Location', $this->routeparser
->urlFor('publicList', ['type' => $type]));
428 * @param Request $request PSR Request
429 * @param Response $response PSR Response
430 * @param string $option One of 'page' or 'order'
431 * @param string|integer $value Value of the option
435 public function list(Request
$request, Response
$response, $option = null, $value = null): Response
437 if (isset($this->session
->{$this->getFilterName()})) {
438 $filters = $this->session
->{$this->getFilterName()};
440 $filters = new MembersList();
443 if ($option !== null) {
446 $filters->current_page
= (int)$value;
449 $filters->orderby
= $value;
454 $members = new Members($filters);
456 $members_list = array();
457 if ($this->login
->isAdmin() ||
$this->login
->isStaff()) {
458 $members_list = $members->getMembersList(true);
460 $members_list = $members->getManagedMembersList(true);
463 $groups = new Groups($this->zdb
, $this->login
);
464 $groups_list = $groups->getList();
466 //assign pagination variables to the template and add pagination links
467 $filters->setViewPagination($this->routeparser
, $this->view
, false);
468 $filters->setViewCommonsFilters($this->preferences
, $this->view
);
470 $this->session
->{$this->getFilterName()} = $filters;
475 'pages/members_list.html.twig',
477 'page_title' => _T("Members management"),
478 'require_mass' => true,
479 'members' => $members_list,
480 'filter_groups_options' => $groups_list,
481 'nb_members' => $members->getCount(),
482 'filters' => $filters,
483 'adv_filters' => $filters instanceof AdvancedMembersList
,
484 'galette_list' => $this->lists_config
->getDisplayElements($this->login
)
493 * @param Request $request PSR Request
494 * @param Response $response PSR Response
498 public function filter(Request
$request, Response
$response): Response
500 $post = $request->getParsedBody();
501 $filters = $this->session
->{$this->getFilterName()} ??
new MembersList();
503 //reinitialize filters
504 if (isset($post['clear_filter'])) {
505 $filters = new MembersList();
506 } elseif (isset($post['clear_adv_filter'])) {
507 $this->session
->{$this->getFilterName()} = null;
508 unset($this->session
->{$this->getFilterName()});
512 ->withHeader('Location', $this->routeparser
->urlFor('advanced-search'));
513 } elseif (isset($post['adv_criteria'])) {
516 ->withHeader('Location', $this->routeparser
->urlFor('advanced-search'));
519 if (isset($post['filter_str'])) { //filter search string
520 $filters->filter_str
= stripslashes(
521 htmlspecialchars($post['filter_str'], ENT_QUOTES
)
525 if (isset($post['field_filter'])) {
526 if (is_numeric($post['field_filter'])) {
527 $filters->field_filter
= $post['field_filter'];
530 //membership to filter
531 if (isset($post['membership_filter'])) {
532 if (is_numeric($post['membership_filter'])) {
533 $filters->membership_filter
534 = $post['membership_filter'];
537 //account status to filter
538 if (isset($post['filter_account'])) {
539 if (is_numeric($post['filter_account'])) {
540 $filters->filter_account
= $post['filter_account'];
544 if (isset($post['email_filter'])) {
545 $filters->email_filter
= (int)$post['email_filter'];
549 isset($post['group_filter'])
550 && $post['group_filter'] > 0
552 $filters->group_filter
= (int)$post['group_filter'];
554 //number of rows to show
555 if (isset($post['nbshow'])) {
556 $filters->show
= (int)$post['nbshow'];
559 if (isset($post['advanced_filtering'])) {
560 if (!$filters instanceof AdvancedMembersList
) {
561 $filters = new AdvancedMembersList($filters);
565 unset($post['advanced_filtering']);
567 foreach ($post as $k => $v) {
568 if (strpos($k, 'free_', 0) === 0) {
571 foreach ($post['free_field'] as $f) {
574 && trim($post['free_text'][$i]) !== ''
576 $fs_search = htmlspecialchars($post['free_text'][$i], ENT_QUOTES
);
578 = (int)$post['free_logical_operator'][$i];
580 = (int)$post['free_query_operator'][$i];
581 $type = (int)$post['free_type'][$i];
586 'search' => $fs_search,
590 $filters->free_search
= $fs;
596 } elseif ($k == 'groups_search') {
598 $filters->groups_search_log_op
= (int)$post['groups_logical_operator'];
599 foreach ($post['groups_search'] as $g) {
600 if (trim($g) !== '') {
605 $filters->groups_search
= $gs;
611 case 'contrib_min_amount':
612 case 'contrib_max_amount':
613 if (trim($v) !== '') {
626 if (isset($post['savesearch'])) {
631 $this->routeparser
->urlFor(
638 $this->session
->{$this->getFilterName()} = $filters;
642 ->withHeader('Location', $this->routeparser
->urlFor('members'));
646 * Advanced search page
648 * @param Request $request PSR Request
649 * @param Response $response PSR Response
653 public function advancedSearch(Request
$request, Response
$response): Response
655 if (isset($this->session
->{$this->getFilterName()})) {
656 $filters = $this->session
->{$this->getFilterName()};
657 if (!$filters instanceof AdvancedMembersList
) {
658 $filters = new AdvancedMembersList($filters);
661 $filters = new AdvancedMembersList();
664 $groups = new Groups($this->zdb
, $this->login
);
665 $groups_list = $groups->getList();
667 //we want only visible fields
668 $fields = $this->members_fields
;
669 $fc = $this->fields_config
;
670 $fc->filterVisible($this->login
, $fields);
673 $member = new Adherent($this->zdb
);
676 ->enableDep('dynamics')
677 ->loadFromLoginOrMail($this->login
->login
);
678 $adh_dynamics = new DynamicFieldsHandle($this->zdb
, $this->login
, $member);
680 $contrib = new Contribution($this->zdb
, $this->login
);
681 $contrib_dynamics = new DynamicFieldsHandle($this->zdb
, $this->login
, $contrib);
684 $statuts = new Status($this->zdb
);
686 //Contributions types
687 $ct = new ContributionsTypes($this->zdb
);
690 $ptypes = new PaymentTypes(
695 $ptlist = $ptypes->getList();
697 $filters->setViewCommonsFilters($this->preferences
, $this->view
);
699 $social = new Social($this->zdb
);
700 $types = $member->getMemberRegisteredTypes();
702 foreach ($types as $type) {
703 $social_types[$type] = $social->getSystemType($type);
709 'pages/advanced_search.html.twig',
711 'page_title' => _T("Advanced search"),
712 'filter_groups_options' => $groups_list,
713 'search_fields' => $fields,
714 'adh_dynamics' => $adh_dynamics->getSearchFields(),
715 'contrib_dynamics' => $contrib_dynamics->getSearchFields(),
716 'adh_socials' => $social_types,
717 'statuts' => $statuts->getList(),
718 'contributions_types' => $ct->getList(),
719 'filters' => $filters,
720 'payments_types' => $ptlist
727 * Members list for ajax
729 * @param Request $request PSR Request
730 * @param Response $response PSR Response
731 * @param string|null $option One of 'page' or 'order'
732 * @param string|integer $value Value of the option
736 public function ajaxList(Request
$request, Response
$response, string $option = null, $value = null): Response
738 $post = $request->getParsedBody();
740 $filters = $this->session
->{$this->getFilterName(['prefix' => 'ajax'])} ??
new MembersList();
742 if ($option == 'page') {
743 $filters->current_page
= (int)$value;
746 //numbers of rows to display
747 if (isset($post['nbshow']) && is_numeric($post['nbshow'])) {
748 $filters->show
= (int)$post['nbshow'];
751 $members = new Members($filters);
752 if (!$this->login
->isAdmin() && !$this->login
->isStaff()) {
753 if ($this->login
->isGroupManager()) {
754 $members_list = $members->getManagedMembersList(true);
759 [$this->login
->id
, $this->login
->login
],
760 'Trying to list group members without access from #%id (%login)'
764 throw new \
Exception('Access denied.');
767 $members_list = $members->getMembersList(true);
770 //assign pagination variables to the template and add pagination links
771 $filters->setViewPagination($this->routeparser
, $this->view
, false);
773 $this->session
->{$this->getFilterName(['prefix' => 'ajax'])} = $filters;
775 $selected_members = null;
776 $unreachables_members = null;
777 if (!isset($post['from'])) {
778 $mailing = $this->session
->mailing
;
779 if (!isset($post['members'])) {
780 $selected_members = $mailing->recipients
;
781 $unreachables_members = $mailing->unreachables
;
784 $selected_members = $m->getArrayList($post['members']);
785 if (isset($post['unreachables']) && is_array($post['unreachables'])) {
786 $unreachables_members = $m->getArrayList($post['unreachables']);
790 switch ($post['from']) {
792 if (!isset($post['gid'])) {
794 'Trying to list group members with no group id provided',
797 throw new \
Exception('A group id is required.');
799 if (!isset($post['members'])) {
800 $group = new Group((int)$post['gid']);
801 $selected_members = array();
802 if (!isset($post['mode']) ||
$post['mode'] == 'members') {
803 $selected_members = $group->getMembers();
804 } elseif ($post['mode'] == 'managers') {
805 $selected_members = $group->getManagers();
808 'Trying to list group members with unknown mode',
811 throw new \
Exception('Unknown mode.');
815 $selected_members = $m->getArrayList($post['members']);
816 if (isset($post['unreachables']) && is_array($post['unreachables'])) {
817 $unreachables_members = $m->getArrayList($post['unreachables']);
822 if (!isset($post['id_adh'])) {
823 throw new \
RuntimeException(
824 'Current selected member must be excluded while attaching!'
832 'filters' => $filters,
833 'members_list' => $members_list,
834 'selected_members' => $selected_members,
835 'unreachables_members' => $unreachables_members
838 if (isset($post['multiple'])) {
839 $params['multiple'] = true;
842 if (isset($post['gid'])) {
843 $params['the_id'] = (int)$post['gid'];
846 if (isset($post['id_adh'])) {
847 $params['excluded'] = (int)$post['id_adh'];
853 'elements/ajax_members.html.twig',
860 * Batch actions handler
862 * @param Request $request PSR Request
863 * @param Response $response PSR Response
867 public function handleBatch(Request
$request, Response
$response): Response
869 $post = $request->getParsedBody();
871 if (isset($post['entries_sel'])) {
872 if (isset($this->session
->{$this->getFilterName()})) {
873 $filters = $this->session
->{$this->getFilterName()};
875 $filters = new MembersList();
878 $filters->selected
= $post['entries_sel'];
880 'cards' => 'pdf-members-cards',
881 'labels' => 'pdf-members-labels',
882 'sendmail' => 'mailing',
883 'attendance_sheet' => 'attendance_sheet_details',
884 'csv' => 'csv-memberslist',
885 'delete' => 'removeMembers',
886 'masschange' => 'masschangeMembers',
887 'masscontributions' => 'massAddContributionsChooseType'
890 foreach ($knowns as $known => $redirect_url) {
891 if (isset($post[$known])) {
892 $this->session
->{$this->getFilterName(['suffix' => $known])} = $filters;
893 $redirect_url = $this->routeparser
->urlFor($redirect_url);
894 if ($known === 'sendmail') {
895 $redirect_url .= '?mailing_new=new';
899 ->withHeader('Location', $redirect_url);
903 throw new \
RuntimeException('Does not know what to batch :(');
905 $this->flash
->addMessage(
907 _T("No member was selected, please check at least one name.")
912 ->withHeader('Location', $this->routeparser
->urlFor('members'));
922 * @param Request $request PSR Request
923 * @param Response $response PSR Response
924 * @param ?integer $id Member id/array of members id
925 * @param string $action null or 'add'
929 public function edit(
933 string $action = 'edit'
935 //instantiate member object
936 $member = new Adherent($this->zdb
);
938 if ($this->session
->member
!== null) {
939 //retrieve from session, in add or edit
940 $member = $this->session
->member
;
941 $this->session
->member
= null;
944 $member->enableAllDeps();
947 //load requested member
949 $can = $member->canEdit($this->login
);
951 $can = $member->canCreate($this->login
);
955 $this->flash
->addMessage(
957 _T("You do not have permission for requested URL.")
963 $this->routeparser
->urlFor('me')
967 //if adding a child, force parent here
968 if ($action === 'addchild') {
969 $member->setParent((int)$this->login
->id
);
972 // flagging required fields
973 $fc = $this->fields_config
;
975 // password required if we create a new member
977 $fc->setNotRequired('mdp_adh');
980 //handle requirements for parent fields
981 $parent_fields = $member->getParentFields();
982 $tpl_parent_fields = []; //for JS when detaching
983 foreach ($parent_fields as $field) {
984 if ($fc->isRequired($field)) {
985 $tpl_parent_fields[] = $field;
986 if ($member->hasParent()) {
987 $fc->setNotRequired($field);
992 // flagging required fields invisible to members
993 if ($this->login
->isAdmin() ||
$this->login
->isStaff()) {
994 $fc->setNotRequired('activite_adh');
995 $fc->setNotRequired('id_statut');
998 // template variable declaration
999 $title = _T("Member Profile");
1000 if ($member->id
!= '') {
1001 $title .= ' (' . _T("modification") . ')';
1003 $title .= ' (' . _T("creation") . ')';
1007 $statuts = new Status($this->zdb
);
1009 $titles = new Titles($this->zdb
);
1012 $groups = new Groups($this->zdb
, $this->login
);
1013 $groups_list = $groups->getSimpleList(true);
1015 $form_elements = $fc->getFormElements(
1023 if ($member->hasParent()) {
1024 $pid = ($member->parent
instanceof Adherent ?
$member->parent
->id
: $member->parent
);
1026 $members = $m->getDropdownMembers(
1032 $route_params['members'] = [
1033 'filters' => $m->getFilters(),
1034 'count' => $m->getCount()
1037 if (count($members)) {
1038 $route_params['members']['list'] = $members;
1041 if ($action === 'edit') {
1042 $route_params['navigate'] = $this->handleNavigationLinks($member->id
);
1046 $this->view
->render(
1048 'pages/member_form.html.twig',
1050 'parent_tpl' => 'page.html.twig',
1051 'autocomplete' => true,
1052 'page_title' => $title,
1053 'member' => $member,
1054 'self_adh' => false,
1055 // pseudo random int
1057 'titles_list' => $titles->getList(),
1058 'statuts' => $statuts->getList(),
1059 'groups' => $groups_list,
1060 'fieldsets' => $form_elements['fieldsets'],
1061 'hidden_elements' => $form_elements['hiddens'],
1062 'parent_fields' => $tpl_parent_fields,
1063 'addchild' => ($action === 'addchild'),
1064 'osocials' => new Social($this->zdb
)
1073 * @param Request $request PSR Request
1074 * @param Response $response PSR Response
1075 * @param integer $id Member id
1079 public function doEdit(Request
$request, Response
$response, int $id): Response
1081 return $this->store($request, $response);
1085 * Massive change page
1087 * @param Request $request PSR Request
1088 * @param Response $response PSR Response
1092 public function massChange(Request
$request, Response
$response): Response
1094 $filters = $this->session
->{$this->getFilterName(['suffix' => 'masschange'])} ??
new MembersList();
1097 'id' => $filters->selected
,
1098 'redirect_uri' => $this->routeparser
->urlFor('members')
1101 $fc = $this->fields_config
;
1102 $form_elements = $fc->getMassiveFormElements($this->members_fields
, $this->login
);
1105 $member = new Adherent($this->zdb
);
1106 $member->disableAllDeps()->enableDep('dynamics');
1109 $statuts = new Status($this->zdb
);
1111 $titles = new Titles($this->zdb
);
1114 $this->view
->render(
1116 'modals/mass_change_members.html.twig',
1118 'mode' => ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') ?
'ajax' : '',
1119 'page_title' => str_replace(
1122 _T('Mass change %count members')
1124 'form_url' => $this->routeparser
->urlFor('masschangeMembersReview'),
1125 'cancel_uri' => $this->routeparser
->urlFor('members'),
1127 'member' => $member,
1128 'fieldsets' => $form_elements['fieldsets'],
1129 'titles_list' => $titles->getList(),
1130 'statuts' => $statuts->getList(),
1131 'require_mass' => true
1138 * Massive changes validation page
1140 * @param Request $request PSR Request
1141 * @param Response $response PSR Response
1145 public function validateMassChange(Request
$request, Response
$response): Response
1147 $post = $request->getParsedBody();
1150 if (!isset($post['confirm'])) {
1151 $this->flash
->addMessage(
1153 _T("Mass changes has not been confirmed!")
1156 //we want only visibles fields
1157 $fc = $this->fields_config
;
1158 $form_elements = $fc->getMassiveFormElements($this->members_fields
, $this->login
);
1160 foreach ($form_elements['fieldsets'] as $form_element) {
1161 foreach ($form_element->elements
as $field) {
1163 isset($post['mass_' . $field->field_id
])
1164 && (isset($post[$field->field_id
]) ||
$field->type
=== FieldsConfig
::TYPE_BOOL
)
1166 $changes[$field->field_id
] = [
1167 'label' => $field->label
,
1168 'value' => $post[$field->field_id
] ??
0
1174 //handle dynamic fields
1175 $member = new Adherent($this->zdb
);
1180 $this->members_fields
,
1183 $dynamic_fields = $member->getDynamicFields()->getFields();
1184 foreach ($dynamic_fields as $field) {
1185 $mass_id = 'mass_info_field_' . $field->getId();
1186 $field_id = 'info_field_' . $field->getId() . '_1';
1188 isset($post[$mass_id])
1189 && (isset($post[$field_id]) ||
$field instanceof Boolean
)
1191 $changes[$field_id] = [
1192 'label' => $field->getName(),
1193 'value' => $post[$field_id] ??
0
1199 $filters = $this->session
->{$this->getFilterName(['suffix' => 'masschange'])};
1201 'id' => $filters->selected
,
1202 'redirect_uri' => $this->routeparser
->urlFor('members')
1206 $statuts = new Status($this->zdb
);
1208 $titles = new Titles($this->zdb
);
1211 $this->view
->render(
1213 'modals/mass_change_members.html.twig',
1215 'mode' => ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') ?
'ajax' : '',
1216 'page_title' => str_replace(
1219 _T('Review mass change %count members')
1221 'form_url' => $this->routeparser
->urlFor('massstoremembers'),
1222 'cancel_uri' => $this->routeparser
->urlFor('members'),
1224 'titles_list' => $titles->getList(),
1225 'statuts' => $statuts->getList(),
1226 'changes' => $changes
1233 * Do massive changes
1235 * @param Request $request PSR Request
1236 * @param Response $response PSR Response
1240 public function doMassChange(Request
$request, Response
$response): Response
1242 $post = $request->getParsedBody();
1243 $redirect_url = $post['redirect_uri'];
1244 $error_detected = [];
1246 $dynamic_fields = null;
1248 unset($post['redirect_uri']);
1249 if (!isset($post['confirm'])) {
1250 $error_detected[] = _T("Mass changes has not been confirmed!");
1252 unset($post['confirm']);
1256 $fc = $this->fields_config
;
1257 $form_elements = $fc->getMassiveFormElements($this->members_fields
, $this->login
);
1258 $disabled = $this->members_fields
;
1259 foreach (array_keys($post) as $key) {
1261 foreach ($form_elements['fieldsets'] as $fieldset) {
1262 if (isset($fieldset->elements
[$key])) {
1269 //try on dynamic fields
1270 if ($dynamic_fields === null) {
1271 //handle dynamic fields
1272 $member = new Adherent($this->zdb
);
1277 $this->members_fields
,
1280 $dynamic_fields = $member->getDynamicFields()->getFields();
1282 foreach ($dynamic_fields as $field) {
1283 $field_id = 'info_field_' . $field->getId() . '_1';
1284 if ($key == $field_id) {
1293 'Permission issue mass editing field ' . $key,
1298 unset($disabled[$key]);
1302 if (!count($post)) {
1303 $error_detected[] = _T("Nothing to do!");
1305 foreach ($ids as $id) {
1306 $is_manager = !$this->login
->isAdmin()
1307 && !$this->login
->isStaff()
1308 && $this->login
->isGroupManager();
1309 $member = new Adherent($this->zdb
);
1310 $member->disableAllDeps();
1312 $member->enableDep('groups');
1314 $member->load((int)$id);
1315 $member->setDependencies(
1317 $this->members_fields
,
1320 if (!$member->canEdit($this->login
)) {
1324 $valid = $member->check($post, [], $disabled);
1325 if ($valid === true) {
1326 $done = $member->store();
1328 $error_detected[] = _T("An error occurred while storing the member.");
1333 $error_detected = array_merge($error_detected, $valid);
1339 if ($mass == 0 && !count($error_detected)) {
1340 $error_detected[] = _T('Something went wront during mass edition!');
1342 $this->flash
->addMessage(
1347 _T('%count members has been changed successfully!')
1352 if (count($error_detected) > 0) {
1353 foreach ($error_detected as $error) {
1354 $this->flash
->addMessage(
1361 if (!($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest')) {
1364 ->withHeader('Location', $redirect_url);
1366 return $this->withJson(
1369 'success' => count($error_detected) === 0
1378 * @param Request $request PSR Request
1379 * @param Response $response PSR Response
1383 public function store(Request
$request, Response
$response): Response
1385 if (!$this->preferences
->pref_bool_selfsubscribe
&& !$this->login
->isLogged()) {
1388 ->withHeader('Location', $this->routeparser
->urlFor('slash'));
1391 $post = $request->getParsedBody();
1392 $member = new Adherent($this->zdb
);
1397 $this->members_fields
,
1401 $success_detected = [];
1402 $error_detected = [];
1404 if ($this->isSelfMembership() && !isset($post[Adherent
::PK
])) {
1405 //mark as self membership
1406 $member->setSelfMembership();
1409 $gaptcha = $this->session
->gaptcha
;
1410 if (!$gaptcha->check($post['gaptcha'])) {
1411 $error_detected[] = _T('Invalid captcha');
1416 if (isset($post['id_adh'])) {
1417 $member->load((int)$post['id_adh']);
1418 if (!$member->canEdit($this->login
)) {
1419 //redirection should have been done before. Just throw an Exception.
1420 throw new \
RuntimeException(
1424 'No right to store member #%id'
1429 if ($member->id
!= '') {
1430 $member->load($this->login
->id
);
1434 // flagging required fields
1435 $fc = $this->fields_config
;
1437 // password required if we create a new member but not from self subscription
1438 if ($member->id
!= '' ||
$this->isSelfMembership()) {
1439 $fc->setNotRequired('mdp_adh');
1443 $member->hasParent() && !isset($post['detach_parent'])
1444 ||
isset($post['parent_id']) && !empty($post['parent_id'])
1446 $parent_fields = $member->getParentFields();
1447 foreach ($parent_fields as $field) {
1448 if ($fc->isRequired($field)) {
1449 $fc->setNotRequired($field);
1454 // flagging required fields invisible to members
1455 if ($this->login
->isAdmin() ||
$this->login
->isStaff()) {
1456 $fc->setNotRequired('activite_adh');
1457 $fc->setNotRequired('id_statut');
1460 $form_elements = $fc->getFormElements(
1463 $this->isSelfMembership()
1465 $fieldsets = $form_elements['fieldsets'];
1466 $required = array();
1467 $disabled = array();
1469 foreach ($fieldsets as $category) {
1470 foreach ($category->elements
as $field) {
1471 if ($field->required
== true) {
1472 $required[$field->field_id
] = true;
1474 if ($field->disabled
== true) {
1475 $disabled[$field->field_id
] = true;
1476 } elseif (!isset($post[$field->field_id
])) {
1477 switch ($field->field_id
) {
1478 //unchecked booleans are not sent from form
1479 case 'bool_admin_adh':
1480 case 'bool_exempt_adh':
1481 case 'bool_display_info':
1482 $post[$field->field_id
] = 0;
1489 $real_requireds = array_diff(array_keys($required), array_keys($disabled));
1491 // send email to member
1492 if ($this->isSelfMembership() ||
isset($post['mail_confirm']) && $post['mail_confirm'] == '1') {
1493 $member->setSendmail(); //flag to send creation email
1497 $redirect_url = $this->routeparser
->urlFor('member', ['id' => $member->id
]);
1498 if (!count($real_requireds) ||
isset($post[array_shift($real_requireds)])) {
1500 $valid = $member->check($post, $required, $disabled);
1501 if ($valid !== true) {
1502 $error_detected = array_merge($error_detected, $valid);
1505 if (count($error_detected) == 0) {
1506 //all goes well, we can proceed
1509 if ($member->id
== '') {
1513 $store = $member->store();
1514 if ($store === true) {
1515 //member has been stored :)
1517 if ($this->isSelfMembership()) {
1518 $success_detected[] = _T("Your account has been created!");
1520 $this->preferences
->pref_mail_method
> GaletteMail
::METHOD_DISABLED
1521 && $member->getEmail() != ''
1523 $success_detected[] = _T("An email has been sent to you, check your inbox.");
1526 $success_detected[] = _T("New member has been successfully added.");
1529 $success_detected[] = _T("Member account has been modified.");
1532 if ($this->login
->isGroupManager()) {
1533 //add/remove user from groups
1534 $groups_adh = $post['groups_adh'] ??
null;
1535 $add_groups = Groups
::addMemberToGroups(
1540 if ($add_groups === false) {
1541 $error_detected[] = _T("An error occurred adding member to its groups.");
1544 if ($this->login
->isSuperAdmin() ||
$this->login
->isAdmin() ||
$this->login
->isStaff()) {
1545 //add/remove manager from groups
1546 $managed_groups_adh = $post['groups_managed_adh'] ??
null;
1547 $add_groups = Groups
::addMemberToGroups(
1549 $managed_groups_adh,
1552 $member->loadGroups();
1554 if ($add_groups === false) {
1555 $error_detected[] = _T("An error occurred adding member to its groups as manager.");
1559 //something went wrong :'(
1560 $error_detected[] = _T("An error occurred while storing the member.");
1564 if (count($error_detected) === 0) {
1566 if ($this->preferences
->pref_force_picture_ratio
== 1) {
1568 $cropping['ratio'] = isset($this->preferences
->pref_member_picture_ratio
) ?
$this->preferences
->pref_member_picture_ratio
: 'square_ratio';
1569 $cropping['focus'] = isset($post['crop_focus']) ?
$post['crop_focus'] : 'center';
1571 $files_res = $member->handleFiles($_FILES, $cropping);
1572 if (is_array($files_res)) {
1573 $error_detected = array_merge($error_detected, $files_res);
1576 if (isset($post['del_photo'])) {
1577 if (!$member->picture
->delete($member->id
)) {
1578 $error_detected[] = _T("Delete failed");
1579 $str_adh = $member->id
. ' (' . $member->sname
. ' ' . ')';
1581 'Unable to delete picture for member ' . $str_adh,
1588 if (count($error_detected) > 0) {
1589 foreach ($error_detected as $error) {
1590 if (strpos($error, '%member_url_') !== false) {
1591 preg_match('/%member_url_(\d+)/', $error, $matches);
1592 $url = $this->routeparser
->urlFor('member', ['id' => $matches[1]]);
1593 $error = str_replace(
1594 '%member_url_' . $matches[1],
1599 $this->flash
->addMessage(
1606 if (count($success_detected) > 0) {
1607 foreach ($success_detected as $success) {
1608 $this->flash
->addMessage(
1615 if (count($error_detected) === 0) {
1616 if ($this->isSelfMembership()) {
1617 $redirect_url = $this->routeparser
->urlFor('login');
1619 isset($post['redirect_on_create'])
1620 && $post['redirect_on_create'] > Adherent
::AFTER_ADD_DEFAULT
1622 switch ($post['redirect_on_create']) {
1623 case Adherent
::AFTER_ADD_TRANS
:
1624 $redirect_url = $this->routeparser
->urlFor('addTransaction');
1626 case Adherent
::AFTER_ADD_NEW
:
1627 $redirect_url = $this->routeparser
->urlFor('addMember');
1629 case Adherent
::AFTER_ADD_SHOW
:
1630 $redirect_url = $this->routeparser
->urlFor('member', ['id' => $member->id
]);
1632 case Adherent
::AFTER_ADD_LIST
:
1633 $redirect_url = $this->routeparser
->urlFor('members');
1635 case Adherent
::AFTER_ADD_HOME
:
1636 $redirect_url = $this->routeparser
->urlFor('slash');
1639 } elseif (!isset($post['id_adh']) && !$member->isDueFree()) {
1640 $redirect_url = $this->routeparser
->urlFor(
1643 ) . '?id_adh=' . $member->id
;
1645 $redirect_url = $this->routeparser
->urlFor('member', ['id' => $member->id
]);
1648 //store entity in session
1649 $this->session
->member
= $member;
1651 if ($this->isSelfMembership()) {
1652 $redirect_url = $this->routeparser
->urlFor('subscribe');
1655 $redirect_url = $this->routeparser
->urlFor(
1657 ['id' => $member->id
]
1660 $redirect_url = $this->routeparser
->urlFor((isset($post['addchild']) ?
'addMemberChild' : 'addMember'));
1668 ->withHeader('Location', $redirect_url);
1676 * Get redirection URI
1678 * @param array $args Route arguments
1682 public function redirectUri(array $args)
1684 return $this->routeparser
->urlFor('members');
1690 * @param array $args Route arguments
1694 public function formUri(array $args)
1696 return $this->routeparser
->urlFor(
1703 * Get confirmation removal page title
1705 * @param array $args Route arguments
1709 public function confirmRemoveTitle(array $args)
1711 if (isset($args['id_adh']) ||
isset($args['id'])) {
1712 //one member removal
1713 $id_adh = $args['id_adh'] ??
$args['id'];
1714 $adh = new Adherent($this->zdb
, (int)$id_adh);
1716 _T('Remove member %1$s'),
1720 //batch members removal
1721 $filters = $this->session
->{$this->getFilterName(['suffix' => 'delete'])};
1722 $this->session
->{$this->getFilterName(['suffix' => 'delete'])} = $filters;
1725 count($filters->selected
),
1726 _T('You are about to remove %count members.')
1734 * @param array $args Route arguments
1735 * @param array $post POST values
1739 protected function doDelete(array $args, array $post)
1741 if (isset($this->session
->{$this->getFilterName(['suffix' => 'delete'])})) {
1742 $filters = $this->session
->{$this->getFilterName(['suffix' => 'delete'])};
1744 $filters = new MembersList();
1746 $members = new Members($filters);
1748 if (!is_array($post['id'])) {
1749 $ids = (array)$post['id'];
1754 return $members->removeMembers($ids);
1760 * Set self memebrship flag
1762 * @return MembersController
1764 private function setSelfMembership(): MembersController
1766 $this->is_self_membership
= true;
1771 * Is self membership?
1775 private function isSelfMembership(): bool
1777 return $this->is_self_membership
;
1781 * Handle navigation links
1783 * @param int $id_adh Current member ID
1787 private function handleNavigationLinks(int $id_adh): array
1789 $navigate = array();
1791 if (isset($this->session
->{$this->getFilterName()})) {
1792 $filters = $this->session
->{$this->getFilterName()};
1794 $filters = new MembersList();
1796 //we must navigate between all members
1800 $this->login
->isAdmin()
1801 ||
$this->login
->isStaff()
1802 ||
$this->login
->isGroupManager()
1804 $m = new Members($filters);
1807 $fields = [Adherent
::PK
, 'nom_adh', 'prenom_adh'];
1808 if ($this->login
->isAdmin() ||
$this->login
->isStaff()) {
1809 $ids = $m->getMembersList(false, $fields);
1811 $ids = $m->getManagedMembersList(false, $fields);
1814 $ids = $ids->toArray();
1815 foreach ($ids as $k => $m) {
1816 if ($m['id_adh'] == $id_adh) {
1818 'cur' => $m['id_adh'],
1819 'count' => $filters->counter
,
1823 $navigate['prev'] = $ids[$k - 1]['id_adh'];
1825 if ($k < count($ids) - 1) {
1826 $navigate['next'] = $ids[$k +
1]['id_adh'];
1837 * Get filter name in session
1839 * @param array|null $args Route arguments
1843 public function getFilterName(array $args = null): string
1845 $filter_name = 'filter_members';
1847 if (isset($args['prefix'])) {
1848 $filter_name = $args['prefix'] . '_' . $filter_name;
1851 if (isset($args['suffix'])) {
1852 $filter_name .= '_' . $args['suffix'];
1855 return $filter_name;