diff --git a/config/Migrations/20211025100313_MailingLists.php b/config/Migrations/20211025100313_MailingLists.php index 6187ed8..cabc846 100644 --- a/config/Migrations/20211025100313_MailingLists.php +++ b/config/Migrations/20211025100313_MailingLists.php @@ -112,11 +112,43 @@ class MailingLists extends AbstractMigration 'null' => true, 'signed' => false, 'length' => 10, - ]); + ]) + ->addColumn('include_primary_email', 'boolean', [ + 'default' => 1, + 'null' => false, + 'comment' => 'Should the primary email address by included in the mailing list' + ]) + ->addForeignKey('mailing_list_id', 'mailing_lists', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->addForeignKey('individual_id', 'individuals', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']); $mailinglists_individuals->addIndex(['mailing_list_id', 'individual_id'], ['unique' => true]); $mailinglists_individuals->create(); + + + $mailinglists_metafields = $this->table('mailing_lists_meta_fields', [ + 'signed' => false, + 'collation' => 'utf8mb4_unicode_ci', + ]); + + $mailinglists_metafields + ->addColumn('mailing_list_id', 'integer', [ + 'default' => null, + 'null' => true, + 'signed' => false, + 'length' => 10, + ]) + ->addColumn('meta_field_id', 'integer', [ + 'default' => null, + 'null' => true, + 'signed' => false, + 'length' => 10, + ]) + ->addPrimaryKey(['mailing_list_id', 'meta_field_id']) + ->addForeignKey('mailing_list_id', 'mailing_lists', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE']) + ->addForeignKey('meta_field_id', 'meta_fields', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE']); + + $mailinglists_metafields->create(); } } diff --git a/libraries/default/meta_fields/cerebrate_individual_extended.json b/libraries/default/meta_fields/cerebrate_individual_extended.json new file mode 100644 index 0000000..b597152 --- /dev/null +++ b/libraries/default/meta_fields/cerebrate_individual_extended.json @@ -0,0 +1,16 @@ +{ + "name": "Cerebrate Individuals extended", + "namespace": "cerebrate", + "description": "Template to extend fields of individuals", + "version": 1, + "scope": "individual", + "uuid": "3bc374c8-3cdd-4900-823e-cc9100ad5179", + "source": "Cerebrate", + "metaFields": [ + { + "field": "alternate_email", + "type": "text", + "multiple": true + } + ] +} diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 64b5777..5a5436c 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -676,16 +676,7 @@ class CRUDComponent extends Component $this->Controller->set('quickFilter', empty($quickFilterFields) ? [] : $quickFilterFields); if (!empty($params['quickFilter']) && !empty($quickFilterFields)) { $this->Controller->set('quickFilterValue', $params['quickFilter']); - foreach ($quickFilterFields as $filterField) { - $likeCondition = false; - if (is_array($filterField)) { - $likeCondition = reset($filterField); - $filterFieldName = array_key_first($filterField); - $queryConditions[$filterFieldName . ' LIKE'] = '%' . $params['quickFilter'] .'%'; - } else { - $queryConditions[$filterField] = $params['quickFilter']; - } - } + $queryConditions = $this->genQuickFilterConditions($params, $query, $quickFilterFields); $query->where(['OR' => $queryConditions]); } else { $this->Controller->set('quickFilterValue', ''); @@ -693,6 +684,28 @@ class CRUDComponent extends Component return $query; } + public function genQuickFilterConditions(array $params, \Cake\ORM\Query $query, array $quickFilterFields): array + { + $queryConditions = []; + foreach ($quickFilterFields as $filterField) { + $likeCondition = false; + if (is_array($filterField)) { + reset($filterField); + $filterFieldName = array_key_first($filterField); + if (!empty($filterField[$filterFieldName])) { + $queryConditions[$filterFieldName . ' LIKE'] = '%' . $params['quickFilter'] . '%'; + } else { + $queryConditions[$filterField] = $params['quickFilter']; + } + } + $query->where(['OR' => $queryConditions]); + } else { + $queryConditions[$filterField] = $params['quickFilter']; + } + } + return $queryConditions; + } + protected function setFilters($params, \Cake\ORM\Query $query, array $options): \Cake\ORM\Query { $filteringLabel = !empty($params['filteringLabel']) ? $params['filteringLabel'] : ''; diff --git a/src/Controller/Component/ParamHandlerComponent.php b/src/Controller/Component/ParamHandlerComponent.php index f76a578..980541b 100644 --- a/src/Controller/Component/ParamHandlerComponent.php +++ b/src/Controller/Component/ParamHandlerComponent.php @@ -26,15 +26,15 @@ class ParamHandlerComponent extends Component $queryString = str_replace('.', '_', $filter); $queryString = str_replace(' ', '_', $queryString); if ($this->request->getQuery($queryString) !== null) { - $parsedParams[$filter] = $this->request->getQuery($queryString); + $parsedParams[$filter] = trim($this->request->getQuery($queryString)); continue; } if (($this->request->getQuery($filter)) !== null) { - $parsedParams[$filter] = $this->request->getQuery($filter); + $parsedParams[$filter] = trim($this->request->getQuery($filter)); continue; } if (($this->request->is('post') || $this->request->is('put')) && $this->request->getData($filter) !== null) { - $parsedParams[$filter] = $this->request->getData($filter); + $parsedParams[$filter] = trim($this->request->getData($filter)); } } return $parsedParams; diff --git a/src/Controller/MailingListsController.php b/src/Controller/MailingListsController.php index 326f56f..1cc60d1 100644 --- a/src/Controller/MailingListsController.php +++ b/src/Controller/MailingListsController.php @@ -6,13 +6,15 @@ use Cake\Utility\Inflector; use Cake\Utility\Hash; use Cake\Utility\Text; use \Cake\Database\Expression\QueryExpression; -use Cake\Error\Debugger; +use Cake\ORM\Query; +use Cake\ORM\Entity; +use Exception; class MailingListsController extends AppController { public $filterFields = ['MailingLists.uuid', 'MailingLists.name', 'description', 'releasability']; public $quickFilterFields = ['MailingLists.uuid', ['MailingLists.name' => true], ['description' => true], ['releasability' => true]]; - public $containFields = ['Users', 'Individuals']; + public $containFields = ['Users', 'Individuals', 'MetaFields']; public function index() { @@ -72,45 +74,151 @@ class MailingListsController extends AppController public function listIndividuals($mailinglist_id) { - $individuals = $this->MailingLists->get($mailinglist_id, [ - 'contain' => 'Individuals' - ])->individuals; - $params = $this->ParamHandler->harvestParams(['quickFilter']); - if (!empty($params['quickFilter'])) { - // foreach ($sharingGroup['sharing_group_orgs'] as $k => $org) { - // if (strpos($org['name'], $params['quickFilter']) === false) { - // unset($sharingGroup['sharing_group_orgs'][$k]); - // } - // } - // $sharingGroup['sharing_group_orgs'] = array_values($sharingGroup['sharing_group_orgs']); + $quickFilter = [ + 'uuid', + ['first_name' => true], + ['last_name' => true], + ]; + $quickFilterUI = array_merge($quickFilter, [ + ['Registered emails' => true], + ]); + $filters = ['uuid', 'first_name', 'last_name', 'quickFilter']; + $queryParams = $this->ParamHandler->harvestParams($filters); + $activeFilters = $queryParams['quickFilter'] ?? []; + + $mailingList = $this->MailingLists->find() + ->where(['MailingLists.id' => $mailinglist_id]) + ->contain('MetaFields') + ->first(); + + $matchingMetaFieldParentIDs = []; + // Collect individuals having a matching meta_field + foreach ($mailingList->meta_fields as $metaField) { + if ( + empty($queryParams['quickFilter']) || + ( + str_contains($metaField->field, 'email') && + str_contains($metaField->value, $queryParams['quickFilter']) + ) + ) { + $matchingMetaFieldParentIDs[$metaField->parent_id] = true; + } } + $matchingMetaFieldParentIDs = array_keys($matchingMetaFieldParentIDs); + $mailingList = $this->MailingLists->loadInto($mailingList, [ + 'Individuals' => function (Query $q) use ($queryParams, $quickFilter, $matchingMetaFieldParentIDs) { + $conditions = []; + if (!empty($queryParams)) { + $conditions = $this->CRUD->genQuickFilterConditions($queryParams, $q, $quickFilter); + } + if (!empty($matchingMetaFieldParentIDs)) { + $conditions[] = function (QueryExpression $exp) use ($matchingMetaFieldParentIDs) { + return $exp->in('Individuals.id', $matchingMetaFieldParentIDs); + }; + } + if (!empty($queryParams['quickFilter'])) { + $conditions[] = [ + 'MailingListsIndividuals.include_primary_email' => true, + 'Individuals.email LIKE' => "%{$queryParams['quickFilter']}%" + ]; + } + $q->where([ + 'OR' => $conditions + ]); + return $q; + } + ]); + $mailingList->injectRegisteredEmailsIntoIndividuals(); if ($this->ParamHandler->isRest()) { - return $this->RestResponse->viewData($individuals, 'json'); + return $this->RestResponse->viewData($mailingList->individuals, 'json'); } + $individuals = $this->CustomPagination->paginate($mailingList->individuals); $this->set('mailing_list_id', $mailinglist_id); + $this->set('quickFilter', $quickFilterUI); + $this->set('activeFilters', $activeFilters); + $this->set('quickFilterValue', $queryParams['quickFilter'] ?? ''); $this->set('individuals', $individuals); } public function addIndividual($mailinglist_id) { $mailingList = $this->MailingLists->get($mailinglist_id, [ - 'contain' => 'Individuals' + 'contain' => ['Individuals', 'MetaFields'] ]); - $conditions = []; + $linkedIndividualsIDs = Hash::extract($mailingList, 'individuals.{n}.id'); + $conditions = [ + 'id NOT IN' => $linkedIndividualsIDs + ]; $dropdownData = [ - 'individuals' => $this->MailingLists->Individuals->getTarget()->find('list', [ - 'sort' => ['name' => 'asc'], - 'conditions' => $conditions - ]) + 'individuals' => $this->MailingLists->Individuals->getTarget()->find() + ->order(['first_name' => 'asc']) + ->where($conditions) + ->all() + ->combine('id', 'full_name') + ->toArray() ]; if ($this->request->is('post') || $this->request->is('put')) { $memberIDs = $this->request->getData()['individuals']; + $chosen_emails = $this->request->getData()['chosen_emails']; + if (!empty($chosen_emails)) { + $chosen_emails = json_decode($chosen_emails, true); + $chosen_emails = !is_null($chosen_emails) ? $chosen_emails : []; + } else { + $chosen_emails = []; + } $members = $this->MailingLists->Individuals->getTarget()->find()->where([ 'id IN' => $memberIDs ])->all()->toArray(); - $success = (bool)$this->MailingLists->Individuals->link($mailingList, $members); + $memberToLink = []; + $memberToUpdate = []; + foreach ($members as $i => $member) { + $includePrimary = in_array('primary', $chosen_emails[$member->id]); + $chosen_emails[$member->id] = array_filter($chosen_emails[$member->id], function($entry) { + return $entry != 'primary'; + }); + $members[$i]->_joinData = new Entity(['include_primary_email' => $includePrimary]); + if (in_array($member->id, $linkedIndividualsIDs)) { // individual already in the list + // $memberToUpdate[] = $members[$i]; + } else { // new individual to add to the list + $memberToLink[] = $members[$i]; + } + } + + // save new individuals + if (!empty($memberToLink)) { + $success = (bool)$this->MailingLists->Individuals->link($mailingList, $memberToLink); + if ($success && !empty($chosen_emails[$member->id])) { // Include any remaining emails from the metaFields + $emailsFromMetaFields = $this->MailingLists->MetaFields->find()->where([ + 'id IN' => $chosen_emails[$member->id] + ])->all()->toArray(); + $success = (bool)$this->MailingLists->MetaFields->link($mailingList, $emailsFromMetaFields); + } + } + + // update existing individuals + if (!empty($memberToUpdate)) { + $memberToUpdateIDs = Hash::extract($memberToUpdate, '{n}.id'); + $metaFieldsToRemove = array_filter($mailingList->meta_fields, function($metaField) use ($memberToUpdateIDs) { + return in_array($metaField->parent_id, $memberToUpdateIDs); + }); + + // Trying to update `include_primary`... + // $success = (bool)$this->MailingLists->Individuals->unlink($mailingList, $memberToUpdate, ['atomic' => false]); + // $success = $success && (bool)$this->MailingLists->Individuals->link($mailingList, $memberToUpdate); + + // Remove and add relevant meta fields + // $success = (bool)$this->MailingLists->MetaFields->unlink($mailingList, $metaFieldsToRemove, ['atomic' => false]); + // if ($success && !empty($chosen_emails[$member->id])) { // Include any remaining emails from the metaFields + // $emailsFromMetaFields = $this->MailingLists->MetaFields->find()->where([ + // 'id IN' => $chosen_emails[$member->id] + // ])->all()->toArray(); + // $success = (bool)$this->MailingLists->MetaFields->link($mailingList, $emailsFromMetaFields); + // } + } + + if ($success) { - $message = __n('%s individual added to the mailing list.', '%s Individuals added to the mailing list.', count($members), count($members)); + $message = __n('{0} individual added to the mailing list.', '{0} Individuals added to the mailing list.', count($members), count($members)); $mailingList = $this->MailingLists->get($mailingList->id); } else { $message = __n('The individual could not be added to the mailing list.', 'The Individuals could not be added to the mailing list.', count($members)); @@ -129,7 +237,7 @@ class MailingListsController extends AppController public function removeIndividual($mailinglist_id, $individual_id=null) { $mailingList = $this->MailingLists->get($mailinglist_id, [ - 'contain' => 'Individuals' + 'contain' => ['Individuals', 'MetaFields'] ]); $individual = []; if (!is_null($individual_id)) { @@ -138,13 +246,20 @@ class MailingListsController extends AppController if ($this->request->is('post') || $this->request->is('delete')) { $success = false; if (!is_null($individual_id)) { - $individual = $this->MailingLists->Individuals->get($individual_id); - $success = (bool)$this->MailingLists->Individuals->unlink($mailingList, [$individual]); + $individualToRemove = $this->MailingLists->Individuals->get($individual_id); + $metaFieldsToRemove = $this->MailingLists->MetaFields->find()->where([ + 'id IN' => Hash::extract($mailingList, 'meta_fields.{n}.id'), + 'parent_id' => $individual_id, + ])->all()->toArray(); + $success = (bool)$this->MailingLists->Individuals->unlink($mailingList, [$individualToRemove]); + if ($success && !empty($metaFieldsToRemove)) { + $success = (bool)$this->MailingLists->MetaFields->unlink($mailingList, $metaFieldsToRemove); + } if ($success) { - $message = __('Individual removed from the mailing list.'); + $message = __('{0} removed from the mailing list.', $individualToRemove->full_name); $mailingList = $this->MailingLists->get($mailingList->id); } else { - $message = __n('Individual could not be removed from the mailing list.'); + $message = __n('{0} could not be removed from the mailing list.', $individual->full_name); } $this->CRUD->setResponseForController('remove_individuals', $success, $message, $mailingList, $mailingList->getErrors()); } else { @@ -155,22 +270,27 @@ class MailingListsController extends AppController if (empty($params['ids'])) { throw new NotFoundException(__('Invalid {0}.', Inflector::singularize($this->MailingLists->Individuals->getAlias()))); } - $individuals = $this->MailingLists->Individuals->find()->where([ + $individualsToRemove = $this->MailingLists->Individuals->find()->where([ 'id IN' => array_map('intval', $params['ids']) ])->all()->toArray(); + $metaFieldsToRemove = $this->MailingLists->MetaFields->find()->where([ + 'id IN' => Hash::extract($mailingList, 'meta_fields.{n}.id'), + 'parent_id IN' => Hash::extract($mailingList, 'meta_fields.{n}.id') + ])->all()->toArray(); + dd($metaFieldsToRemove); $unlinkSuccesses = 0; - foreach ($individuals as $individual) { - $success = (bool)$this->MailingLists->Individuals->unlink($mailingList, [$individual]); + foreach ($individualsToRemove as $individualToRemove) { + $success = (bool)$this->MailingLists->Individuals->unlink($mailingList, [$individualToRemove]); $results[] = $success; if ($success) { $unlinkSuccesses++; } } $mailingList = $this->MailingLists->get($mailingList->id); - $success = $unlinkSuccesses == count($individuals); + $success = $unlinkSuccesses == count($individualsToRemove); $message = __( '{0} {1} have been removed.', - $unlinkSuccesses == count($individuals) ? __('All') : sprintf('%s / %s', $unlinkSuccesses, count($individuals)), + $unlinkSuccesses == count($individualsToRemove) ? __('All') : sprintf('%s / %s', $unlinkSuccesses, count($individualsToRemove)), Inflector::singularize($this->MailingLists->Individuals->getAlias()) ); $this->CRUD->setResponseForController('remove_individuals', $success, $message, $mailingList, []); diff --git a/src/Model/Entity/MailingList.php b/src/Model/Entity/MailingList.php index 96e1507..02ff121 100644 --- a/src/Model/Entity/MailingList.php +++ b/src/Model/Entity/MailingList.php @@ -3,7 +3,6 @@ namespace App\Model\Entity; use App\Model\Entity\AppModel; -use Cake\ORM\Entity; class MailingList extends AppModel { @@ -18,4 +17,35 @@ class MailingList extends AppModel 'uuid' => true, 'user_id' => true, ]; + + private $metaFieldsByParentId = []; + + public function injectRegisteredEmailsIntoIndividuals() + { + if (empty($this->individuals)) { + return; + } + if (!empty($this->meta_fields)) { + foreach ($this->meta_fields as $meta_field) { + $this->metaFieldsByParentId[$meta_field->parent_id][] = $meta_field; + } + } + foreach ($this->individuals as $i => $individual) { + $this->individuals[$i]->mailinglist_emails = $this->collectEmailsForMailingList($individual); + } + } + + protected function collectEmailsForMailingList($individual) + { + $emails = []; + if (!empty($individual['_joinData']) && !empty($individual['_joinData']['include_primary_email'])) { + $emails[] = $individual->email; + } + if (!empty($this->metaFieldsByParentId[$individual->id])) { + foreach ($this->metaFieldsByParentId[$individual->id] as $metaField) { + $emails[] = $metaField->value; + } + } + return $emails; + } } diff --git a/src/Model/Table/IndividualsTable.php b/src/Model/Table/IndividualsTable.php index 4c2d3a7..3afb8cf 100644 --- a/src/Model/Table/IndividualsTable.php +++ b/src/Model/Table/IndividualsTable.php @@ -36,6 +36,15 @@ class IndividualsTable extends AppTable $this->belongsToMany('Organisations', [ 'through' => 'Alignments', ]); + + $this->hasMany('MetaFields') + ->setForeignKey('parent_id') + ->setBindingKey('id') + ->setConditions([ + 'MetaFields.scope' => 'individual' + ]) + ->setDependent(true); + $this->belongsToMany('MailingLists'); $this->setDisplayField('email'); } diff --git a/src/Model/Table/MailingListsTable.php b/src/Model/Table/MailingListsTable.php index 4296583..bb8f641 100644 --- a/src/Model/Table/MailingListsTable.php +++ b/src/Model/Table/MailingListsTable.php @@ -18,19 +18,12 @@ class MailingListsTable extends AppTable $this->belongsTo( 'Users' ); - // $this->belongsToMany( - // 'Individuals', - // [ - // 'className' => 'Individuals', - // 'foreignKey' => 'individual_id', - // 'joinTable' => 'sgo', - // 'targetForeignKey' => 'organisation_id' - // ] - // ); $this->belongsToMany('Individuals', [ 'joinTable' => 'mailing_lists_individuals', ]); + // Change to HasMany? + $this->belongsToMany('MetaFields'); $this->setDisplayField('name'); } diff --git a/src/Model/Table/MetaFieldsTable.php b/src/Model/Table/MetaFieldsTable.php index ba4cc66..fce8930 100644 --- a/src/Model/Table/MetaFieldsTable.php +++ b/src/Model/Table/MetaFieldsTable.php @@ -15,6 +15,12 @@ class MetaFieldsTable extends AppTable $this->setDisplayField('field'); $this->belongsTo('MetaTemplates'); $this->belongsTo('MetaTemplateFields'); + $this->belongsTo('Individuals') + ->setForeignKey('parent_id') + ->setBindingKey('id') + ->setConditions([ + 'scope' => 'individual' + ]); } public function validationDefault(Validator $validator): Validator diff --git a/templates/MailingLists/add_individual.php b/templates/MailingLists/add_individual.php index 9bbe641..7ac22a2 100644 --- a/templates/MailingLists/add_individual.php +++ b/templates/MailingLists/add_individual.php @@ -1,24 +1,121 @@ element('genericElements/Form/genericForm', [ - 'data' => [ - 'title' => __('Add members to mailing list {0} [{1}]', h($mailingList->name), h($mailingList->id)), - // 'description' => __('Mailing list are email distribution lists containing individuals.'), - 'model' => 'MailingLists', - 'fields' => [ - [ - 'field' => 'individuals', - 'type' => 'dropdown', - 'multiple' => true, - 'select2' => true, - 'label' => __('Members'), - 'class' => 'select2-input', - 'options' => $dropdownData['individuals'] - ], +echo $this->element('genericElements/Form/genericForm', [ + 'data' => [ + 'title' => __('Add members to `{0}` [{1}]', h($mailingList->name), h($mailingList->id)), + 'model' => 'MailingLists', + 'fields' => [ + [ + 'field' => 'individuals', + 'type' => 'dropdown', + 'multiple' => true, + 'select2' => true, + 'label' => __('Members'), + 'class' => 'select2-input', + 'options' => $dropdownData['individuals'] ], - 'submit' => [ - 'action' => $this->request->getParam('action') + [ + 'field' => 'chosen_emails', + 'type' => 'text', + 'templates' => ['inputContainer' => '