Merge pull request #43 from CactusPuppy/176895554/mailbluster
Integrate Snapcon with Mailbluster
This commit is contained in:
commit
db8cb3ea1a
12 changed files with 314 additions and 0 deletions
3
Gemfile
3
Gemfile
|
|
@ -221,6 +221,9 @@ gem 'dalli'
|
|||
|
||||
gem 'icalendar'
|
||||
|
||||
# for making external requests easier
|
||||
gem 'httparty'
|
||||
|
||||
# pagination
|
||||
gem 'pagy', '<4.0'
|
||||
|
||||
|
|
|
|||
|
|
@ -715,6 +715,7 @@ DEPENDENCIES
|
|||
guard-rspec
|
||||
haml-lint
|
||||
haml-rails
|
||||
httparty
|
||||
icalendar
|
||||
iso-639
|
||||
jquery-datatables
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ class ConferenceRegistrationsController < ApplicationController
|
|||
sign_in(@registration.user)
|
||||
end
|
||||
|
||||
MailblusterEditLeadJob.perform_later(@user, add_tags: ["snapcon-#{@conference.short_title}"])
|
||||
|
||||
if @conference.tickets.visible.any? && !current_user.supports?(@conference)
|
||||
redirect_to conference_tickets_path(@conference.short_title),
|
||||
notice: 'You are now registered and will be receiving E-Mail notifications.'
|
||||
|
|
@ -87,6 +89,7 @@ class ConferenceRegistrationsController < ApplicationController
|
|||
|
||||
def destroy
|
||||
if @registration.destroy
|
||||
MailblusterEditLeadJob.perform_later(@user, remove_tags: ["snapcon-#{@conference.short_title}"])
|
||||
redirect_to root_path,
|
||||
notice: "You are not registered for #{@conference.title} anymore!"
|
||||
else
|
||||
|
|
|
|||
9
app/jobs/mailbluster_create_lead_job.rb
Normal file
9
app/jobs/mailbluster_create_lead_job.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MailblusterCreateLeadJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(user)
|
||||
MailblusterManager.create_lead(user)
|
||||
end
|
||||
end
|
||||
7
app/jobs/mailbluster_delete_lead_job.rb
Normal file
7
app/jobs/mailbluster_delete_lead_job.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
class MailblusterDeleteLeadJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(user)
|
||||
MailblusterManager.delete_lead(user)
|
||||
end
|
||||
end
|
||||
9
app/jobs/mailbluster_edit_lead_job.rb
Normal file
9
app/jobs/mailbluster_edit_lead_job.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MailblusterEditLeadJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(user, add_tags: [], remove_tags: [], old_email: nil)
|
||||
MailblusterManager.edit_lead(user, add_tags: add_tags, remove_tags: remove_tags, old_email: old_email)
|
||||
end
|
||||
end
|
||||
48
app/models/concerns/track_saved_changes.rb
Normal file
48
app/models/concerns/track_saved_changes.rb
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# https://github.com/ccmcbeck/after-commit
|
||||
module TrackSavedChanges
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# expose the details if consumer wants to do more
|
||||
# attr_reader :ts_saved_changes_history, :ts_saved_changes_unfiltered
|
||||
after_initialize :ts_reset_saved_changes
|
||||
after_save :ts_track_saved_changes
|
||||
end
|
||||
|
||||
# on initalize, but useful for fine grain control
|
||||
def ts_reset_saved_changes
|
||||
@ts_saved_changes_unfiltered = {}
|
||||
@ts_saved_changes_history = []
|
||||
end
|
||||
|
||||
# filter out any changes that result in the original value
|
||||
def ts_saved_changes
|
||||
@ts_saved_changes_unfiltered.reject { |_k, v| v[0] == v[1] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# on save
|
||||
def ts_track_saved_changes
|
||||
# maintain an array of ActiveModel::Dirty.changes
|
||||
@ts_saved_changes_history << previous_changes.dup
|
||||
# accumulate the most recent changes
|
||||
@ts_saved_changes_history.last.each_pair { |k, v| ts_track_saved_change k, v }
|
||||
end
|
||||
|
||||
# v is an an array of [prev, current]
|
||||
def ts_track_saved_change(key, value)
|
||||
if @ts_saved_changes_unfiltered.key? key
|
||||
@ts_saved_changes_unfiltered[key][1] = ts_track_saved_value value[1]
|
||||
else
|
||||
@ts_saved_changes_unfiltered[key] = value.dup
|
||||
end
|
||||
end
|
||||
|
||||
# type safe dup inspred by http://stackoverflow.com/a/20955038
|
||||
def ts_track_saved_value(value)
|
||||
value.dup
|
||||
rescue TypeError
|
||||
value
|
||||
end
|
||||
end
|
||||
|
|
@ -53,6 +53,7 @@ class UserDisabled < StandardError
|
|||
end
|
||||
|
||||
class User < ApplicationRecord
|
||||
include TrackSavedChanges
|
||||
rolify
|
||||
# prevent N+1 queries with has_cached_role? by preloading roles *always*
|
||||
default_scope { preload(:roles) }
|
||||
|
|
@ -80,6 +81,12 @@ class User < ApplicationRecord
|
|||
|
||||
after_save :touch_events
|
||||
|
||||
# Note that using after_create_commit and after_update_commit does not work.
|
||||
# See https://github.com/CactusPuppy/snapcon/pull/43#discussion_r609458034
|
||||
after_commit :mailbluster_create_lead, on: :create
|
||||
after_commit :mailbluster_delete_lead, on: :destroy
|
||||
after_commit :mailbluster_update_lead, on: :update, if: ->(user){ ['name', 'email'].any? { |key| user.ts_saved_changes.key? key } }
|
||||
|
||||
# add scope
|
||||
scope :comment_notifiable, ->(conference) {joins(:roles).where('roles.name IN (?)', [:organizer, :cfp]).where('roles.resource_type = ? AND roles.resource_id = ?', 'Conference', conference.id)}
|
||||
|
||||
|
|
@ -361,12 +368,32 @@ class User < ApplicationRecord
|
|||
User.count == 1 && User.first.email == 'deleted@localhost.osem'
|
||||
end
|
||||
|
||||
# TODO: email_hash function for mailbluster
|
||||
# def email_hash
|
||||
# Digest::MD5.hexdigest user.email
|
||||
# end
|
||||
|
||||
private
|
||||
|
||||
def setup_role
|
||||
self.is_admin = true if User.empty?
|
||||
end
|
||||
|
||||
def mailbluster_create_lead
|
||||
MailblusterCreateLeadJob.perform_later self
|
||||
ts_reset_saved_changes
|
||||
end
|
||||
|
||||
def mailbluster_delete_lead
|
||||
MailblusterDeleteLeadJob.perform_later email
|
||||
ts_reset_saved_changes
|
||||
end
|
||||
|
||||
def mailbluster_update_lead
|
||||
MailblusterEditLeadJob.perform_later(self, old_email: ts_saved_changes.fetch('email', [nil])[0])
|
||||
ts_reset_saved_changes
|
||||
end
|
||||
|
||||
def touch_events
|
||||
event_users.each(&:touch)
|
||||
end
|
||||
|
|
|
|||
40
app/services/mailbluster_manager.rb
Normal file
40
app/services/mailbluster_manager.rb
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
class MailblusterManager
|
||||
include HTTParty
|
||||
base_uri 'https://api.mailbluster.com/api/leads/'
|
||||
@auth_headers = {
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => ENV['MAILBLUSTER_API_KEY']
|
||||
}
|
||||
}
|
||||
|
||||
def self.query_api(method, path, body: {})
|
||||
options = @auth_headers.merge(body: body.to_json)
|
||||
send(method, path, options).parsed_response
|
||||
end
|
||||
|
||||
def self.create_lead(user)
|
||||
query_api(:post, '/', body: {
|
||||
'email' => user.email,
|
||||
'firstName' => user.name,
|
||||
'overrideExisting' => true,
|
||||
'subscribed' => true,
|
||||
'tags' => [ENV['OSEM_NAME'] || 'snapcon']
|
||||
})
|
||||
end
|
||||
|
||||
def self.edit_lead(user, add_tags: [], remove_tags: [], old_email: nil)
|
||||
email_hash = Digest::MD5.hexdigest(old_email.presence || user.email)
|
||||
query_api(:put, "/#{email_hash}", body: {
|
||||
'email' => user.email,
|
||||
'firstName' => user.name,
|
||||
'addTags' => add_tags,
|
||||
'removeTags' => remove_tags
|
||||
})
|
||||
end
|
||||
|
||||
def self.delete_lead(email)
|
||||
email_hash = Digest::MD5.hexdigest email
|
||||
query_api(:delete, "/#{email_hash}")
|
||||
end
|
||||
end
|
||||
|
|
@ -80,8 +80,30 @@ FactoryBot.define do
|
|||
last_sign_in_at { Date.today }
|
||||
is_disabled { false }
|
||||
|
||||
after(:build) do |user|
|
||||
url_mailbluster = 'https://api.mailbluster.com/api/leads/'
|
||||
response_body = "{
|
||||
\"message\": \"Lead created\",
|
||||
\"lead\": {
|
||||
\"id\": 329395,
|
||||
\"firstName\": \"#{user.name}\",
|
||||
\"lastName\": \"\",
|
||||
\"fullName\": \"#{user.name}\",
|
||||
\"email\": \"#{user.email}\",
|
||||
\"subscribed\": true,
|
||||
\"tags\": [
|
||||
#{ENV['OSEM_NAME'] || 'snapcon'}
|
||||
],
|
||||
}
|
||||
}"
|
||||
WebMock.stub_request(:post, url_mailbluster)
|
||||
.to_return(body: response_body, status: 200)
|
||||
end
|
||||
# Called by every user creation
|
||||
|
||||
after(:create) do |user|
|
||||
user.is_admin = false
|
||||
|
||||
# save with bang cause we want change in DB and not just in object instance
|
||||
user.save!
|
||||
end
|
||||
|
|
|
|||
140
spec/services/mailbluster_manager_spec.rb
Normal file
140
spec/services/mailbluster_manager_spec.rb
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'webmock/rspec'
|
||||
|
||||
describe MailblusterManager, type: :model do
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
before(:each) do
|
||||
WebMock.reset_executed_requests!
|
||||
end
|
||||
|
||||
url = 'https://api.mailbluster.com/api/leads/'
|
||||
|
||||
describe 'query_api' do
|
||||
it 'translates :get to a get request' do
|
||||
stub_request(:get, url)
|
||||
described_class.query_api(:get, '/')
|
||||
|
||||
expect(WebMock).to have_requested(:get, url)
|
||||
end
|
||||
|
||||
it 'translates :post to a post request' do
|
||||
stub_request(:post, url + 'path')
|
||||
described_class.query_api(:post, '/path', body: { key: 'value' })
|
||||
|
||||
expect(WebMock).to have_requested(:post, url + 'path').with(body: { key: 'value' })
|
||||
end
|
||||
end
|
||||
|
||||
describe 'create_lead' do
|
||||
it 'makes a post request to Mailbluster\'s API and gets the correct response' do
|
||||
response_body = "{
|
||||
\"message\": \"Lead created\",
|
||||
\"lead\": {
|
||||
\"id\": 329395,
|
||||
\"firstName\": \"#{user.name}\",
|
||||
\"lastName\": \"\",
|
||||
\"fullName\": \"#{user.name}\",
|
||||
\"email\": \"#{user.email}\",
|
||||
\"subscribed\": true,
|
||||
\"tags\": [
|
||||
#{ENV['OSEM_NAME'] || 'snapcon'}
|
||||
],
|
||||
}
|
||||
}"
|
||||
stub_request(:post, url)
|
||||
.to_return(body: response_body, status: 200)
|
||||
response = described_class.create_lead(user)
|
||||
|
||||
expect(WebMock).to have_requested(:post, url).with(body: {
|
||||
'email': user.email,
|
||||
'firstName': user.name,
|
||||
'overrideExisting': true,
|
||||
'subscribed': true,
|
||||
'tags': [ENV['OSEM_NAME'] || 'snapcon']
|
||||
}.to_json)
|
||||
expect(response).to eq(response_body)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'edit_lead' do
|
||||
it 'makes a put request to Mailbluster\'s API to change the email and gets the correct response' do
|
||||
response_body = "{
|
||||
\"message\": \"Lead updated\",
|
||||
\"lead\": {
|
||||
\"id\": 329395,
|
||||
\"firstName\": \"#{user.name}\",
|
||||
\"lastName\": \"\",
|
||||
\"fullName\": \"#{user.name}\",
|
||||
\"email\": \"#{user.email}\",
|
||||
\"subscribed\": true,
|
||||
\"tags\": [
|
||||
#{ENV['OSEM_NAME'] || 'snapcon'}
|
||||
],
|
||||
}
|
||||
}"
|
||||
old_email = user.email
|
||||
user.email = 'new@new.org'
|
||||
user.save
|
||||
stub_request(:put, url + Digest::MD5.hexdigest(old_email))
|
||||
.to_return(body: response_body, status: 200)
|
||||
response = described_class.edit_lead(user, old_email: old_email)
|
||||
|
||||
expect(WebMock).to have_requested(:put, url + Digest::MD5.hexdigest(old_email)).with(body: {
|
||||
'email': user.email,
|
||||
'firstName': user.name,
|
||||
'addTags': [],
|
||||
'removeTags': []
|
||||
}.to_json)
|
||||
expect(response).to eq(response_body)
|
||||
end
|
||||
|
||||
it 'makes a put request to Mailbluster\'s API to add a tag and gets the correct response' do
|
||||
response_body = "{
|
||||
\"message\": \"Lead updated\",
|
||||
\"lead\": {
|
||||
\"id\": 329395,
|
||||
\"firstName\": \"#{user.name}\",
|
||||
\"lastName\": \"\",
|
||||
\"fullName\": \"#{user.name}\",
|
||||
\"email\": \"#{user.email}\",
|
||||
\"subscribed\": true,
|
||||
\"tags\": [
|
||||
#{ENV['OSEM_NAME'] || 'snapcon'}, '2021'
|
||||
],
|
||||
}
|
||||
}"
|
||||
stub_request(:put, url + Digest::MD5.hexdigest(user.email))
|
||||
.to_return(body: response_body, status: 200)
|
||||
add_tags = ['2021']
|
||||
response = described_class.edit_lead(user, add_tags: add_tags)
|
||||
|
||||
expect(WebMock).to have_requested(:put, url + Digest::MD5.hexdigest(user.email)).with(body: {
|
||||
'email': user.email,
|
||||
'firstName': user.name,
|
||||
'addTags': add_tags,
|
||||
'removeTags': []
|
||||
}.to_json)
|
||||
expect(response).to eq(response_body)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'delete_lead' do
|
||||
it 'correctly requests the right URL and gets a valid response' do
|
||||
email_hash = Digest::MD5.hexdigest user.email
|
||||
response_body = "{
|
||||
\"message\":\"Lead deleted\",
|
||||
\"leadHash\":\"#{email_hash}\"
|
||||
}"
|
||||
lead_url = url + email_hash.to_s
|
||||
stub_request(:delete, lead_url)
|
||||
.to_return(body: response_body)
|
||||
response = described_class.delete_lead(user.email)
|
||||
|
||||
expect(WebMock).to have_requested(:delete, lead_url)
|
||||
expect(response).to eq(response_body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -11,6 +11,7 @@ RSpec.configure do |config|
|
|||
config.before(:each) do
|
||||
mock_commercial_request
|
||||
mock_image_request
|
||||
mock_default_mailbluster
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -39,3 +40,7 @@ def mock_image_request
|
|||
WebMock.stub_request(:post, 'https://api.cloudinary.com/v1_1/snapcon/image/destroy')
|
||||
.to_return(status: 200, body: {}.to_json, headers: {})
|
||||
end
|
||||
|
||||
def mock_default_mailbluster
|
||||
WebMock.stub_request(:any, /api.mailbluster.com/)
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue