From eb2291562b4d077a67b83937437009144721a7e5 Mon Sep 17 00:00:00 2001 From: Jeremy Skinner Date: Fri, 2 Mar 2018 14:12:31 +0000 Subject: [PATCH 1/8] Add Communications profile type, opt-out field and mailchimp integration module. --- ...cts.contact_tab.crm_communications_tab.yml | 35 +++++++++++++++++++ ...e.crm_communications.field_crm_opt_out.yml | 22 ++++++++++++ ...ield.storage.profile.field_crm_opt_out.yml | 17 +++++++++ .../profile.type.crm_communications.yml | 16 +++++++++ .../contacts_mailchimp.info.yml | 8 +++++ .../contacts_mailchimp.module | 29 +++++++++++++++ 6 files changed, 127 insertions(+) create mode 100644 config/install/contacts.contact_tab.crm_communications_tab.yml create mode 100644 config/install/field.field.profile.crm_communications.field_crm_opt_out.yml create mode 100644 config/install/field.storage.profile.field_crm_opt_out.yml create mode 100644 config/install/profile.type.crm_communications.yml create mode 100644 modules/contacts_mailchimp/contacts_mailchimp.info.yml create mode 100644 modules/contacts_mailchimp/contacts_mailchimp.module diff --git a/config/install/contacts.contact_tab.crm_communications_tab.yml b/config/install/contacts.contact_tab.crm_communications_tab.yml new file mode 100644 index 0000000..397341b --- /dev/null +++ b/config/install/contacts.contact_tab.crm_communications_tab.yml @@ -0,0 +1,35 @@ +uuid: 5800747c-04df-4555-902f-b233214033a7 +langcode: en +status: true +dependencies: + config: + - profile.type.crm_communications + module: + - profile +id: crm_communications_tab +label: Communications +path: communications +layout: contacts_tab_content.stacked +weight: null +relationships: + profile_crm_communications: + id: 'typed_data_entity_relationship:entity:user:profile_crm_communications' + name: profile_crm_communications + source: user +blocks: + contacts_entity_profile_crm_communications: + id: 'contacts_entity:profile-crm_communications' + label: 'Communications Profile block' + provider: contacts + label_display: '0' + mode: view + create: true + operation: contacts_dashboard + view_mode: contacts_dashboard + edit_link: content + edit_id: edit + region: top + context_mapping: + entity: profile_crm_communications + user: user + name: contacts_entity_profile_crm_communications diff --git a/config/install/field.field.profile.crm_communications.field_crm_opt_out.yml b/config/install/field.field.profile.crm_communications.field_crm_opt_out.yml new file mode 100644 index 0000000..b0a0422 --- /dev/null +++ b/config/install/field.field.profile.crm_communications.field_crm_opt_out.yml @@ -0,0 +1,22 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.profile.crm_opt_out + - profile.type.crm_communications +id: profile.crm_communications.crm_opt_out +field_name: crm_opt_out +entity_type: profile +bundle: crm_communications +label: 'Opt Out' +description: '' +required: false +translatable: false +default_value: + - + value: 0 +default_value_callback: '' +settings: + on_label: 'Yes' + off_label: 'No' +field_type: boolean \ No newline at end of file diff --git a/config/install/field.storage.profile.field_crm_opt_out.yml b/config/install/field.storage.profile.field_crm_opt_out.yml new file mode 100644 index 0000000..bfcc5f1 --- /dev/null +++ b/config/install/field.storage.profile.field_crm_opt_out.yml @@ -0,0 +1,17 @@ +langcode: en +status: true +dependencies: + module: + - profile +id: profile.crm_opt_out +field_name: crm_opt_out +entity_type: profile +type: boolean +settings: { } +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/config/install/profile.type.crm_communications.yml b/config/install/profile.type.crm_communications.yml new file mode 100644 index 0000000..23dcfd4 --- /dev/null +++ b/config/install/profile.type.crm_communications.yml @@ -0,0 +1,16 @@ +langcode: en +status: true +dependencies: { } +id: crm_communications +label: Communications +registration: false +multiple: false +roles: + authenticated: '0' + administrator: '0' + crm_indiv: '0' + crm_org: '0' + student: '0' +weight: 0 +use_revisions: false +description: '' diff --git a/modules/contacts_mailchimp/contacts_mailchimp.info.yml b/modules/contacts_mailchimp/contacts_mailchimp.info.yml new file mode 100644 index 0000000..f27bf63 --- /dev/null +++ b/modules/contacts_mailchimp/contacts_mailchimp.info.yml @@ -0,0 +1,8 @@ +name: Contacts MailChimp Integration +description: "MailChimp integration for the Contacts module" +type: module +core: 8.x + +dependencies: + - contacts + - mailchimp_lists (>=8.x-1.6) \ No newline at end of file diff --git a/modules/contacts_mailchimp/contacts_mailchimp.module b/modules/contacts_mailchimp/contacts_mailchimp.module new file mode 100644 index 0000000..5c351ff --- /dev/null +++ b/modules/contacts_mailchimp/contacts_mailchimp.module @@ -0,0 +1,29 @@ +getEntityTypeId() == 'profile' && $entity->bundle() == 'crm_communications' && $entity->hasField($field_name)) { + $opt_out = $entity->get($field_name)->value; + + if(isset($entity->original)) { + $original = $entity->original->get($field_name)->value; + } + else { + $original = null; + } + + //TODO: Investigate making this asynchronous + if(($original === 0 || $original == NULL) && $opt_out === 1 && $entity->uid->entity->hasField('mail')) { + $email = $entity->uid->entity->mail->value; + $lists = mailchimp_get_lists_for_email($email); + foreach($lists as $list) { + mailchimp_unsubscribe($list->id, $email); + } + } + } +} \ No newline at end of file From be52a8f9b395505d09ff2392e445c13ea2d765d2 Mon Sep 17 00:00:00 2001 From: Jeremy Skinner Date: Mon, 5 Mar 2018 13:20:11 +0000 Subject: [PATCH 2/8] Set opt out when user unsubscribes from mailchimp --- .../contacts_mailchimp.module | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/modules/contacts_mailchimp/contacts_mailchimp.module b/modules/contacts_mailchimp/contacts_mailchimp.module index 5c351ff..52dd8bd 100644 --- a/modules/contacts_mailchimp/contacts_mailchimp.module +++ b/modules/contacts_mailchimp/contacts_mailchimp.module @@ -26,4 +26,26 @@ function contacts_mailchimp_entity_presave(Drupal\Core\Entity\EntityInterface $e } } } +} + +/** + * Implements hook_mailchimp_process_webhook(). + */ +function contacts_mailchimp_mailchimp_process_webhook($type, $data) { + // If user unsubscribes from mailchimp, treat it as a full opt-out + if($type == 'unsubscribe') { + $ids = \Drupal::entityQuery('profile') + ->condition('uid.entity.mail', $data['email']) + ->execute(); + + $profiles = \Drupal\profile\Entity\Profile::loadMultiple($ids); + + /* @var $profile \Drupal\profile\Entity\Profile */ + foreach($profiles as $profile) { + if($profile->hasField('crm_opt_out')) { + $profile->set('crm_opt_out', 1); + $profile->save(); + } + } + } } \ No newline at end of file From a0b8cdaab38fa5486797da38eaab7254e1321e47 Mon Sep 17 00:00:00 2001 From: Jeremy Skinner Date: Mon, 5 Mar 2018 14:53:48 +0000 Subject: [PATCH 3/8] Apply patch for broken hook_entity_field_storage_info --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index db6adf2..62301ec 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,8 @@ "patches": { "drupal/core": { "#2865059: Multiple machine name elements from same source": "https://www.drupal.org/files/issues/2865059-13-allow_multiple_machine_name_per_source.patch", - "#736066: ajax.js insert command sometimes wraps content in a div": "https://www.drupal.org/files/issues/736066-296.patch" + "#736066: ajax.js insert command sometimes wraps content in a div": "https://www.drupal.org/files/issues/736066-296.patch", + "#2584729: Hook entity_field_storage_info is broken": "https://www.drupal.org/files/issues/core-entity-sql-content-entity-storage-2583111-1_0.patch" }, "drupal/ctools": { "#2667652: Option to expose filters in block on views block display": "https://www.drupal.org/files/issues/ctools-option_to_expose-2667652-3.patch", From c36bdd1b3a0c543537a5b186ab133dbb332b1556 Mon Sep 17 00:00:00 2001 From: Jeremy Skinner Date: Mon, 5 Mar 2018 15:03:57 +0000 Subject: [PATCH 4/8] Create the opt-out field through code instead of config and ensure the mailchimp opt out query filters on correct bundle --- ...e.crm_communications.field_crm_opt_out.yml | 22 --------- ...ield.storage.profile.field_crm_opt_out.yml | 17 ------- contacts.module | 49 +++++++++++++++++++ .../contacts_mailchimp.module | 1 + 4 files changed, 50 insertions(+), 39 deletions(-) delete mode 100644 config/install/field.field.profile.crm_communications.field_crm_opt_out.yml delete mode 100644 config/install/field.storage.profile.field_crm_opt_out.yml diff --git a/config/install/field.field.profile.crm_communications.field_crm_opt_out.yml b/config/install/field.field.profile.crm_communications.field_crm_opt_out.yml deleted file mode 100644 index b0a0422..0000000 --- a/config/install/field.field.profile.crm_communications.field_crm_opt_out.yml +++ /dev/null @@ -1,22 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.profile.crm_opt_out - - profile.type.crm_communications -id: profile.crm_communications.crm_opt_out -field_name: crm_opt_out -entity_type: profile -bundle: crm_communications -label: 'Opt Out' -description: '' -required: false -translatable: false -default_value: - - - value: 0 -default_value_callback: '' -settings: - on_label: 'Yes' - off_label: 'No' -field_type: boolean \ No newline at end of file diff --git a/config/install/field.storage.profile.field_crm_opt_out.yml b/config/install/field.storage.profile.field_crm_opt_out.yml deleted file mode 100644 index bfcc5f1..0000000 --- a/config/install/field.storage.profile.field_crm_opt_out.yml +++ /dev/null @@ -1,17 +0,0 @@ -langcode: en -status: true -dependencies: - module: - - profile -id: profile.crm_opt_out -field_name: crm_opt_out -entity_type: profile -type: boolean -settings: { } -module: core -locked: false -cardinality: 1 -translatable: true -indexes: { } -persist_with_no_fields: false -custom_storage: false diff --git a/contacts.module b/contacts.module index 88f5a7f..adeca34 100644 --- a/contacts.module +++ b/contacts.module @@ -723,3 +723,52 @@ function contacts_preprocess_block(&$variables, $hook) { function contacts_form_user_register_form_alter(&$form, FormStateInterface $form_state, $form_id) { $form['account']['roles']['#default_value'][] = 'crm_indiv'; } + +/** + * Implements hook_entity_bundle_field_info + */ +function contacts_entity_bundle_field_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { + // Create the crm_opt_out field. This is a critical field and by creating it through code, it prevents site admins from deleting the field. + if($entity_type->id() == 'profile' && $bundle == 'crm_communications') { + + $fields['crm_opt_out'] = \Drupal\Core\Field\BaseFieldDefinition::create('boolean') + ->setName('crm_opt_out') + ->setLabel('Opt Out') + ->setTargetEntityTypeId('profile') + ->setTargetBundle('crm_communications') + ->setDefaultValue(0) + ->setDisplayConfigurable("form", TRUE) + ->setDisplayConfigurable("view", TRUE) + ->setDisplayOptions("form", [ + 'type'=>'boolean_checkbox', + 'region' => 'content', + 'settings' => ['display_label', TRUE], + 'weight' => 0 + ]) + ->setDisplayOptions("view", [ + 'type'=>'boolean_checkbox', + 'region' => 'content', + 'settings' => ['display_label', TRUE], + 'weight' => 0 + ]) + ->setSettings(['on_label'=>'Yes', 'off_label' => 'No']); + + return $fields; + } +} + +/** + * Implements hook_entity_field_storage_info(). + */ +function contacts_entity_field_storage_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type) { + // Ensure backing storage exists for crm_communications + if($entity_type->id() == 'profile') { + $storage['crm_opt_out'] = \Drupal\Core\Field\BaseFieldDefinition::create('boolean') + ->setName('crm_opt_out') + ->setCardinality(1) + ->setTargetBundle('crm_communications') + ->setTranslatable(TRUE) + ->setTargetEntityTypeId('profile'); + return $storage; + } +} \ No newline at end of file diff --git a/modules/contacts_mailchimp/contacts_mailchimp.module b/modules/contacts_mailchimp/contacts_mailchimp.module index 52dd8bd..bfc395f 100644 --- a/modules/contacts_mailchimp/contacts_mailchimp.module +++ b/modules/contacts_mailchimp/contacts_mailchimp.module @@ -36,6 +36,7 @@ function contacts_mailchimp_mailchimp_process_webhook($type, $data) { if($type == 'unsubscribe') { $ids = \Drupal::entityQuery('profile') ->condition('uid.entity.mail', $data['email']) + ->condition('type', 'crm_communications') ->execute(); $profiles = \Drupal\profile\Entity\Profile::loadMultiple($ids); From b967ccbd2395f174d11d02b82f6e1ab903aea298 Mon Sep 17 00:00:00 2001 From: Jeremy Skinner Date: Mon, 5 Mar 2018 15:45:50 +0000 Subject: [PATCH 5/8] Reformatting and code standards fixes --- contacts.module | 31 ++--- .../contacts_mailchimp.info.yml | 2 +- .../contacts_mailchimp.module | 112 ++++++++++-------- 3 files changed, 77 insertions(+), 68 deletions(-) diff --git a/contacts.module b/contacts.module index adeca34..646fe15 100644 --- a/contacts.module +++ b/contacts.module @@ -5,6 +5,8 @@ * Module related hook implementations for the contacts module. */ +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Template\Attribute; use Drupal\Core\Url; use Drupal\Core\Link; @@ -725,13 +727,12 @@ function contacts_form_user_register_form_alter(&$form, FormStateInterface $form } /** - * Implements hook_entity_bundle_field_info + * Implements hook_entity_bundle_field_info(). */ -function contacts_entity_bundle_field_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { +function contacts_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { // Create the crm_opt_out field. This is a critical field and by creating it through code, it prevents site admins from deleting the field. - if($entity_type->id() == 'profile' && $bundle == 'crm_communications') { - - $fields['crm_opt_out'] = \Drupal\Core\Field\BaseFieldDefinition::create('boolean') + if ($entity_type->id() == 'profile' && $bundle == 'crm_communications') { + $fields['crm_opt_out'] = BaseFieldDefinition::create('boolean') ->setName('crm_opt_out') ->setLabel('Opt Out') ->setTargetEntityTypeId('profile') @@ -740,18 +741,18 @@ function contacts_entity_bundle_field_info(\Drupal\Core\Entity\EntityTypeInterfa ->setDisplayConfigurable("form", TRUE) ->setDisplayConfigurable("view", TRUE) ->setDisplayOptions("form", [ - 'type'=>'boolean_checkbox', + 'type' => 'boolean_checkbox', 'region' => 'content', 'settings' => ['display_label', TRUE], - 'weight' => 0 + 'weight' => 0, ]) ->setDisplayOptions("view", [ - 'type'=>'boolean_checkbox', + 'type' => 'boolean_checkbox', 'region' => 'content', 'settings' => ['display_label', TRUE], - 'weight' => 0 + 'weight' => 0, ]) - ->setSettings(['on_label'=>'Yes', 'off_label' => 'No']); + ->setSettings(['on_label' => 'Yes', 'off_label' => 'No']); return $fields; } @@ -760,10 +761,10 @@ function contacts_entity_bundle_field_info(\Drupal\Core\Entity\EntityTypeInterfa /** * Implements hook_entity_field_storage_info(). */ -function contacts_entity_field_storage_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type) { - // Ensure backing storage exists for crm_communications - if($entity_type->id() == 'profile') { - $storage['crm_opt_out'] = \Drupal\Core\Field\BaseFieldDefinition::create('boolean') +function contacts_entity_field_storage_info(EntityTypeInterface $entity_type) { + // Ensure backing storage exists for crm_communications. + if ($entity_type->id() == 'profile') { + $storage['crm_opt_out'] = BaseFieldDefinition::create('boolean') ->setName('crm_opt_out') ->setCardinality(1) ->setTargetBundle('crm_communications') @@ -771,4 +772,4 @@ function contacts_entity_field_storage_info(\Drupal\Core\Entity\EntityTypeInterf ->setTargetEntityTypeId('profile'); return $storage; } -} \ No newline at end of file +} diff --git a/modules/contacts_mailchimp/contacts_mailchimp.info.yml b/modules/contacts_mailchimp/contacts_mailchimp.info.yml index f27bf63..ed53c97 100644 --- a/modules/contacts_mailchimp/contacts_mailchimp.info.yml +++ b/modules/contacts_mailchimp/contacts_mailchimp.info.yml @@ -5,4 +5,4 @@ core: 8.x dependencies: - contacts - - mailchimp_lists (>=8.x-1.6) \ No newline at end of file + - mailchimp_lists (>=8.x-1.6) diff --git a/modules/contacts_mailchimp/contacts_mailchimp.module b/modules/contacts_mailchimp/contacts_mailchimp.module index bfc395f..f79d599 100644 --- a/modules/contacts_mailchimp/contacts_mailchimp.module +++ b/modules/contacts_mailchimp/contacts_mailchimp.module @@ -1,52 +1,60 @@ -getEntityTypeId() == 'profile' && $entity->bundle() == 'crm_communications' && $entity->hasField($field_name)) { - $opt_out = $entity->get($field_name)->value; - - if(isset($entity->original)) { - $original = $entity->original->get($field_name)->value; - } - else { - $original = null; - } - - //TODO: Investigate making this asynchronous - if(($original === 0 || $original == NULL) && $opt_out === 1 && $entity->uid->entity->hasField('mail')) { - $email = $entity->uid->entity->mail->value; - $lists = mailchimp_get_lists_for_email($email); - foreach($lists as $list) { - mailchimp_unsubscribe($list->id, $email); - } - } - } -} - -/** - * Implements hook_mailchimp_process_webhook(). - */ -function contacts_mailchimp_mailchimp_process_webhook($type, $data) { - // If user unsubscribes from mailchimp, treat it as a full opt-out - if($type == 'unsubscribe') { - $ids = \Drupal::entityQuery('profile') - ->condition('uid.entity.mail', $data['email']) - ->condition('type', 'crm_communications') - ->execute(); - - $profiles = \Drupal\profile\Entity\Profile::loadMultiple($ids); - - /* @var $profile \Drupal\profile\Entity\Profile */ - foreach($profiles as $profile) { - if($profile->hasField('crm_opt_out')) { - $profile->set('crm_opt_out', 1); - $profile->save(); - } - } - } -} \ No newline at end of file +getEntityTypeId() == 'profile' && $entity->bundle() == 'crm_communications' && $entity->hasField($field_name)) { + $opt_out = $entity->get($field_name)->value; + + if (isset($entity->original)) { + $original = $entity->original->get($field_name)->value; + } + else { + $original = NULL; + } + + // TODO: Investigate making this asynchronous. + if (($original === 0 || $original == NULL) && $opt_out === 1 && $entity->uid->entity->hasField('mail')) { + $email = $entity->uid->entity->mail->value; + $lists = mailchimp_get_lists_for_email($email); + foreach ($lists as $list) { + mailchimp_unsubscribe($list->id, $email); + } + } + } +} + +/** + * Implements hook_mailchimp_process_webhook(). + */ +function contacts_mailchimp_mailchimp_process_webhook($type, $data) { + // If user unsubscribes from mailchimp, treat it as a full opt-out. + if ($type == 'unsubscribe') { + $ids = \Drupal::entityQuery('profile') + ->condition('uid.entity.mail', $data['email']) + ->condition('type', 'crm_communications') + ->execute(); + + $profiles = Profile::loadMultiple($ids); + + /* @var $profile \Drupal\profile\Entity\Profile */ + foreach ($profiles as $profile) { + if ($profile->hasField('crm_opt_out')) { + $profile->set('crm_opt_out', 1); + $profile->save(); + } + } + } +} From cb78fead877ecbd86ef4b2153e68f2c82ceb435f Mon Sep 17 00:00:00 2001 From: Jeremy Skinner Date: Mon, 5 Mar 2018 16:29:58 +0000 Subject: [PATCH 6/8] Fix view display type for opt out field --- contacts.module | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contacts.module b/contacts.module index 646fe15..bd07e67 100644 --- a/contacts.module +++ b/contacts.module @@ -747,7 +747,7 @@ function contacts_entity_bundle_field_info(EntityTypeInterface $entity_type, $bu 'weight' => 0, ]) ->setDisplayOptions("view", [ - 'type' => 'boolean_checkbox', + 'type' => 'boolean', 'region' => 'content', 'settings' => ['display_label', TRUE], 'weight' => 0, From aff0cc4812ad943770e0652fe9b94db2bc4a9f59 Mon Sep 17 00:00:00 2001 From: Jeremy Skinner Date: Mon, 5 Mar 2018 16:37:16 +0000 Subject: [PATCH 7/8] Coder review; Line length --- contacts.module | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contacts.module b/contacts.module index bd07e67..56bc015 100644 --- a/contacts.module +++ b/contacts.module @@ -730,7 +730,7 @@ function contacts_form_user_register_form_alter(&$form, FormStateInterface $form * Implements hook_entity_bundle_field_info(). */ function contacts_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { - // Create the crm_opt_out field. This is a critical field and by creating it through code, it prevents site admins from deleting the field. + // Create the crm_opt_out field. if ($entity_type->id() == 'profile' && $bundle == 'crm_communications') { $fields['crm_opt_out'] = BaseFieldDefinition::create('boolean') ->setName('crm_opt_out') From 7a54ce7534ae11f977e19a69badd56c3d30c3bfa Mon Sep 17 00:00:00 2001 From: Jeremy Skinner Date: Tue, 6 Mar 2018 09:20:29 +0000 Subject: [PATCH 8/8] Update tab preferences based on code review --- config/install/contacts.contact_tab.crm_communications_tab.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/install/contacts.contact_tab.crm_communications_tab.yml b/config/install/contacts.contact_tab.crm_communications_tab.yml index 397341b..8c8f5f0 100644 --- a/config/install/contacts.contact_tab.crm_communications_tab.yml +++ b/config/install/contacts.contact_tab.crm_communications_tab.yml @@ -1,4 +1,3 @@ -uuid: 5800747c-04df-4555-902f-b233214033a7 langcode: en status: true dependencies: @@ -19,7 +18,7 @@ relationships: blocks: contacts_entity_profile_crm_communications: id: 'contacts_entity:profile-crm_communications' - label: 'Communications Profile block' + label: 'Communication Preferences' provider: contacts label_display: '0' mode: view