effective_mailchimp 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/admin/mailchimp_categories_controller.rb +15 -0
  3. data/app/controllers/admin/mailchimp_controller.rb +25 -0
  4. data/app/controllers/admin/mailchimp_interests_controller.rb +15 -0
  5. data/app/controllers/admin/mailchimp_list_members_controller.rb +15 -0
  6. data/app/controllers/admin/mailchimp_lists_controller.rb +0 -10
  7. data/app/datatables/admin/effective_mailchimp_categories_datatable.rb +32 -0
  8. data/app/datatables/admin/effective_mailchimp_interests_datatable.rb +47 -0
  9. data/app/datatables/admin/effective_mailchimp_list_members_datatable.rb +39 -0
  10. data/app/datatables/admin/effective_mailchimp_lists_datatable.rb +5 -3
  11. data/app/helpers/effective_mailchimp_helper.rb +10 -0
  12. data/app/models/concerns/effective_mailchimp_user.rb +5 -1
  13. data/app/models/effective/mailchimp_api.rb +43 -15
  14. data/app/models/effective/mailchimp_category.rb +73 -0
  15. data/app/models/effective/mailchimp_interest.rb +92 -0
  16. data/app/models/effective/mailchimp_list.rb +35 -37
  17. data/app/models/effective/mailchimp_list_member.rb +42 -1
  18. data/app/views/admin/mailchimp/_sync.html.haml +15 -0
  19. data/app/views/admin/mailchimp/index.html.haml +80 -0
  20. data/app/views/admin/mailchimp_interests/_form.html.haml +10 -0
  21. data/app/views/admin/mailchimp_lists/_form.html.haml +1 -0
  22. data/app/views/admin/mailchimp_user/_form.html.haml +10 -4
  23. data/app/views/effective/mailchimp_user/_fields.html.haml +36 -9
  24. data/config/locales/effective_mailchimp.en.yml +12 -0
  25. data/config/routes.rb +7 -9
  26. data/db/migrate/101_create_effective_mailchimp.rb +37 -0
  27. data/lib/effective_mailchimp/version.rb +1 -1
  28. data/lib/effective_mailchimp.rb +6 -2
  29. data/lib/tasks/effective_mailchimp_tasks.rake +0 -16
  30. metadata +13 -2
  31. data/app/views/admin/mailchimp_lists/index.html.haml +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3502aae94186923697f39763042a537f6adf5426396e576b2ce17821cc892c70
4
- data.tar.gz: dac06ef19961ed0e8eab2a548f2d7a810c50fbfccdd7219d0d198247f69353b2
3
+ metadata.gz: d3b3306349bc71e4dcd895a1be96411b94e82c76155972d5ee73ad302487fa5d
4
+ data.tar.gz: 590439bc94cc48d34586b14eeba77476e5fc715ee32118a973c640fdc25e6def
5
5
  SHA512:
6
- metadata.gz: 73df6b018f47d66c0d5ba1fa27a02124c65c9544e6322e37bad2240cc21e2f73baae853bb68bb3e3c6ac8ef691ba6d627958988f2521884cf21244526b15da6f
7
- data.tar.gz: 021eca9c105c49ee522df6c0b60f3d64e608ec9a8adc3e0930a018e6ff4ef56c8965796efdee1e0c9e1b3d988bb8b4f7578e022ffcc665a49a14c087683dfbe4
6
+ metadata.gz: 59eab329289651cd579828198bafbad8093aa94e452866f6dc2a51b5a787a7b077e57d644e5caa7cf91b868d12db82bdb28284680e7a3608f216c7bbbd4e73ad
7
+ data.tar.gz: fc0bbcadfffd0caae215be540ad150bf588264ae7ee013b4a4cbe8d0ea7064fe603d4f9bd4abfe70c6ef341d931d5e1ec1bed60f15735f5603f777e54c827d1a
@@ -0,0 +1,15 @@
1
+ module Admin
2
+ class MailchimpCategoriesController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_mailchimp) }
5
+
6
+ include Effective::CrudController
7
+
8
+ private
9
+
10
+ def permitted_params
11
+ params.require(:effective_mailchimp_category).permit!
12
+ end
13
+
14
+ end
15
+ end
@@ -3,6 +3,31 @@ module Admin
3
3
  before_action(:authenticate_user!) if defined?(Devise)
4
4
  before_action { EffectiveResources.authorize!(self, :admin, :effective_mailchimp) }
5
5
 
6
+ include Effective::CrudController
7
+
8
+ page_title 'Mailchimp'
9
+
10
+ # /admin/mailchimp
11
+ def index
12
+ end
13
+
14
+ # Sync All
15
+ def mailchimp_sync
16
+ EffectiveResources.authorize!(self, :admin, :mailchimp_sync)
17
+
18
+ api = EffectiveMailchimp.api
19
+ merge_fields = current_user.class.new().mailchimp_merge_fields
20
+
21
+ Effective::MailchimpList.sync!(api: api, merge_fields: merge_fields)
22
+ Effective::MailchimpCategory.sync!(api: api)
23
+ Effective::MailchimpInterest.sync!(api: api)
24
+
25
+ flash[:success] = "Successfully synced mailchimp data"
26
+
27
+ redirect_back(fallback_location: effective_mailchimp.admin_mailchimp_path)
28
+ end
29
+
30
+ # Sync one user
6
31
  def mailchimp_sync_user
7
32
  resource = current_user.class.find(params[:id])
8
33
 
@@ -0,0 +1,15 @@
1
+ module Admin
2
+ class MailchimpInterestsController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_mailchimp) }
5
+
6
+ include Effective::CrudController
7
+
8
+ private
9
+
10
+ def permitted_params
11
+ params.require(:effective_mailchimp_interest).permit!
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Admin
2
+ class MailchimpListMembersController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_mailchimp) }
5
+
6
+ include Effective::CrudController
7
+
8
+ private
9
+
10
+ def permitted_params
11
+ params.require(:effective_mailchimp_list_member).permit!
12
+ end
13
+
14
+ end
15
+ end
@@ -5,16 +5,6 @@ module Admin
5
5
 
6
6
  include Effective::CrudController
7
7
 
