dotmailer 0.0.3 → 0.0.4

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.
@@ -73,6 +73,13 @@ module DotMailer
73
73
  end
74
74
  end
75
75
 
76
+ def delete(path)
77
+ rescue_api_errors do
78
+ endpoint = endpoint_for(path)
79
+ RestClient.delete endpoint, :accept => :json
80
+ end
81
+ end
82
+
76
83
  def to_s
77
84
  "#{self.class.name} api_user: #{api_user}"
78
85
  end
@@ -104,6 +104,10 @@ module DotMailer
104
104
  client.put_json "/contacts/#{id}", attributes.merge('dataFields' => data_fields_for_api)
105
105
  end
106
106
 
107
+ def delete
108
+ client.delete "/contacts/#{id}"
109
+ end
110
+
107
111
  def subscribed?
108
112
  status == SUBSCRIBED_STATUS
109
113
  end
@@ -2,11 +2,15 @@ require 'csv'
2
2
  require 'active_support/core_ext/object/blank'
3
3
 
4
4
  module DotMailer
5
+ # This is the maximum number of times we will poll the dotMailer
6
+ # API to see if an import has finished.
7
+ MAX_TRIES = 10
8
+
5
9
  class ContactImport
6
- def self.import(account, contacts)
10
+ def self.import(account, contacts, wait_for_finish = false)
7
11
  contact_import = new(account, contacts)
8
12
 
9
- contact_import.start
13
+ contact_import.start(wait_for_finish)
10
14
 
11
15
  contact_import
12
16
  end
@@ -18,12 +22,14 @@ module DotMailer
18
22
  self.contacts = contacts
19
23
  end
20
24
 
21
- def start
25
+ def start(wait_for_finish)
22
26
  validate_headers
23
27
 
24
28
  response = client.post_csv '/contacts/import', contacts_csv
25
29
 
26
30
  self.id = response['id']
31
+
32
+ wait_until_finished if wait_for_finish
27
33
  end
28
34
 
29
35
  def status
@@ -90,5 +96,17 @@ module DotMailer
90
96
  def valid_headers
91
97
  @valid_headers ||= %w(id email optInType emailType) + account.data_fields.map(&:name)
92
98
  end
99
+
100
+ def wait_until_finished
101
+ # Wait for the import to finish, backing off in incremental powers
102
+ # of 2, a maximum of MAX_TRIES times.
103
+ #
104
+ # (i.e. 1s, 4s, 9s, 16s, ..., MAX_TRIES ** 2)
105
+ #
106
+ # A MAX_TRIES of 10 means we will wait a total of 385 seconds before
107
+ # giving up.
108
+ finished = (1..MAX_TRIES).detect { |i| sleep(i ** 2) && finished? }
109
+ raise ImportNotFinished unless finished
110
+ end
93
111
  end
94
112
  end
@@ -1,7 +1,13 @@
1
1
  module DotMailer
2
+ class Exception < ::Exception
3
+ end
4
+
2
5
  class ImportNotFinished < Exception
3
6
  end
4
7
 
8
+ class InvalidFromAddress < Exception
9
+ end
10
+
5
11
  class InvalidRequest < Exception
6
12
  end
7
13
 