8
- def mailchimp_sync
9
- EffectiveResources.authorize!(self, :mailchimp_sync, Effective::MailchimpList)
10
-
11
- Effective::MailchimpList.sync!
12
-
13
- flash[:success] = "Successfully synced mailchimp lists"
14
-
15
- redirect_back(fallback_location: effective_mailchimp.admin_mailchimp_lists_path)
16
- end
17
-
18
8
  private
19
9
 
20
10
  def permitted_params
@@ -0,0 +1,32 @@
1
+ module Admin
2
+ class EffectiveMailchimpCategoriesDatatable < Effective::Datatable
3
+
4
+ datatable do
5
+ length :all
6
+
7
+ order :name
8
+
9
+ col :updated_at, visible: false
10
+ col :created_at, visible: false
11
+ col :id, visible: false
12
+
13
+ col :mailchimp_list
14
+
15
+ col :list_id, visible: false
16
+ col :mailchimp_id, visible: false
17
+
18
+ col :name
19
+ col :list_name, visible: false
20
+
21
+ col :display_type
22
+
23
+ col :mailchimp_interests
24
+
25
+ actions_col
26
+ end
27
+
28
+ collection do
29
+ Effective::MailchimpCategory.deep.all
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,47 @@
1
+ module Admin
2
+ class EffectiveMailchimpInterestsDatatable < Effective::Datatable
3
+ filters do
4
+ scope :all
5
+ scope :subscribable
6
+ end
7
+
8
+ datatable do
9
+ length :all
10
+ order :display_order
11
+
12
+ col :updated_at, visible: false
13
+ col :created_at, visible: false
14
+ col :id, visible: false
15
+
16
+ col :mailchimp_list
17
+ col :mailchimp_category
18
+
19
+ col :list_id, visible: false
20
+ col :category_id, visible: false
21
+ col :mailchimp_id, visible: false
22
+
23
+ col :name
24
+ col :can_subscribe
25
+ col :force_subscribe
26
+
27
+ col :list_name, visible: false
28
+ col :category_name, visible: false
29
+ col :display_order, visible: false
30
+
31
+ col :subscriber_count
32
+
33
+ actions_col
34
+ end
35
+
36
+ collection do
37
+ scope = Effective::MailchimpInterest.deep
38
+
39
+ if attributes[:mailchimp_category_id].present?
40
+ scope = scope.sorted
41
+ end
42
+
43
+ scope
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,39 @@
1
+ module Admin
2
+ class EffectiveMailchimpListMembersDatatable < Effective::Datatable
3
+ filters do
4
+ scope :all
5
+ scope :subscribed
6
+ end
7
+
8
+ datatable do
9
+ order :updated_at
10
+
11
+ col :updated_at, visible: false
12
+ col :created_at, visible: false
13
+ col :id, visible: false
14
+
15
+ col :mailchimp_list
16
+ col :user
17
+
18
+ col :mailchimp_id, visible: false
19
+ col :web_id, visible: false
20
+
21
+ col :email_address
22
+ col :full_name
23
+ col :subscribed
24
+
25
+ col :interests, visible: false
26
+ col :mailchimp_interests, search: { collection: Effective::MailchimpInterest.order(:name), fuzzy: true }
27
+
28
+ col :last_synced_at
29
+
30
+ actions_col do |member|
31
+ dropdown_link_to('Edit', "/admin/users/#{member.user.to_param}/edit#tab-mailchimp", 'data-turbolinks': false)
32
+ end
33
+ end
34
+
35
+ collection do
36
+ Effective::MailchimpListMember.deep.all
37
+ end
38
+ end
39
+ end
@@ -6,7 +6,8 @@ module Admin
6
6
  end
7
7
 
8
8
  datatable do
9
- order :updated_at
9
+ length :all
10
+ order :name
10
11
 
11
12
  col :updated_at, visible: false
12
13
  col :created_at, visible: false
@@ -21,13 +22,14 @@ module Admin
21
22
 
22
23
  col :url, label: 'Mailchimp' do |ml|
23
24
  [
24
- link_to('View Campaign', ml.url, target: '_blank'),
25
25
  link_to('View Members', ml.members_url, target: '_blank'),
26
26
  link_to('View Merge Fields', ml.merge_fields_url, target: '_blank')
27
27
  ].join('<br>').html_safe
28
28
  end
29
29
 
30
- col :merge_fields
30
+ col :merge_fields, visible: false do |ml|
31
+ ml.merge_fields.join(', ')
32
+ end
31
33
 
32
34
  actions_col
33
35
  end
@@ -1,2 +1,12 @@
1
1
  module EffectiveMailchimpHelper
2
+
3
+ def mailchimp_list_member_interests_collection(mailchimp_interests)
4
+ interests = mailchimp_interests.select { |interest| interest.can_subscribe? || interest.force_subscribe? }
5
+
6
+ interests.map do |interest|
7
+ label = (interest.force_subscribe? ? (interest.to_s + ' ' + content_tag(:small, 'required', class: 'text-hint')) : interest.to_s).html_safe
8
+ [label, interest.mailchimp_id, disabled: (interest.force_subscribe || !interest.can_subscribe?)]
9
+ end
10
+ end
11
+
2
12
  end
@@ -35,7 +35,7 @@ module EffectiveMailchimpUser
35
35
 
36
36
  # The user updated the form
37
37
  after_commit(if: -> { mailchimp_member_update_required? }) do
38
- EffectiveMailchimpUpdateJob.perform_later(self)
38
+ EffectiveMailchimpUpdateJob.perform_later(self) # This calls user.mailchimp_update! on the background
39
39
  end
40
40
  end
41
41
 
@@ -140,6 +140,10 @@ module EffectiveMailchimpUser
140
140
  mailchimp_list_members.select(&:subscribed?).map(&:mailchimp_list)
141
141
  end
142
142
 
143
+ def mailchimp_subscribed_interests
144
+ mailchimp_list_members.select(&:subscribed?).flat_map(&:interests)
145
+ end
146
+
143
147
  def mailchimp_list_member(mailchimp_list:)
144
148
  raise('expected a MailchimpList') unless mailchimp_list.kind_of?(Effective::MailchimpList)
145
149
  mailchimp_list_members.find { |mlm| mlm.mailchimp_list_id == mailchimp_list.id }
@@ -29,6 +29,22 @@ module Effective
29
29
  "https://#{server}.admin.mailchimp.com"
30
30
  end
31
31
 
32
+ def audience_url
33
+ "https://#{server}.admin.mailchimp.com/audience/"
34
+ end
35
+
36
+ def groups_url
37
+ "https://#{server}.admin.mailchimp.com/lists/dashboard/groups/"
38
+ end
39
+
40
+ def contacts_url
41
+ "https://#{server}.admin.mailchimp.com/audience/contacts"
42
+ end
43
+
44
+ def campaigns_url
45
+ "https://#{server}.admin.mailchimp.com/campaigns/"
46
+ end
47
+
32
48
  def public_url
33
49
  "https://mailchimp.com"
34
50
  end
@@ -52,6 +68,20 @@ module Effective
52
68
  client.lists.get_list(id.try(:mailchimp_id) || id)
53
69
  end
54
70
 
71
+ def categories(list_id)
72
+ Rails.logger.info "[effective_mailchimp] Index Interest Categories..." if debug?
73
+
74
+ response = client.lists.get_list_interest_categories(list_id.try(:mailchimp_id) || list_id)
75
+ Array(response['categories']) - [nil, '', {}]
76
+ end
77
+
78
+ def interests(list_id, category_id)
79
+ Rails.logger.info "[effective_mailchimp] Index Interest Category Interests..." if debug?
80
+
81
+ response = client.lists.list_interest_category_interests(list_id, category_id)
82
+ Array(response['interests']) - [nil, '', {}]
83
+ end
84
+
55
85
  def list_member(id, email)
56
86
  raise('expected an email') unless email.to_s.include?('@')
57
87
 
@@ -63,7 +93,7 @@ module Effective
63
93
  end
64
94
 
65
95
  def list_merge_fields(id)
66
- response = client.lists.get_list_merge_fields(id, count: 100)
96
+ response = client.lists.get_list_merge_fields(id.try(:mailchimp_id) || id, count: 100)
67
97
  Array(response['merge_fields']) - [nil, '', {}]
68
98
  end
69
99
 
@@ -73,7 +103,7 @@ module Effective
73
103
  payload = { name: name.to_s.titleize, tag: name.to_s, type: type }
74
104
 
75
105
  begin
76
- client.lists.add_list_merge_field(id, payload)
106
+ client.lists.add_list_merge_field(id.try(:mailchimp_id) || id, payload)
77
107
  rescue MailchimpMarketing::ApiError => e
78
108
  false
79
109
  end
@@ -85,31 +115,29 @@ module Effective
85
115
  existing = list_member(member.mailchimp_list, member.user.email)
86
116
  return existing if existing.present?
87
117
 
88
- merge_fields = member.user.mailchimp_merge_fields
89
- raise('expected user mailchimp_merge_fields to be a Hash') unless merge_fields.kind_of?(Hash)
90
-
91
- payload = {
92
- email_address: member.user.email,
93
- status: (member.subscribed ? 'subscribed' : 'unsubscribed'),
94
- merge_fields: merge_fields.delete_if { |k, v| v.blank? }
95
- }
96
-
118
+ payload = list_member_payload(member)
97
119
  client.lists.add_list_member(member.mailchimp_list.mailchimp_id, payload)
98
120
  end
99
121
 
100
122
  def list_member_update(member)
101
123
  raise('expected an Effective::MailchimpListMember') unless member.kind_of?(Effective::MailchimpListMember)
102
124
 
125
+ payload = list_member_payload(member)
126
+ client.lists.update_list_member(member.mailchimp_list.mailchimp_id, member.email, payload)
127
+ end
128
+
129
+ def list_member_payload(member)
130
+ raise('expected an Effective::MailchimpListMember') unless member.kind_of?(Effective::MailchimpListMember)
131
+
103
132
  merge_fields = member.user.mailchimp_merge_fields
104
133
  raise('expected user mailchimp_merge_fields to be a Hash') unless merge_fields.kind_of?(Hash)
105
134
 
106
135
  payload = {
107
136
  email_address: member.user.email,
108
137
  status: (member.subscribed ? 'subscribed' : 'unsubscribed'),
109
- merge_fields: merge_fields.delete_if { |k, v| v.blank? }
110
- }
111
-
112
- client.lists.update_list_member(member.mailchimp_list.mailchimp_id, member.email, payload)
138
+ merge_fields: merge_fields.delete_if { |k, v| v.blank? },
139
+ interests: member.interests_hash.presence
140
+ }.compact
113
141
  end
114
142
 
115
143
  end