@@ -0,0 +1,36 @@
1
+ require 'active_support/core_ext/hash/slice'
2
+
3
+ module DotMailer
4
+ class FromAddress
5
+ def initialize(attributes)
6
+ self.attributes = attributes
7
+ end
8
+
9
+ def id
10
+ attributes['id']
11
+ end
12
+
13
+ def email
14
+ attributes['email']
15
+ end
16
+
17
+ def to_hash
18
+ attributes.slice('id', 'email')
19
+ end
20
+
21
+ def ==(other)
22
+ attributes == other.attributes
23
+ end
24
+
25
+ def to_s
26
+ %{#{self.class.name} id: #{id}, email: #{email}}
27
+ end
28
+
29
+ def inspect
30
+ to_s
31
+ end
32
+
33
+ protected
34
+ attr_accessor :attributes
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ module DotMailer
2
+ class Segment
3
+ def initialize(account, attributes)
4
+ self.account = account
5
+ self.attributes = attributes
6
+ end
7
+
8
+ def self.find_by_id(account, id)
9
+ response = account.client.get "/segments"
10
+ response = response.detect {|segment| segment['id'] == id}
11
+
12
+ new(account, response)
13
+ end
14
+
15
+ def id
16
+ attributes['id']
17
+ end
18
+
19
+ def refresh!
20
+ client.post_json "/segments/refresh/#{self.id}", {}
21
+ end
22
+
23
+ def refresh_progress
24
+ response = client.get "/segments/refresh/#{self.id}"
25
+ return response["status"]
26
+ end
27
+
28
+ private
29
+ attr_accessor :attributes, :account
30
+
31
+ def client
32
+ account.client
33
+ end
34
+ end
35
+ end
36
+
@@ -1,3 +1,3 @@
1
1
  module DotMailer
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -4,11 +4,15 @@ describe DotMailer::Account do
4
4
  let(:api_user) { double 'api user' }
5
5
  let(:api_pass) { double 'api pass' }
6
6
  let(:client) { double 'client' }
7
+ let(:cache) { double 'cache' }
7
8
 
8
9
  subject { DotMailer::Account.new(api_user, api_pass) }
9
10
 
10
11
  before(:each) do
11
- subject.stub :client => client
12
+ subject.stub(
13
+ :client => client,
14
+ :cache => cache
15
+ )
12
16
  end
13
17
 
14
18
  describe '#initialize' do
@@ -51,11 +55,6 @@ describe DotMailer::Account do
51
55
 
52
56
  describe '#data_fields' do
53
57
  let(:data_fields) { double 'data fields' }
54
- let(:cache) { double 'cache' }
55
-
56
- before(:each) do
57
- subject.stub :cache => cache
58
- end
59
58
 
60
59
  context 'when the cache is empty' do
61
60
  before(:each) do
@@ -90,10 +89,8 @@ describe DotMailer::Account do
90
89
  describe '#create_data_field' do
91
90
  let(:name) { double 'name' }
92
91
  let(:options) { double 'options' }
93
- let(:cache) { double 'cache' }
94
92
 
95
93
  before(:each) do
96
- subject.stub :cache => cache
97
94
  DotMailer::DataField.stub :create
98
95
  cache.stub :delete
99
96
  end
@@ -110,4 +107,56 @@ describe DotMailer::Account do
110
107
  subject.create_data_field(name, options)
111
108
  end
112
109
  end
110
+
111
+ describe '#from_addresses' do
112
+ let(:attributes) { double 'attributes' }
113
+ let(:response) { 3.times.map { attributes } }
114
+ let(:from_address) { double 'from address' }
115
+ let(:from_addresses) { 3.times.map { from_address } }
116
+
117
+ before(:each) do
118
+ DotMailer::FromAddress.stub :new => from_address
119
+ end
120
+
121
+ context 'when the cache is empty' do
122
+ before(:each) do
123
+ cache.stub(:fetch).with('from_addresses').and_yield
124
+ client.stub :get => response
125
+ end
126
+
127
+ it 'should call get on the client with the correct path' do
128
+ client.should_receive(:get).with('/custom-from-addresses')
129
+
130
+ subject.from_addresses
131
+ end
132
+
133
+ it 'should initialize 3 FromAddresses' do
134
+ DotMailer::FromAddress.should_receive(:new).with(attributes).exactly(3).times
135
+
136
+ subject.from_addresses
137
+ end
138
+
139
+ its(:from_addresses) { should == from_addresses }
140
+ end
141
+
142
+ context 'when the cache is not empty' do
143
+ before(:each) do
144
+ cache.stub(:fetch).with('from_addresses').and_return(response)
145
+ end
146
+
147
+ it 'should not call get on the client' do
148
+ client.should_not_receive(:get)
149
+
150
+ subject.from_addresses
151
+ end
152
+
153
+ it 'should initialize 3 FromAddresses' do
154
+ DotMailer::FromAddress.should_receive(:new).with(attributes).exactly(3).times
155
+
156
+ subject.from_addresses
157
+ end
158
+
159
+ its(:from_addresses) { should == from_addresses }
160
+ end
161
+ end
113
162
  end
@@ -0,0 +1,206 @@
1
+ require 'spec_helper'
2
+
3
+ describe DotMailer::Campaign do
4
+ let(:client) { double 'client' }
5
+ let(:account) { double 'account', :client => client }
6
+
7
+ let(:id) { 123 }
8
+ let(:name) { 'my_campaign' }
9
+ let(:campaign_subject) { 'My Campaign' }
10
+ let(:from_name) { 'Me' }
11
+ let(:from_email) { 'me@example.com' }
12
+ let(:html_content) { '<h1>Hello!</h1><a href="http://$UNSUB$">Unsubscribe</a>' }
13
+ let(:plain_text_content) { "Hello!\n======\nhttp://$UNSUB$" }
14
+
15
+ let(:from_address) do
16
+ DotMailer::FromAddress.new 'id' => 123, 'email' => from_email
17
+ end
18
+
19
+ subject do
20
+ DotMailer::Campaign.new(account, {
21
+ 'id' => id,
22
+ 'name' => name,
23
+ 'subject' => campaign_subject,
24
+ 'fromName' => from_name,
25
+ 'fromAddress' => from_address.to_hash,
26
+ 'htmlContent' => html_content,
27
+ 'plainTextContent' => plain_text_content
28
+ })
29
+ end
30
+
31
+ describe 'Class' do
32
+ subject { DotMailer::Campaign }
33
+
34
+ describe '.create' do
35
+ let(:response) { double 'response' }
36
+ let(:campaign) { double 'campaign' }
37
+
38
+ # We define a method so we can override keys within
39
+ # context blocks without redefining other keys
40
+ def attributes
41
+ {
42
+ :name => name,
43
+ :subject => campaign_subject,
44
+ :from_name => from_name,
45
+ :from_email => from_email,
46
+ :html_content => html_content,
47
+ :plain_text_content => plain_text_content
48
+ }
49
+ end
50
+
51
+ before(:each) do
52
+ account.stub :from_addresses => [from_address]
53
+ client.stub :post_json => response
54
+ subject.stub :new => campaign
55
+ end
56
+
57
+ [
58
+ :name,
59
+ :subject,
60
+ :from_name,
61
+ :from_email,
62
+ :html_content,
63
+ :plain_text_content
64
+ ].each do |attribute|
65
+ context "without specifying #{attribute}" do
66
+ define_method :attributes do
67
+ super().except(attribute)
68
+ end
69
+
70
+ it 'should raise an error' do
71
+ expect { subject.create(account, attributes) }.to \
72
+ raise_error(RuntimeError, "missing :#{attribute}")
73
+ end
74
+ end
75
+ end
76
+
77
+ context 'when the fromAddress is not a valid from address' do
78
+ let(:unknown_email) { 'unknown@example.com' }
79
+
80
+ def attributes
81
+ super.merge(:from_email => unknown_email)
82
+ end
83
+
84
+ it 'should raise an error' do
85
+ expect { subject.create(account, attributes) }.to \
86
+ raise_error(DotMailer::InvalidFromAddress, unknown_email)
87
+ end
88
+ end
89
+
90
+ it 'should call post_json on the client with the correct path' do
91
+ client.should_receive(:post_json).with('/campaigns', anything)
92
+
93
+ subject.create(account, attributes)
94
+ end
95
+
96
+ it 'should call post_json on the client with the correct parameters' do
97
+ client.should_receive(:post_json).with(anything, {
98
+ 'Name' => name,
99
+ 'Subject' => campaign_subject,
100
+ 'FromName' => from_name,
101
+ 'FromAddress' => from_address.to_hash,
102
+ 'HtmlContent' => html_content,
103
+ 'PlainTextContent' => plain_text_content
104
+ })
105
+
106
+ subject.create(account, attributes)
107
+ end
108
+
109
+ it 'should instantiate a new Campaign object with the account and response' do
110
+ subject.should_receive(:new).with(account, response)
111
+
112
+ subject.create(account, attributes)
113
+ end
114
+
115
+ it 'should return the new Campaign object' do
116
+ subject.create(account, attributes).should == campaign
117
+ end
118
+ end
119
+
120
+ describe '.find_by_id' do
121
+ let(:id) { 123 }
122
+ let(:response) { double 'response' }
123
+ let(:campaign) { double 'campaign' }
124
+
125
+ before(:each) do
126
+ subject.stub :new => campaign
127
+ client.stub :get => response
128
+ end
129
+
130
+ it 'should call get on the client with the correct parameters' do
131
+ client.should_receive(:get).with("/campaigns/#{id}")
132
+
133
+ subject.find_by_id(account, id)
134
+ end
135
+
136
+ it 'should initialize a Campaign with the response' do
137
+ subject.should_receive(:new).with(account, response)
138
+
139
+ subject.find_by_id(account, id)
140
+ end
141
+
142
+ it 'should return the new Campaign object' do
143
+ subject.find_by_id(account, id).should == campaign
144
+ end
145
+ end
146
+ end
147
+
148
+ its(:id) { should == id }
149
+ its(:name) { should == name }
150
+ its(:from_name) { should == from_name }
151
+ its(:from_address) { should == from_address }
152
+ its(:html_content) { should == html_content }
153
+ its(:plain_text_content) { should == plain_text_content }
154
+
155
+ describe '#send_to_contact_ids' do
156
+ let(:contact_ids) { double 'contact ids' }
157
+
158
+ it 'should call post_json on the client with the correct path' do
159
+ client.should_receive(:post_json).with('/campaigns/send', anything)
160
+
161
+ subject.send_to_contact_ids contact_ids
162
+ end
163
+
164
+ it 'should call post_json on the client with the contact ids' do
165
+ client.should_receive(:post_json).with(anything, {
166
+ 'campaignId' => id,
167
+ 'contactIds' => contact_ids
168
+ })
169
+
170
+ subject.send_to_contact_ids contact_ids
171
+ end
172
+ end
173
+
174
+ describe '#send_to_segment' do
175
+ let(:segment) { double 'segment', :id => 123 }
176
+
177
+ it 'should call post_json on the client with the correct path' do
178
+ client.should_receive(:post_json).with('/campaigns/send', anything)
179
+
180
+ subject.send_to_segment segment
181
+ end
182
+
183
+ it 'should call post_json on the client with the contact ids' do
184
+ client.should_receive(:post_json).with(anything, {
185
+ 'campaignId' => id,
186
+ 'addressBookIds' => [segment.id]
187
+ })
188
+
189
+ subject.send_to_segment segment
190
+ end
191
+ end
192
+
193
+ describe '#summary' do
194
+ let(:summary) { double 'campaign_summary'}
195
+
196
+ it 'should call get on the client with the correct path' do
197
+ client.should_receive(:get).with("/campaigns/#{id}/summary")
198
+ subject.summary
199
+ end
200
+
201
+ it 'should return a CampaignSummary object' do
202
+ client.should_receive(:get).with(anything).and_return(summary)
203
+ subject.summary.class.should be(DotMailer::CampaignSummary)
204
+ end
205
+ end
206
+ end