@@ -0,0 +1,73 @@
1
+ module Effective
2
+ class MailchimpCategory < ActiveRecord::Base
3
+ self.table_name = (EffectiveMailchimp.mailchimp_categories_table_name || :mailchimp_categories).to_s
4
+
5
+ belongs_to :mailchimp_list
6
+ has_many :mailchimp_interests
7
+
8
+ effective_resource do
9
+ mailchimp_id :string # ID of this Mailchimp InterestCategory
10
+ list_id :string # Mailchimp list ID
11
+
12
+ name :string # Title
13
+ list_name :string
14
+
15
+ display_type :string # Type
16
+
17
+ timestamps
18
+ end
19
+
20
+ validates :mailchimp_id, presence: true
21
+ validates :list_id, presence: true
22
+ validates :name, presence: true
23
+
24
+ scope :deep, -> { all }
25
+ scope :sorted, -> { order(:name) }
26
+ scope :subscribable, -> { all }
27
+
28
+ # Creates or builds all the Lists
29
+ def self.sync!(api: EffectiveMailchimp.api)
30
+ # For every mailchimp_list, get all the categories
31
+ mailchimp_lists = Effective::MailchimpList.all
32
+
33
+ mailchimp_lists.each do |mailchimp_list|
34
+ # All the Groups from Mailchimp
35
+ categories = api.categories(mailchimp_list.mailchimp_id)
36
+
37
+ # Get all our existing Effective::MailchimpCategory records
38
+ mailchimp_categories = where(mailchimp_list: mailchimp_list)
39
+
40
+ # Find or create Effective::MailchimpGroups based on existing groups
41
+ categories.each do |category|
42
+ mailchimp_id = category['id']
43
+ mailchimp_category = mailchimp_categories.find { |mc| mc.mailchimp_id == mailchimp_id } || new()
44
+
45
+ mailchimp_category.assign_attributes(
46
+ mailchimp_list: mailchimp_list,
47
+
48
+ mailchimp_id: mailchimp_id,
49
+ list_id: category['list_id'],
50
+ list_name: mailchimp_list.name,
51
+ name: (category['title'] || category['name']),
52
+ display_type: category['type']
53
+ )
54
+
55
+ mailchimp_category.save!
56
+ end
57
+
58
+ # Destroy any Effective::MailchimpGroups resources if they no longer returned by groups
59
+ mailchimp_categories.each do |mailchimp_category|
60
+ category = categories.find { |category| category['id'] == mailchimp_category.mailchimp_id }
61
+ mailchimp_category.destroy! unless category.present?
62
+ end
63
+ end
64
+
65
+ true
66
+ end
67
+
68
+ def to_s
69
+ name.presence || model_name.human
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,92 @@
1
+ module Effective
2
+ class MailchimpInterest < ActiveRecord::Base
3
+ self.table_name = (EffectiveMailchimp.mailchimp_interests_table_name || :mailchimp_interests).to_s
4
+
5
+ belongs_to :mailchimp_list
6
+ belongs_to :mailchimp_category
7
+
8
+ effective_resource do
9
+ mailchimp_id :string # ID of this interest on Mailchimp
10
+
11
+ list_id :string # Mailchimp list ID
12
+ category_id :string # Interest Category
13
+
14
+ name :string
15
+ list_name :string
16
+ category_name :string
17
+
18
+ subscriber_count :integer
19
+ display_order :integer
20
+
21
+ can_subscribe :boolean
22
+ force_subscribe :boolean
23
+
24
+ timestamps
25
+ end
26
+
27
+ validates :mailchimp_id, presence: true
28
+ validates :list_id, presence: true
29
+ validates :category_id, presence: true
30
+ validates :name, presence: true
31
+
32
+ scope :deep, -> { all }
33
+ scope :sorted, -> { order(:display_order) }
34
+ scope :subscribable, -> { where(can_subscribe: true) }
35
+
36
+ # Creates or builds all the Lists
37
+ def self.sync!(api: EffectiveMailchimp.api)
38
+ # For every mailchimp_list, get all the interests
39
+ mailchimp_lists = Effective::MailchimpList.deep.all
40
+
41
+ mailchimp_lists.each do |mailchimp_list|
42
+ mailchimp_list.mailchimp_categories.each do |mailchimp_category|
43
+
44
+ # All the Interests from Mailchimp
45
+ interests = api.interests(mailchimp_list.mailchimp_id, mailchimp_category.mailchimp_id)
46
+
47
+ # Get all our existing Effective::MailchimpGroup records
48
+ mailchimp_interests = where(mailchimp_list: mailchimp_list, mailchimp_category: mailchimp_category)
49
+
50
+ # Find or create Effective::MailchimpInterests based on existing lists and categories
51
+ interests.each do |interest|
52
+ mailchimp_id = interest['id']
53
+ mailchimp_interest = mailchimp_interests.find { |mi| mi.mailchimp_id == mailchimp_id } || new()
54
+
55
+ mailchimp_interest.assign_attributes(
56
+ mailchimp_list: mailchimp_list,
57
+ mailchimp_category: mailchimp_category,
58
+
59
+ mailchimp_id: mailchimp_id,
60
+
61
+ list_id: interest['list_id'],
62
+ list_name: mailchimp_list.name,
63
+
64
+ category_id: interest['category_id'],
65
+ category_name: mailchimp_category.name,
66
+
67
+ name: interest['name'],
68
+ display_order: interest['display_order'],
69
+ subscriber_count: interest['subscriber_count']
70
+ )
71
+
72
+ mailchimp_interest.assign_attributes(can_subscribe: true) if mailchimp_interest.new_record?
73
+ mailchimp_interest.save!
74
+ end
75
+
76
+ # Destroy any Effective::MailchimpGroups resources if they no longer returned by interests
77
+ mailchimp_interests.each do |mailchimp_interest|
78
+ interest = interests.find { |interest| interest['id'] == mailchimp_interest.mailchimp_id }
79
+ mailchimp_interest.destroy! unless interest.present?
80
+ end
81
+ end
82
+ end
83
+
84
+ true
85
+ end
86
+
87
+ def to_s
88
+ name.presence || model_name.human
89
+ end
90
+
91
+ end
92
+ end
@@ -3,6 +3,8 @@ module Effective
3
3
  self.table_name = (EffectiveMailchimp.mailchimp_lists_table_name || :mailchimp_lists).to_s
4
4
 
5
5
  has_many :mailchimp_list_members, dependent: :delete_all
6
+ has_many :mailchimp_categories, dependent: :delete_all
7
+ has_many :mailchimp_interests, dependent: :delete_all
6
8
 
7
9
  effective_resource do
8
10
  mailchimp_id :string
@@ -16,18 +18,14 @@ module Effective
16
18
  timestamps
17
19
  end
18
20
 
19
- def to_s
20
- name.presence || model_name.human
21
- end
22
-
23
- scope :deep, -> { all }
21
+ scope :deep, -> { includes(:mailchimp_categories, :mailchimp_interests) }
24
22
  scope :sorted, -> { order(:name) }
25
23
  scope :subscribable, -> { where(can_subscribe: true) }
26
24
 
27
25
  # Creates or builds all the Lists
28
- def self.sync!
26
+ def self.sync!(api: EffectiveMailchimp.api, merge_fields: nil)
29
27
  # All the Lists from Mailchimp
30
- lists = EffectiveMailchimp.api.lists
28
+ lists = api.lists()
31
29
 
32
30
  # Get all our existing Effective::MailchimpList records
33
31
  mailchimp_lists = all()
@@ -35,11 +33,17 @@ module Effective
35
33
  # Find or create Effective::Mailchimp based on existing lists
36
34
  lists.each do |list|
37
35
  mailchimp_id = list['id']
38
- web_id = list['web_id']
39
- name = list['name']
40
36
 
41
37
  mailchimp_list = mailchimp_lists.find { |ml| ml.mailchimp_id == mailchimp_id } || new()
42
- mailchimp_list.assign_attributes(mailchimp_id: mailchimp_id, web_id: web_id, name: name)
38
+ mailchimp_list.assign_attributes(
39
+ mailchimp_id: mailchimp_id,
40
+
41
+ web_id: list['web_id'],
42
+ name: list['name'],
43
+ updated_at: Time.zone.now
44
+ )
45
+
46
+ mailchimp_list.assign_attributes(can_subscribe: true) if mailchimp_list.new_record?
43
47
  mailchimp_list.save!
44
48
  end
45
49
 
@@ -49,27 +53,37 @@ module Effective
49
53
  mailchimp_list.destroy! unless list.present?
50
54
  end
51
55
 
52
- true
53
- end
54
-
55
- # This creates our local merge fields ON Mailchimp
56
- def create_mailchimp_merge_fields!(merge_fields)
57
- raise('expected a Hash of merge fields') unless merge_fields.kind_of?(Hash)
58
-
59
- merge_fields.keys.each do |name|
60
- EffectiveMailchimp.api.add_merge_field(mailchimp_id, name: name)
56
+ # Sync merge fields
57
+ if merge_fields.present?
58
+ merge_field_keys = merge_fields.keys.map(&:to_s)
59
+
60
+ mailchimp_lists.reject(&:destroyed?).each do |mailchimp_list|
61
+ existing = api.list_merge_fields(mailchimp_list).map { |hash| hash['tag'] }
62
+ (merge_field_keys - existing).each do |name|
63
+ puts "Adding merge field #{name} to #{mailchimp_list}"
64
+ api.add_merge_field(mailchimp_list, name: name)
65
+ end
66
+ end
61
67
  end
62
68
 
63
69
  true
64
70
  end
65
71
 
72
+ def to_s
73
+ name.presence || model_name.human
74
+ end
75
+
66
76
  def merge_fields
67
77
  return [] unless mailchimp_id
68
78
  EffectiveMailchimp.api.list_merge_fields(mailchimp_id).map { |hash| hash['tag'] }.sort
69
79
  end
70
80
 
71
- def url
72
- EffectiveMailchimp.api.admin_url + "/campaigns/#f_list:#{web_id}"
81
+ def grouped?
82
+ mailchimp_categories.present? && mailchimp_categories.any? { |category| category.mailchimp_interests.present? }
83
+ end
84
+
85
+ def ungrouped?
86
+ !grouped?
73
87
  end
74
88
 
75
89
  def members_url
@@ -80,21 +94,5 @@ module Effective
80
94
  EffectiveMailchimp.api.admin_url + "/lists/settings/merge-tags?id=#{web_id}"
81
95
  end
82
96
 
83
- def can_subscribe!
84
- update!(can_subscribe: true)
85
- end
86
-
87
- def cannot_subscribe!
88
- update!(can_subscribe: false)
89
- end
90
-
91
- def force_subscribe!
92
- update!(force_subscribe: true)
93
- end
94
-
95
- def unforce_subscribe!
96
- update!(force_subscribe: false)
97
- end
98
-
99
97
  end
100
98
  end
@@ -14,6 +14,8 @@ module Effective
14
14
  email_address :string
15
15
  full_name :string
16
16
 
17
+ interests :text # Array of mailchimp_interest mailchimp_ids
18
+
17
19
  # We set this on our side to update mailchimp and subscribe the user
18
20
  subscribed :boolean
19
21
 
@@ -26,10 +28,17 @@ module Effective
26
28
  timestamps
27
29
  end
28
30
 
31
+ if EffectiveResources.serialize_with_coder?
32
+ serialize :interests, type: Array, coder: YAML
33
+ else
34
+ serialize :interests, Array
35
+ end
36
+
29
37
  validates :mailchimp_list_id, uniqueness: { scope: [:user_type, :user_id] }
30
38
 
31
39
  scope :deep, -> { includes(:mailchimp_list, :user) }
32
40
  scope :sorted, -> { order(:id) }
41
+ scope :subscribed, -> { where(subscribed: true) }
33
42
 
34
43
  def to_s
35
44
  mailchimp_list&.to_s || model_name.human
@@ -39,6 +48,37 @@ module Effective
39
48
  email_address.presence || user.email
40
49
  end
41
50
 
51
+ # Array of MailchimpInterest mailchimp_ids
52
+ def interests
53
+ Array(self[:interests]) - [nil, '']
54
+ end
55
+
56
+ # Array of MailchimpInterests
57
+ def mailchimp_interests
58
+ all_mailchimp_interests.select { |interest| interests.include?(interest.mailchimp_id) }
59
+ end
60
+
61
+ # We use this to add the force_subscribed interests
62
+ def build_interests(mailchimp_interest)
63
+ mailchimp_ids = Array(mailchimp_interest).map { |interest| interest.try(:mailchimp_id) || interest }
64
+ raise('expected an array of MailchimpInterests or mailchimp_ids') unless mailchimp_ids.all? { |id| id.kind_of?(String) && id.length > 1 }
65
+
66
+ assign_attributes(interests: interests | mailchimp_ids)
67
+ end
68
+
69
+ # {"25a38426c9" => false, "9b826db370" => true }
70
+ def interests_hash
71
+ all_mailchimp_interests.each_with_object({}) do |interest, hash|
72
+ hash[interest.mailchimp_id] = interests.include?(interest.mailchimp_id)
73
+ end
74
+ end
75
+
76
+ # From the mailchimp list
77
+ def all_mailchimp_interests
78
+ return [] if mailchimp_list.blank?
79
+ mailchimp_list.mailchimp_categories.flat_map(&:mailchimp_interests)
80
+ end
81
+
42
82
  def assign_mailchimp_attributes(atts)
43
83
  assign_attributes(
44
84
  mailchimp_id: atts['id'],
@@ -46,7 +86,8 @@ module Effective
46
86
  email_address: atts['email_address'],
47
87
  full_name: atts['full_name'],
48
88
  subscribed: (atts['status'] == 'subscribed'),
49
- last_synced_at: Time.zone.now
89
+ last_synced_at: Time.zone.now,
90
+ interests: Hash(atts['interests']).select { |_, subscribed| subscribed == true }.keys
50
91
  )
51
92
  end
52
93
 
@@ -0,0 +1,15 @@
1
+ - if EffectiveResources.authorized?(self, :admin, :mailchimp_sync)
2
+ %p= link_to 'Sync changes from Mailchimp', effective_mailchimp.mailchimp_sync_admin_mailchimp_index_path, 'data-method': :post, class: 'btn btn-primary'
3
+
4
+ %p.text-muted
5
+ %small
6
+ - mailchimp_last_synced_at = Effective::MailchimpList.maximum(:updated_at)
7
+
8
+ last synced with
9
+ = link_to 'Mailchimp', EffectiveMailchimp.api.admin_url, target: '_blank'
10
+
11
+ - if mailchimp_last_synced_at.present?
12
+ = time_ago_in_words(mailchimp_last_synced_at)
13
+ ago.
14
+ - else
15
+ never.
@@ -0,0 +1,80 @@
1
+ %h1.effective-admin-heading= @page_title
2
+
3
+ = card('Mailchimp Settings') do
4
+ - if EffectiveMailchimp.api_blank?
5
+ .alert.alert-danger You are not connected to Mailchimp. Please set your API key.
6
+
7
+ - if EffectiveMailchimp.api_present?
8
+ %p
9
+ = succeed('.') do
10
+ Please configure your
11
+ = link_to 'audiences', EffectiveMailchimp.api.audience_url, target: '_blank'
12
+ and
13
+ = link_to 'groups', EffectiveMailchimp.api.groups_url, target: '_blank'
14
+ on the Mailchimp website then sync the changes so they can be updated below
15
+
16
+ %p
17
+ You can also visit the
18
+ = link_to('contacts', EffectiveMailchimp.api.contacts_url, target: '_blank')
19
+ and
20
+ = link_to('campaigns', EffectiveMailchimp.api.campaigns_url, target: '_blank')
21
+ at anytime.
22
+
23
+ %p.text-muted
24
+ %small
25
+ To change the names or display order of items below, please visit the Mailchimp website then sync changes.
26
+ %br
27
+ This operation also creates the audience merge fields and updates the subscriber counts for each group below.
28
+
29
+ = render('admin/mailchimp/sync')
30
+
31
+ = collapse('More settings') do
32
+ %p
33
+ For more information see
34
+ = succeed(',') do
35
+ = link_to(etsd(Effective::MailchimpList), effective_mailchimp.admin_mailchimp_lists_path, target: '_blank')
36
+ = succeed(',') do
37
+ = link_to(etsd(Effective::MailchimpListMember), effective_mailchimp.admin_mailchimp_list_members_path, target: '_blank')
38
+ = succeed(',') do
39
+ = link_to(etsd(Effective::MailchimpCategory), effective_mailchimp.admin_mailchimp_categories_path, target: '_blank')
40
+ and
41
+ = succeed('.') do
42
+ = link_to(etsd(Effective::MailchimpInterest), effective_mailchimp.admin_mailchimp_interests_path, target: '_blank')
43
+
44
+ %p The following merge fields are sent to Mailchimp when a user subscribes:
45
+
46
+ %ul
47
+ - current_user.mailchimp_merge_fields.keys.sort.each do |key|
48
+ %li= key
49
+
50
+ .mb-4
51
+
52
+ = card(Effective::MailchimpList) do
53
+ %p
54
+ Audiences contain a list of your contacts.
55
+ View all #{link_to ets(Effective::MailchimpList), EffectiveMailchimp.api.audience_url, target: '_blank'}.
56
+
57
+ %p
58
+ %small
59
+ To change the name, please visit the Mailchimp website then sync changes.
60
+ Press the Edit button to change the Can Subscribe and Force Subscribe settings.
61
+
62
+ - datatable = Admin::EffectiveMailchimpListsDatatable.new
63
+ = render_datatable(datatable, simple: true, inline: true)
64
+
65
+ = card(Effective::MailchimpCategory) do
66
+ %p
67
+ The following groups are displayed to the user where they can opt-in to each interest.
68
+ View all #{link_to ets(Effective::MailchimpCategory), EffectiveMailchimp.api.groups_url, target: '_blank'}.
69
+
70
+ %p
71
+ %small
72
+ To change the name, please visit the Mailchimp website then sync changes.
73
+ Press the Edit button to change the Can Subscribe and Force Subscribe settings.
74
+ Sync changes to update the subscriber counts.
75
+
76
+ - Effective::MailchimpCategory.all.each do |mailchimp_category|
77
+ = card(mailchimp_category.to_s) do
78
+ - datatable = Admin::EffectiveMailchimpInterestsDatatable.new(mailchimp_category: mailchimp_category)
79
+ = render_datatable(datatable, simple: true, inline: true)
80
+
@@ -0,0 +1,10 @@
1
+ = effective_form_with(model: [:admin, mailchimp_interest], engine: true) do |f|
2
+ = f.static_field :name, label: EffectiveResources.et(mailchimp_interest)
3
+
4
+ = f.check_box :can_subscribe, label: "Yes, display this interest and allow them to subscribe"
5
+
6
+ = f.check_box :force_subscribe,
7
+ label: "Yes, force users to subscribe. Subscribe them automatically and do not allow unsubscribe from the website",
8
+ hint: "They can still unsubscribe from the email link to unsubscribe"
9
+
10
+ = effective_submit(f)
@@ -1,5 +1,6 @@
1
1
  = effective_form_with(model: [:admin, mailchimp_list], engine: true) do |f|
2
2
  = f.static_field :name, label: EffectiveResources.et(mailchimp_list)
3
+ = f.static_field :merge_fields
3
4
 
4
5
  = f.check_box :can_subscribe, label: "Yes, display users and allow them to subscribe"
5
6
 
@@ -1,13 +1,19 @@
1
- = card(ets(Effective::MailchimpList)) do
2
- %p.text-muted #{user} is subscribed to #{pluralize(user.mailchimp_subscribed_lists.count, et(Effective::MailchimpList))}.
1
+ = card('Mailchimp') do
2
+ %p.text-muted
3
+ = succeed('.') do
4
+ = user
5
+ is subscribed to
6
+ = pluralize(user.mailchimp_subscribed_lists.count, etd(Effective::MailchimpList))
7
+ and
8
+ = pluralize(user.mailchimp_subscribed_interests.count, etd(Effective::MailchimpInterest))
3
9
 
4
10
  %p.text-muted
5
11
  Please visit
6
- = link_to 'All ' + ets(Effective::MailchimpList), effective_mailchimp.admin_mailchimp_lists_path, target: '_blank'
12
+ = link_to 'Mailchimp Settings', effective_mailchimp.admin_mailchimp_path, target: '_blank'
7
13
  to configure which are displayed.
8
14
 
9
15
  %p
10
- %strong Subscribed
16
+ %strong Subscribed to the following
11
17
 
12
18
  = effective_form_with model: [:admin, user] do |f|
13
19
  = f.hidden_field :id
@@ -7,14 +7,41 @@
7
7
  = fmlm.hidden_field :id
8
8
  = fmlm.hidden_field :mailchimp_list_id
9
9
 
10
- - if mailchimp_list.force_subscribe?
11
- %p
12
- - if fmlm.object.cannot_be_subscribed?
13
- = fmlm.check_box :subscribed, label: fmlm.object.to_s, disabled: true, hint: 'required but unsubscribed', checked: false
10
+ - # With groups, probably single audience implementation
11
+ - if mailchimp_list.grouped?
12
+ = fmlm.hidden_field :subscribed, value: true
13
+
14
+ - # For each group / mailchimp_category
15
+ - mailchimp_list.mailchimp_categories.each do |mailchimp_category|
16
+ - mailchimp_interests = mailchimp_category.mailchimp_interests
17
+
18
+ - # Force subscription of each interest
19
+ - forced = mailchimp_interests.select { |mi| mi.force_subscribe? }
20
+ - fmlm.object.build_interests(forced)
21
+
22
+ - forced.each do |mailchimp_interest|
23
+ = fmlm.hidden_field :interests, name: "#{fmlm.object_name}[interests][]", value: mailchimp_interest.mailchimp_id
24
+
25
+ - # Add control for mailchimp interests
26
+ - mailchimp_interests_collection = mailchimp_list_member_interests_collection(mailchimp_interests)
27
+
28
+ - if mailchimp_category.display_type == 'select'
29
+ = fmlm.select :interests, mailchimp_interests_collection, label: mailchimp_category.to_s, name: "#{fmlm.object_name}[interests][]"
30
+ - elsif mailchimp_category.display_type == 'radio'
31
+ = fmlm.radios :interests, mailchimp_interests_collection, label: mailchimp_category.to_s, name: "#{fmlm.object_name}[interests][]"
14
32
  - else
15
- = fmlm.check_box :subscribed, label: fmlm.object.to_s, disabled: true, hint: 'required', checked: true
16
- = fmlm.hidden_field :subscribed, value: true
33
+ = fmlm.checks :interests, mailchimp_interests_collection, label: mailchimp_category.to_s, name: "#{fmlm.object_name}[interests][]"
34
+
35
+ - # No groups, multiple audiences implementation
36
+ - if mailchimp_list.ungrouped?
37
+ - if mailchimp_list.force_subscribe?
38
+ %p
39
+ - if fmlm.object.cannot_be_subscribed?
40
+ = fmlm.check_box :subscribed, label: fmlm.object.to_s, disabled: true, hint: 'required but unsubscribed', checked: false
41
+ - else
42
+ = fmlm.check_box :subscribed, label: fmlm.object.to_s, disabled: true, hint: 'required', checked: true
43
+ = fmlm.hidden_field :subscribed, value: true
17
44
 
18
- - elsif mailchimp_list.can_subscribe?
19
- %p
20
- = fmlm.check_box :subscribed, label: fmlm.object.to_s
45
+ - elsif mailchimp_list.can_subscribe?
46
+ %p
47
+ = fmlm.check_box :subscribed, label: fmlm.object.to_s
@@ -0,0 +1,12 @@
1
+ en:
2
+ effective_mailchimp:
3
+ name: 'Effective Mailchimp'
4
+ acronym: 'Mailchimp'
5
+
6
+ activerecord:
7
+ models:
8
+ # These ones should stay effective
9
+ effective/mailchimp_list: 'Mailchimp Audience'
10
+ effective/mailchimp_list_member: 'Mailchimp Audience Member'
11
+ effective/mailchimp_category: 'Mailchimp Group'
12
+ effective/mailchimp_interest: 'Mailchimp Group Interest'
data/config/routes.rb CHANGED
@@ -13,19 +13,17 @@ EffectiveMailchimp::Engine.routes.draw do
13
13
  end
14
14
 
15
15
  namespace :admin do
16
- resources :mailchimp_lists, only: [:index, :edit, :update] do
17
- post :can_subscribe, on: :member
18
- post :cannot_subscribe, on: :member
19
-
20
- post :force_subscribe, on: :member
21
- post :unforce_subscribe, on: :member
22
-
23
- get :mailchimp_sync, on: :collection
24
- end
16
+ resources :mailchimp_lists, only: [:index, :edit, :update]
17
+ resources :mailchimp_interests, only: [:index, :edit, :update]
18
+ resources :mailchimp_categories, only: :index
19
+ resources :mailchimp_list_members, only: :index
25
20
 
26
21
  resources :mailchimp, only: [] do
22
+ post :mailchimp_sync, on: :collection
27
23
  post :mailchimp_sync_user, on: :member
28
24
  end
25
+
26
+ get '/mailchimp', to: 'mailchimp#index', as: :mailchimp
29
27
  end
30
28
 
31
29
  end
@@ -11,6 +11,41 @@ class CreateEffectiveMailchimp < ActiveRecord::Migration[6.0]
11
11
  t.timestamps
12
12
  end
13
13
 
14
+ create_table :mailchimp_categories do |t|
15
+ t.integer :mailchimp_list_id
16
+
17
+ t.string :mailchimp_id
18
+ t.string :list_id
19
+
20
+ t.string :name
21
+ t.string :list_name
22
+
23
+ t.string :display_type
24
+
25
+ t.timestamps
26
+ end
27
+
28
+ create_table :mailchimp_interests do |t|
29
+ t.integer :mailchimp_list_id
30
+ t.integer :mailchimp_category_id
31
+
32
+ t.string :mailchimp_id
33
+ t.string :list_id
34
+ t.string :category_id
35
+
36
+ t.string :name
37
+ t.string :list_name
38
+ t.string :category_name
39
+
40
+ t.integer :display_order
41
+ t.integer :subscriber_count
42
+
43
+ t.boolean :can_subscribe
44
+ t.boolean :force_subscribe
45
+
46
+ t.timestamps
47
+ end
48
+
14
49
  create_table :mailchimp_list_members do |t|
15
50
  t.integer :user_id
16
51
  t.string :user_type
@@ -26,6 +61,8 @@ class CreateEffectiveMailchimp < ActiveRecord::Migration[6.0]
26
61
  t.boolean :subscribed, default: false
27
62
  t.boolean :cannot_be_subscribed, default: false
28
63
 
64
+ t.text :interests
65
+
29
66
  t.datetime :last_synced_at
30
67
 
31
68
  t.timestamps
@@ -1,3 +1,3 @@
1
1
  module EffectiveMailchimp
2
- VERSION = '0.6.0'.freeze
2
+ VERSION = '0.7.0'.freeze
3
3
  end
@@ -7,7 +7,7 @@ module EffectiveMailchimp
7
7
 
8
8
  def self.config_keys
9
9
  [
10
- :mailchimp_lists_table_name, :mailchimp_list_members_table_name,
10
+ :mailchimp_lists_table_name, :mailchimp_list_members_table_name, :mailchimp_categories_table_name, :mailchimp_interests_table_name,
11
11
  :layout,
12
12
  :api_key
13
13
  ]
@@ -23,8 +23,12 @@ module EffectiveMailchimp
23
23
  api_key.present?
24
24
  end
25
25
 
26
+ def self.api_blank?
27
+ api_key.blank?
28
+ end
29
+
26
30
  def self.permitted_params
27
- [ :mailchimp_user_form_action, mailchimp_list_members_attributes: [:id, :mailchimp_list_id, :subscribed] ]
31
+ [ :mailchimp_user_form_action, mailchimp_list_members_attributes: [:id, :mailchimp_list_id, :subscribed, interests: []] ]
28
32
  end
29
33
 
30
34
  end
@@ -5,20 +5,4 @@ namespace :effective_mailchimp do
5
5
  load "#{__dir__}/../../db/seeds.rb"
6
6
  end
7
7
 
8
- # bundle exec rake effective_mailchimp:create_mailchimp_merge_fields
9
- task create_mailchimp_merge_fields: :environment do
10
- merge_fields = User.new.mailchimp_merge_fields()
11
-
12
- Effective::MailchimpList.sync!
13
-
14
- collection = Effective::MailchimpList.all
15
-
16
- collection.find_each do |list|
17
- puts "Creating #{list} merge fields"
18
- list.create_mailchimp_merge_fields!(merge_fields)
19
- end
20
-
21
- puts 'All done'
22
- end
23
-
24
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: effective_mailchimp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code and Effect
@@ -193,23 +193,34 @@ files:
193
193
  - app/assets/javascripts/effective_mailchimp/base.js
194
194
  - app/assets/stylesheets/effective_mailchimp.scss
195
195
  - app/assets/stylesheets/effective_mailchimp/base.scss
196
+ - app/controllers/admin/mailchimp_categories_controller.rb
196
197
  - app/controllers/admin/mailchimp_controller.rb
198
+ - app/controllers/admin/mailchimp_interests_controller.rb
199
+ - app/controllers/admin/mailchimp_list_members_controller.rb
197
200
  - app/controllers/admin/mailchimp_lists_controller.rb
198
201
  - app/controllers/effective/mailchimp_controller.rb
202
+ - app/datatables/admin/effective_mailchimp_categories_datatable.rb
203
+ - app/datatables/admin/effective_mailchimp_interests_datatable.rb
204
+ - app/datatables/admin/effective_mailchimp_list_members_datatable.rb
199
205
  - app/datatables/admin/effective_mailchimp_lists_datatable.rb
200
206
  - app/helpers/effective_mailchimp_helper.rb
201
207
  - app/jobs/effective_mailchimp_update_job.rb
202
208
  - app/models/concerns/effective_mailchimp_user.rb
203
209
  - app/models/effective/mailchimp_api.rb
210
+ - app/models/effective/mailchimp_category.rb
211
+ - app/models/effective/mailchimp_interest.rb
204
212
  - app/models/effective/mailchimp_list.rb
205
213
  - app/models/effective/mailchimp_list_member.rb
214
+ - app/views/admin/mailchimp/_sync.html.haml
215
+ - app/views/admin/mailchimp/index.html.haml
216
+ - app/views/admin/mailchimp_interests/_form.html.haml
206
217
  - app/views/admin/mailchimp_lists/_form.html.haml
207
- - app/views/admin/mailchimp_lists/index.html.haml
208
218
  - app/views/admin/mailchimp_user/_form.html.haml
209
219
  - app/views/admin/mailchimp_user/_sync.html.haml
210
220
  - app/views/effective/mailchimp_user/_fields.html.haml
211
221
  - app/views/effective/mailchimp_user/_sync.html.haml
212
222
  - config/effective_mailchimp.rb
223
+ - config/locales/effective_mailchimp.en.yml
213
224
  - config/routes.rb
214
225
  - db/migrate/101_create_effective_mailchimp.rb
215
226
  - db/seeds.rb
@@ -1,20 +0,0 @@
1
- %h1.effective-admin-heading= @page_title
2
-
3
- - resource = (@_effective_resource || Effective::Resource.new(controller_path))
4
-
5
- .resource-buttons
6
- = render_resource_buttons(resource.klass, (action ||= :index) => false)
7
-
8
- = card do
9
- = collapse('Show merge field settings') do
10
- %p The following Merge fields are sent to Mailchimp when a user subscribes:
11
-
12
- %ul
13
- - current_user.mailchimp_merge_fields.keys.each do |key|
14
- %li= key
15
-
16
- %p To have these fields displayed in Mailchimp, please configure each campaign with any of these merge fields.
17
-
18
- .mb-4
19
-
20
- = render_datatable @datatable