acts_as_icontact 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +13 -9
- data/VERSION +1 -1
- data/acts_as_icontact.gemspec +15 -3
- data/bin/icontact +22 -0
- data/lib/acts_as_icontact/rails.rb +10 -8
- data/lib/acts_as_icontact/rails/callbacks.rb +31 -0
- data/lib/acts_as_icontact/rails/macro.rb +14 -1
- data/lib/acts_as_icontact/rails/mappings.rb +74 -0
- data/lib/acts_as_icontact/resource.rb +11 -2
- data/lib/acts_as_icontact/resources/contact.rb +1 -1
- data/lib/acts_as_icontact/resources/custom_field.rb +51 -0
- data/lib/acts_as_icontact/resources/list.rb +5 -0
- data/spec/examples/schema.rb +7 -3
- data/spec/rails_spec.rb +67 -21
- data/spec/rails_spec/callbacks_spec.rb +19 -0
- data/spec/rails_spec/mappings_spec.rb +61 -0
- data/spec/resource_spec.rb +5 -0
- data/spec/resources/custom_field_spec.rb +52 -0
- data/spec/resources/list_spec.rb +5 -0
- data/spec/support/spec_fakeweb.rb +5 -0
- metadata +16 -6
data/README.markdown
CHANGED
@@ -152,9 +152,13 @@ When you call the `acts_as_icontact` method in an ActiveRecord class declaration
|
|
152
152
|
4. If an `icontact_status` field exists, creates named scopes on the model class for each iContact status. _(Pending)_
|
153
153
|
|
154
154
|
### Options
|
155
|
-
Option values and field mappings can be passed to the `acts_as_icontact` declaration to set default behavior for the model class.
|
155
|
+
Option values and field mappings can be passed to the `acts_as_icontact` declaration to set default behavior for the model class.
|
156
156
|
|
157
|
-
`
|
157
|
+
`list` -- _The name or ID number of a list to subscribe new contacts to automatically_
|
158
|
+
`lists` -- _Like `list` but takes an array of names or numbers; new contacts will be subscribed to all of them_
|
159
|
+
`exception_on_failure` -- _If true, throws an ActsAsIcontact::SyncError when synchronization fails. Defaults to false._
|
160
|
+
|
161
|
+
A note about failure: problems with synchronization are always logged to the standard Rails log. For most applications, however, updating iContact is a secondary consideration; if a new user is registering, you _probably_ don't want exceptions bubbling up and the whole transaction rolling back just because of a transient iContact server outage. So exceptions are something you have to deliberately enable.
|
158
162
|
|
159
163
|
### Field Mappings
|
160
164
|
You can add contact integration to any ActiveRecord model that tracks an email address. (If your model _doesn't_ include email but you want to use iContact with it, you are very, very confused.)
|
@@ -162,13 +166,13 @@ You can add contact integration to any ActiveRecord model that tracks an email a
|
|
162
166
|
Any fields that are named the same as iContact's personal information fields, or custom fields you've previously declared, will be autodiscovered. Otherwise you can map them:
|
163
167
|
|
164
168
|
class Customer < ActiveRecord::Base
|
165
|
-
acts_as_icontact :
|
166
|
-
:
|
167
|
-
:
|
168
|
-
:
|
169
|
-
:
|
170
|
-
:
|
171
|
-
:
|
169
|
+
acts_as_icontact :lists => ['New Customers', 'All Users'] # Puts new contact on two lists
|
170
|
+
:firstName => :given_name, # Key is iContact field, value is Rails field
|
171
|
+
:lastName => :family_name,
|
172
|
+
:street => :address1,
|
173
|
+
:street2 => :address2,
|
174
|
+
:rails_id => :id # Custom field created in iContact
|
175
|
+
:preferred_customer => :preferred? # Custom field
|
172
176
|
end
|
173
177
|
|
174
178
|
A few iContact-specific fields are exceptions, and have different autodiscovery names to avoid collisions with other attributes in your application:
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
1
|
+
0.2.1
|
data/acts_as_icontact.gemspec
CHANGED
@@ -2,17 +2,19 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{acts_as_icontact}
|
5
|
-
s.version = "0.2.
|
5
|
+
s.version = "0.2.1"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
8
|
s.authors = ["Stephen Eley"]
|
9
|
-
s.date = %q{2009-07-
|
9
|
+
s.date = %q{2009-07-27}
|
10
|
+
s.default_executable = %q{icontact}
|
10
11
|
s.description = %q{ActsAsIcontact connects Ruby applications with the iContact e-mail marketing service using the iContact API v2.0. Building on the RestClient gem, it offers two significant feature sets:
|
11
12
|
|
12
13
|
* Simple, consistent access to all resources in the iContact API; and
|
13
14
|
* Automatic synchronizing between ActiveRecord models and iContact contact lists for Rails applications.
|
14
15
|
}
|
15
16
|
s.email = %q{sfeley@gmail.com}
|
17
|
+
s.executables = ["icontact"]
|
16
18
|
s.extra_rdoc_files = [
|
17
19
|
"README.markdown"
|
18
20
|
]
|
@@ -24,18 +26,22 @@ Gem::Specification.new do |s|
|
|
24
26
|
"Rakefile",
|
25
27
|
"VERSION",
|
26
28
|
"acts_as_icontact.gemspec",
|
29
|
+
"bin/icontact",
|
27
30
|
"init.rb",
|
28
31
|
"lib/acts_as_icontact.rb",
|
29
32
|
"lib/acts_as_icontact/config.rb",
|
30
33
|
"lib/acts_as_icontact/connection.rb",
|
31
34
|
"lib/acts_as_icontact/exceptions.rb",
|
32
35
|
"lib/acts_as_icontact/rails.rb",
|
36
|
+
"lib/acts_as_icontact/rails/callbacks.rb",
|
33
37
|
"lib/acts_as_icontact/rails/macro.rb",
|
38
|
+
"lib/acts_as_icontact/rails/mappings.rb",
|
34
39
|
"lib/acts_as_icontact/resource.rb",
|
35
40
|
"lib/acts_as_icontact/resource_collection.rb",
|
36
41
|
"lib/acts_as_icontact/resources/account.rb",
|
37
42
|
"lib/acts_as_icontact/resources/client.rb",
|
38
43
|
"lib/acts_as_icontact/resources/contact.rb",
|
44
|
+
"lib/acts_as_icontact/resources/custom_field.rb",
|
39
45
|
"lib/acts_as_icontact/resources/list.rb",
|
40
46
|
"lib/acts_as_icontact/resources/message.rb",
|
41
47
|
"rails/init.rb",
|
@@ -43,11 +49,14 @@ Gem::Specification.new do |s|
|
|
43
49
|
"spec/connection_spec.rb",
|
44
50
|
"spec/examples/schema.rb",
|
45
51
|
"spec/rails_spec.rb",
|
52
|
+
"spec/rails_spec/callbacks_spec.rb",
|
53
|
+
"spec/rails_spec/mappings_spec.rb",
|
46
54
|
"spec/resource_collection_spec.rb",
|
47
55
|
"spec/resource_spec.rb",
|
48
56
|
"spec/resources/account_spec.rb",
|
49
57
|
"spec/resources/client_spec.rb",
|
50
58
|
"spec/resources/contact_spec.rb",
|
59
|
+
"spec/resources/custom_field_spec.rb",
|
51
60
|
"spec/resources/list_spec.rb",
|
52
61
|
"spec/resources/message_spec.rb",
|
53
62
|
"spec/spec.opts",
|
@@ -60,18 +69,21 @@ Gem::Specification.new do |s|
|
|
60
69
|
s.rdoc_options = ["--charset=UTF-8"]
|
61
70
|
s.require_paths = ["lib"]
|
62
71
|
s.rubyforge_project = %q{actsasicontact}
|
63
|
-
s.rubygems_version = %q{1.3.
|
72
|
+
s.rubygems_version = %q{1.3.5}
|
64
73
|
s.summary = %q{Automatic bridge between iContact e-mail marketing service and Rails ActiveRecord}
|
65
74
|
s.test_files = [
|
66
75
|
"spec/config_spec.rb",
|
67
76
|
"spec/connection_spec.rb",
|
68
77
|
"spec/examples/schema.rb",
|
78
|
+
"spec/rails_spec/callbacks_spec.rb",
|
79
|
+
"spec/rails_spec/mappings_spec.rb",
|
69
80
|
"spec/rails_spec.rb",
|
70
81
|
"spec/resource_collection_spec.rb",
|
71
82
|
"spec/resource_spec.rb",
|
72
83
|
"spec/resources/account_spec.rb",
|
73
84
|
"spec/resources/client_spec.rb",
|
74
85
|
"spec/resources/contact_spec.rb",
|
86
|
+
"spec/resources/custom_field_spec.rb",
|
75
87
|
"spec/resources/list_spec.rb",
|
76
88
|
"spec/resources/message_spec.rb",
|
77
89
|
"spec/spec_helper.rb",
|
data/bin/icontact
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.dirname(__FILE__) + "/../lib"
|
4
|
+
require 'acts_as_icontact'
|
5
|
+
require 'rubygems'
|
6
|
+
require 'readline'
|
7
|
+
require 'bond'
|
8
|
+
require 'bond/completion'
|
9
|
+
|
10
|
+
module ActsAsIcontact
|
11
|
+
# Lifted from: http://tagaholic.me/2009/07/23/mini-irb-and-mini-script-console.html
|
12
|
+
history_file = File.join(ENV["HOME"], '.icontact_history')
|
13
|
+
IO.readlines(history_file).each {|e| Readline::HISTORY << e.chomp } if File.exists?(history_file)
|
14
|
+
print "# ActsAsIcontact command line (type 'exit' to quit)\n"
|
15
|
+
while (input = Readline.readline('>> ', true)) != 'exit'
|
16
|
+
begin puts "=> #{eval(input).inspect}"; rescue Exception; puts "Error: #{$!}" end
|
17
|
+
end
|
18
|
+
File.open(history_file, 'w') {|f| f.write Readline::HISTORY.to_a.join("\n") }
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
exit!
|
@@ -4,18 +4,20 @@ Dir[File.join(File.dirname(__FILE__), 'rails', '*.rb')].sort.each do |path|
|
|
4
4
|
require "acts_as_icontact/rails/#{filename}"
|
5
5
|
end
|
6
6
|
|
7
|
-
module ActsAsIcontact
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
end
|
7
|
+
# module ActsAsIcontact
|
8
|
+
# module Rails
|
9
|
+
# module ClassMethods
|
10
|
+
# extend Mappings
|
11
|
+
# extend Macro
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
# end
|
14
15
|
|
15
16
|
if defined?(::ActiveRecord)
|
16
17
|
module ::ActiveRecord
|
17
18
|
class Base
|
18
|
-
extend ActsAsIcontact::Rails::ClassMethods
|
19
|
+
extend ActsAsIcontact::Rails::ClassMethods::Mappings
|
20
|
+
extend ActsAsIcontact::Rails::ClassMethods::Macro
|
19
21
|
end
|
20
22
|
end
|
21
23
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module ActsAsIcontact
|
2
|
+
module Rails
|
3
|
+
module Callbacks
|
4
|
+
|
5
|
+
protected
|
6
|
+
# Called after a new record has been saved. Creates a new contact in iContact.
|
7
|
+
def icontact_after_create
|
8
|
+
c = ActsAsIcontact::Contact.new
|
9
|
+
self.class.icontact_mappings.each do |rails, iContact|
|
10
|
+
if (value = self.send(rails))
|
11
|
+
ic = (iContact.to_s + '=').to_sym # Blah. This feels like it should be easier.
|
12
|
+
c.send(ic, value)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
if c.save
|
16
|
+
# Update with iContact fields returned
|
17
|
+
@icontact_in_progress = true
|
18
|
+
self.class.icontact_mappings.each do |rails, iContact|
|
19
|
+
unless (value = c.send(iContact)).blank?
|
20
|
+
r = (rails.to_s + '=').to_sym # Blah. This feels like it should be easier.
|
21
|
+
self.send(r, value)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
self.save
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -6,7 +6,20 @@ module ActsAsIcontact
|
|
6
6
|
# The core macro for ActsAsIcontact's Rails integration. Establishes callbacks to keep Rails models in
|
7
7
|
# sync with iContact. See the README for more on what it does.
|
8
8
|
def acts_as_icontact(options = {})
|
9
|
-
|
9
|
+
# Fail on exceptions?
|
10
|
+
@icontact_exception_on_failure = options.delete(:exception_on_failure) || false
|
11
|
+
|
12
|
+
# Combines :list and :lists parameters into one array
|
13
|
+
@icontact_default_lists = []
|
14
|
+
@icontact_default_lists << options.delete(:list) if options.has_key?(:list)
|
15
|
+
@icontact_default_lists += options.delete(:lists) if options.has_key?(:lists)
|
16
|
+
|
17
|
+
# Set up field mappings
|
18
|
+
set_mappings(options)
|
19
|
+
|
20
|
+
# If we haven't flaked out so far, we must be doing okay. Make magic happen.
|
21
|
+
include ActsAsIcontact::Rails::Callbacks
|
22
|
+
after_create :icontact_after_create
|
10
23
|
end
|
11
24
|
|
12
25
|
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module ActsAsIcontact
|
2
|
+
module Rails
|
3
|
+
module ClassMethods
|
4
|
+
module Mappings
|
5
|
+
|
6
|
+
ICONTACT_DEFAULT_MAPPINGS = {
|
7
|
+
:contactId => [:icontact_id, :icontactId],
|
8
|
+
:email => [:email, :email_address, :eMail, :emailAddress],
|
9
|
+
:firstName => [:firstName, :first_name, :fname],
|
10
|
+
:lastName => [:lastName, :last_name, :lname],
|
11
|
+
:street => [:street, :street1, :address, :address1],
|
12
|
+
:street2 => [:street2, :address2],
|
13
|
+
:city => [:city],
|
14
|
+
:state => [:state, :province, :state_or_province],
|
15
|
+
:postalCode => [:postalCode, :postal_code, :zipCode, :zip_code, :zip],
|
16
|
+
:phone => [:phone, :phoneNumber, :phone_number],
|
17
|
+
:fax => [:fax, :faxNumber, :fax_number],
|
18
|
+
:business => [:business, :company, :companyName, :company_name, :businessName, :business_name],
|
19
|
+
:status => [:icontact_status, :icontactStatus],
|
20
|
+
:createDate => [:icontact_created, :icontactCreated, :icontact_create_date, :icontactCreateDate],
|
21
|
+
:bounceCount => [:icontact_bounces, :icontactBounces, :icontact_bounce_count, :icontactBounceCount]
|
22
|
+
}
|
23
|
+
|
24
|
+
# A hash containing the final list of iContact-to-Rails mappings. The mappings take into account both
|
25
|
+
# analysis of existing field names and explicit mappings on the `acts_as_icontact` macro line.
|
26
|
+
def icontact_mappings
|
27
|
+
@icontact_mappings
|
28
|
+
end
|
29
|
+
|
30
|
+
# A two-element array indicating the association used to uniquely identify this record between Rails and iContact.
|
31
|
+
# The first element is the Rails field; the second element is the iContact field.
|
32
|
+
# First uses whatever field maps the contactId; if none, looks for a mapping from the Rails ID. Uses the email address
|
33
|
+
# mapping (which is required) as a last resort.
|
34
|
+
def icontact_identity_map
|
35
|
+
icontact_mappings.rassoc(:contactId) or icontact_mappings.assoc(:id) or icontact_mappings.rassoc(:email)
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
# Sets up the mapping hash from iContact fields to Rails fields.
|
41
|
+
def set_mappings(options)
|
42
|
+
@icontact_mappings = {}
|
43
|
+
set_default_mappings
|
44
|
+
set_custom_field_mappings
|
45
|
+
@icontact_mappings.merge!(options)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def set_default_mappings
|
50
|
+
ICONTACT_DEFAULT_MAPPINGS.each do |iContactField, candidates|
|
51
|
+
candidates.each do |candidate|
|
52
|
+
if rails_field?(candidate)
|
53
|
+
@icontact_mappings[candidate] = iContactField
|
54
|
+
break
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_custom_field_mappings
|
61
|
+
@icontact_custom_fields ||= CustomField.list
|
62
|
+
@icontact_custom_fields.each do |field|
|
63
|
+
f = field.to_sym
|
64
|
+
@icontact_mappings[f] = f if rails_field?(f)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def rails_field?(field)
|
69
|
+
column_names.include?(field.to_s) or instance_methods.include?(field)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -102,7 +102,7 @@ module ActsAsIcontact
|
|
102
102
|
@errors
|
103
103
|
end
|
104
104
|
|
105
|
-
# Returns
|
105
|
+
# Returns a resource or collection of resources.
|
106
106
|
def self.find(type, options={})
|
107
107
|
case type
|
108
108
|
when :first
|
@@ -111,6 +111,8 @@ module ActsAsIcontact
|
|
111
111
|
all(options)
|
112
112
|
when Integer
|
113
113
|
find_by_id(type)
|
114
|
+
when String
|
115
|
+
find_by_string(type) # Implemented in subclasses
|
114
116
|
else
|
115
117
|
raise ActsAsIcontact::QueryError, "Don't know how to find '#{type.to_s}'"
|
116
118
|
end
|
@@ -137,8 +139,10 @@ module ActsAsIcontact
|
|
137
139
|
def self.find_by_id(id)
|
138
140
|
response = self.connection[id].get
|
139
141
|
parsed = JSON.parse(response)
|
140
|
-
raise
|
142
|
+
raise ActsAsIcontact::QueryError, "iContact's response did not contain a #{resource_name}!" unless parsed[resource_name]
|
141
143
|
self.new(parsed[resource_name])
|
144
|
+
rescue RestClient::ResourceNotFound
|
145
|
+
raise ActsAsIcontact::QueryError, "The #{resource_name} with id #{id} could not be found"
|
142
146
|
end
|
143
147
|
|
144
148
|
# Two resources are identical if they have exactly the same property array.
|
@@ -196,6 +200,11 @@ module ActsAsIcontact
|
|
196
200
|
base[uri_component]
|
197
201
|
end
|
198
202
|
|
203
|
+
# Allows some subclasses to implement finding by a key identifier string. The base resource just throws an exception.
|
204
|
+
def self.find_by_string(value)
|
205
|
+
raise ActsAsIcontact::QueryError, "You cannot search on #{collection_name} with a string value!"
|
206
|
+
end
|
207
|
+
|
199
208
|
# The primary key field for this resource. Used on updates.
|
200
209
|
def self.primary_key
|
201
210
|
resource_name + "Id"
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module ActsAsIcontact
|
2
|
+
class CustomField < Resource
|
3
|
+
|
4
|
+
# Has a default fieldType of "text" and displayToUser of "false" if not overridden as options.
|
5
|
+
def initialize(properties={})
|
6
|
+
# Capture privateName as the ID field if it was passed in
|
7
|
+
@customFieldId = properties["privateName"] || properties[:privateName]
|
8
|
+
super({:fieldType => "text", :displayToUser => false}.merge(properties))
|
9
|
+
end
|
10
|
+
|
11
|
+
# Uses privateName as its ID in resource URLs. The custom field resource is just weird that way.
|
12
|
+
def id
|
13
|
+
@customFieldId
|
14
|
+
end
|
15
|
+
|
16
|
+
# privateName must not contain certain punctuation; fieldType must be "text" or "checkbox".
|
17
|
+
def validate_on_save(fields)
|
18
|
+
raise ActsAsIcontact::ValidationError, "privateName cannot contain spaces, quotes, slashes or brackets" if fields["privateName"] =~ /[\s\"\'\/\\\[\]]/
|
19
|
+
raise ActsAsIcontact::ValidationError, "fieldType must be 'text' or 'checkbox'" unless fields["fieldType"] =~ /^(text|checkbox)$/
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
# Searches on privateName. For this class it's the proper way to find by the primary key anyway.
|
24
|
+
def self.find_by_string(value)
|
25
|
+
find_by_id(value)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Requires privateName, displayToUser, fieldType
|
29
|
+
def self.required_on_create
|
30
|
+
super + %w(privateName displayToUser fieldType)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Treats displayToUser as a boolean field (accepts true and false)
|
34
|
+
def self.boolean_fields
|
35
|
+
super << "displayToUser"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Uses privateName as its primary key field
|
39
|
+
def self.primary_key
|
40
|
+
"privateName"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns an array listing all custom fields. This is very convenient for certain tasks (e.g. the mapping in our Rails integration).
|
44
|
+
def self.list
|
45
|
+
list = []
|
46
|
+
fields = self.all
|
47
|
+
fields.each {|f| list << f.privateName}
|
48
|
+
list
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -5,6 +5,11 @@ module ActsAsIcontact
|
|
5
5
|
ActsAsIcontact.client
|
6
6
|
end
|
7
7
|
|
8
|
+
# Searches on list name.
|
9
|
+
def self.find_by_string(value)
|
10
|
+
first(:name => value)
|
11
|
+
end
|
12
|
+
|
8
13
|
# Requires name, emailOwnerOnChange, welcomeOnManualAdd, welcomeOnSignupAdd, and welcomeMessageId.
|
9
14
|
def self.required_on_create
|
10
15
|
super << "name" << "emailOwnerOnChange" << "welcomeOnManualAdd" << "welcomeOnSignupAdd" << "welcomeMessageId"
|
data/spec/examples/schema.rb
CHANGED
@@ -5,9 +5,13 @@ ActiveRecord::Schema.define do
|
|
5
5
|
t.string "email", :null => false
|
6
6
|
t.string "icontact_status"
|
7
7
|
t.string "status"
|
8
|
-
t.
|
9
|
-
t.
|
10
|
-
t.string "
|
8
|
+
t.integer "icontact_id"
|
9
|
+
t.string "address"
|
10
|
+
t.string "state_or_province"
|
11
|
+
t.string "zip"
|
12
|
+
t.string "business"
|
13
|
+
t.datetime "icontactCreated"
|
14
|
+
t.integer "bounces"
|
11
15
|
t.string "custom_field"
|
12
16
|
t.datetime "created_at"
|
13
17
|
t.datetime "updated_at"
|
data/spec/rails_spec.rb
CHANGED
@@ -3,24 +3,9 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
3
3
|
require 'activerecord'
|
4
4
|
require 'rails/init'
|
5
5
|
|
6
|
-
#
|
7
|
-
|
8
|
-
|
9
|
-
# t.string "firstName"
|
10
|
-
# t.string "surname"
|
11
|
-
# t.string "email", :null => false
|
12
|
-
# t.string "icontact_status"
|
13
|
-
# t.string "status"
|
14
|
-
# t.string "icontact_id"
|
15
|
-
# t.datetime "icontact_created"
|
16
|
-
# t.string "bounces"
|
17
|
-
# t.string "custom_field"
|
18
|
-
# t.datetime "created_at"
|
19
|
-
# t.datetime "updated_at"
|
20
|
-
# end
|
21
|
-
|
22
|
-
acts_as_icontact
|
23
|
-
end
|
6
|
+
# Spreading out our Rails specs
|
7
|
+
Dir["#{File.dirname(__FILE__)}/rails_spec/*.rb"].each {|f| load f}
|
8
|
+
|
24
9
|
|
25
10
|
describe "Rails integration" do
|
26
11
|
before(:all) do
|
@@ -28,9 +13,70 @@ describe "Rails integration" do
|
|
28
13
|
:schema => File.dirname(__FILE__) + '/examples/schema.rb'
|
29
14
|
end
|
30
15
|
|
31
|
-
|
32
|
-
|
33
|
-
@
|
16
|
+
before(:each) do
|
17
|
+
# Create each Person class in a different module so that we can try different permutations.
|
18
|
+
@module = Module.new do
|
19
|
+
# Dummy model - comes from spec/examples/schema.rb and using the nullDB adapter
|
20
|
+
class Person < ActiveRecord::Base
|
21
|
+
# create_table "people", :force => true do |t|
|
22
|
+
# t.string "firstName"
|
23
|
+
# t.string "surname"
|
24
|
+
# t.string "email", :null => false
|
25
|
+
# t.string "icontact_status"
|
26
|
+
# t.string "status"
|
27
|
+
# t.string "icontact_id"
|
28
|
+
# t.string "address"
|
29
|
+
# t.string "state_or_province"
|
30
|
+
# t.string "zip"
|
31
|
+
# t.string "business"
|
32
|
+
# t.datetime "icontactCreated"
|
33
|
+
# t.string "bounces"
|
34
|
+
# t.string "custom_field"
|
35
|
+
# t.datetime "created_at"
|
36
|
+
# t.datetime "updated_at"
|
37
|
+
# end
|
38
|
+
|
39
|
+
# Fake out ActiveRecord's column introspection
|
40
|
+
def self.name
|
41
|
+
"Person"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
@class = @module.module_eval("Person")
|
48
|
+
end
|
49
|
+
|
50
|
+
it "allows the acts_as_icontact macro method" do
|
51
|
+
@class.should respond_to(:acts_as_icontact)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "sets whether to throw exceptions on failure" do
|
55
|
+
@class.acts_as_icontact :exception_on_failure => true
|
56
|
+
@class.instance_variable_get(:@icontact_exception_on_failure).should be_true
|
57
|
+
end
|
58
|
+
|
59
|
+
it "defaults to NOT setting exceptions on failure" do
|
60
|
+
@class.acts_as_icontact
|
61
|
+
@class.instance_variable_get(:@icontact_exception_on_failure).should be_false
|
62
|
+
end
|
63
|
+
|
64
|
+
it "sets default lists from a single list in the options" do
|
65
|
+
@class.acts_as_icontact :list => 444444
|
66
|
+
@class.instance_variable_get(:@icontact_default_lists).should == [444444]
|
67
|
+
end
|
68
|
+
|
69
|
+
it "sets default lists from an array of lists in the options" do
|
70
|
+
@class.acts_as_icontact :lists => [444555, "Test List 3"]
|
71
|
+
@class.instance_variable_get(:@icontact_default_lists).should == [444555, "Test List 3"]
|
72
|
+
end
|
73
|
+
|
74
|
+
it "sets default lists from both a list _and_ an array of lists in the options" do
|
75
|
+
@class.acts_as_icontact :lists => [444555, "Test List 3"], :list => 444444
|
76
|
+
@class.instance_variable_get(:@icontact_default_lists).should == [444444, 444555, "Test List 3"]
|
34
77
|
end
|
35
78
|
|
79
|
+
include Mappings
|
80
|
+
include Callbacks
|
81
|
+
|
36
82
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
share_as :Callbacks do
|
2
|
+
context "callbacks" do
|
3
|
+
before(:each) do
|
4
|
+
@class.acts_as_icontact :list => "First Test", :surname => :lastName
|
5
|
+
@person = @class.new(:firstName => "John", :surname => "Smith", :email => "john@example.org")
|
6
|
+
end
|
7
|
+
|
8
|
+
it "will create a new contact after record creation" do
|
9
|
+
# ActsAsIcontact::Contact.any_instance.expects(:save).returns(true)
|
10
|
+
@person.save
|
11
|
+
end
|
12
|
+
|
13
|
+
it "updates the Person with the results of the contact save" do
|
14
|
+
@person.save
|
15
|
+
@person.icontact_id.should == 333444
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
share_as :Mappings do
|
2
|
+
|
3
|
+
context "mappings" do
|
4
|
+
before(:each) do
|
5
|
+
@class.acts_as_icontact :list => 444444, :surname => :lastName
|
6
|
+
end
|
7
|
+
|
8
|
+
it "uses any non-option keys as field mappings" do
|
9
|
+
@class.icontact_mappings[:surname].should == :lastName
|
10
|
+
end
|
11
|
+
|
12
|
+
it "does not map option keys" do
|
13
|
+
@class.icontact_mappings.should_not have_key(:list)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "maps fields it can find from the default list" do
|
17
|
+
@class.icontact_mappings[:firstName].should == :firstName
|
18
|
+
end
|
19
|
+
|
20
|
+
it "maps second choices when it can find them" do
|
21
|
+
@class.icontact_mappings[:zip].should == :postalCode
|
22
|
+
end
|
23
|
+
|
24
|
+
it "maps the icontact_ exception names" do
|
25
|
+
@class.icontact_mappings[:icontactCreated].should == :createDate
|
26
|
+
end
|
27
|
+
|
28
|
+
it "does not map the default form of exception names" do
|
29
|
+
@class.icontact_mappings.should_not have_key(:status)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "maps icontact_status" do
|
33
|
+
@class.icontact_mappings[:icontact_status].should == :status
|
34
|
+
end
|
35
|
+
|
36
|
+
it "maps the address field" do
|
37
|
+
@class.icontact_mappings[:address].should == :street
|
38
|
+
end
|
39
|
+
|
40
|
+
it "maps custom fields" do
|
41
|
+
@class.icontact_mappings[:custom_field].should == :custom_field
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "identity mapping" do
|
46
|
+
it "looks for contactId first" do
|
47
|
+
@class.acts_as_icontact
|
48
|
+
@class.icontact_identity_map.should == [:icontact_id, :contactId]
|
49
|
+
end
|
50
|
+
|
51
|
+
it "looks for a Rails ID custom field second" do
|
52
|
+
@class.acts_as_icontact :icontact_id => nil, :id => :test_field
|
53
|
+
@class.icontact_identity_map.should == [:id, :test_field]
|
54
|
+
end
|
55
|
+
|
56
|
+
it "uses email as last resort" do
|
57
|
+
@class.acts_as_icontact :icontact_id => nil
|
58
|
+
@class.icontact_identity_map.should == [:email, :email]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/spec/resource_spec.rb
CHANGED
@@ -80,6 +80,11 @@ describe ActsAsIcontact::Resource do
|
|
80
80
|
a.too.should == "sar"
|
81
81
|
end
|
82
82
|
|
83
|
+
it "can attempt to find a single resource by string identifier" do
|
84
|
+
ActsAsIcontact::Resource.expects(:find_by_string).returns(nil)
|
85
|
+
ActsAsIcontact::Resource.find("bar")
|
86
|
+
end
|
87
|
+
|
83
88
|
end
|
84
89
|
|
85
90
|
it "knows its properties" do
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe ActsAsIcontact::CustomField do
|
4
|
+
it "requires privateName, displayToUser, fieldType" do
|
5
|
+
cf = ActsAsIcontact::CustomField.new(:displayToUser => nil, :fieldType => nil)
|
6
|
+
lambda{cf.save}.should raise_error(ActsAsIcontact::ValidationError, "Missing required fields: privateName, displayToUser, fieldType")
|
7
|
+
end
|
8
|
+
|
9
|
+
it "uses true and false to assign boolean fields" do
|
10
|
+
cf = ActsAsIcontact::CustomField.new
|
11
|
+
cf.displayToUser = true
|
12
|
+
cf.instance_variable_get(:@properties)["displayToUser"].should == 1
|
13
|
+
end
|
14
|
+
|
15
|
+
it "defaults displayToUser to false" do
|
16
|
+
cf = ActsAsIcontact::CustomField.new
|
17
|
+
cf.displayToUser.should be_false
|
18
|
+
end
|
19
|
+
|
20
|
+
it "defaults fieldType to text" do
|
21
|
+
cf = ActsAsIcontact::CustomField.new
|
22
|
+
cf.fieldType.should == "text"
|
23
|
+
end
|
24
|
+
|
25
|
+
it "uses privateName as its primary key" do
|
26
|
+
ActsAsIcontact::CustomField.primary_key.should == "privateName"
|
27
|
+
end
|
28
|
+
|
29
|
+
it "uses privateName as its resource ID" do
|
30
|
+
cf = ActsAsIcontact::CustomField.new(:privateName => "blah")
|
31
|
+
cf.connection.url.should =~ /blah/
|
32
|
+
end
|
33
|
+
|
34
|
+
it "can find fields by string" do
|
35
|
+
cf = ActsAsIcontact::CustomField.find("test_field")
|
36
|
+
cf.publicName.should == "Test Field"
|
37
|
+
end
|
38
|
+
|
39
|
+
it "validates the format of the privateName" do
|
40
|
+
cf = ActsAsIcontact::CustomField.new(:privateName => "This isn't a valid [privateName].")
|
41
|
+
lambda{cf.save}.should raise_error(ActsAsIcontact::ValidationError, "privateName cannot contain spaces, quotes, slashes or brackets")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "validates the value of fieldType" do
|
45
|
+
cf = ActsAsIcontact::CustomField.new(:fieldType => "radio", :privateName => "test")
|
46
|
+
lambda{cf.save}.should raise_error(ActsAsIcontact::ValidationError, "fieldType must be 'text' or 'checkbox'")
|
47
|
+
end
|
48
|
+
|
49
|
+
it "can easily retrieve a list of custom field names" do
|
50
|
+
ActsAsIcontact::CustomField.list.should == %w(test_field custom_field)
|
51
|
+
end
|
52
|
+
end
|
data/spec/resources/list_spec.rb
CHANGED
@@ -21,6 +21,11 @@ describe ActsAsIcontact::List do
|
|
21
21
|
l.welcomeOnManualAdd.should be_true
|
22
22
|
end
|
23
23
|
|
24
|
+
it "can find a list by name" do
|
25
|
+
l = ActsAsIcontact::List.find("First Test")
|
26
|
+
l.id.should == 444444
|
27
|
+
end
|
28
|
+
|
24
29
|
context "associations" do
|
25
30
|
# Create one good list
|
26
31
|
before(:each) do
|
@@ -30,6 +30,7 @@ FakeWeb.register_uri(:get, "#{i}/a/111111/c?limit=500", :body => %q<{"clientfold
|
|
30
30
|
|
31
31
|
# Contacts
|
32
32
|
FakeWeb.register_uri(:get, "#{ic}/contacts?limit=1&status=total&firstName=John&lastName=Test", :body => %q<{"contacts":[{"email":"john@example.org","firstName":"John","lastName":"Test","status":"normal","contactId":"333333","createDate":"2009-07-24 01:00:00"}]}>)
|
33
|
+
FakeWeb.register_uri(:post, "#{ic}/contacts", :body => %q<{"contacts":[{"email":"john@example.org","firstName":"John","lastName":"Smith","status":"normal","contactId":"333444","createDate":"2009-07-24 01:00:00","street":"","street2":"","prefix":"","suffix":"","fax":"","phone":"","city":"","state":"","postalCode":"","bounceCount":0,"custom_field":"","test_field":"","business":""}]}>)
|
33
34
|
|
34
35
|
# Lists
|
35
36
|
FakeWeb.register_uri(:get, "#{ic}/lists?limit=1&name=First%20Test", :body => %q<{"lists":[{"listId":"444444","name":"First Test","emailOwnerOnChange":"0","welcomeOnManualAdd":"0","welcomeOnSignupAdd":"0","welcomeMessageId":"555555","description":"Just a test list."}]}>)
|
@@ -40,3 +41,7 @@ FakeWeb.register_uri(:get, "#{ic}/messages/555555", :body => %q<{"message":{"mes
|
|
40
41
|
|
41
42
|
#### Test message for associations originating from Message spec
|
42
43
|
FakeWeb.register_uri(:get, "#{ic}/messages?limit=1&subject=Test%20Message", :body => %q<{"messages":[{"messageId":"666666","subject":"Test Message","messageType":"normal","textBody":"Hi there!\nThis is just a test.","htmlBody":"<p><b>Hi there!</b></p><p>This is just a <i>test.</i></p>","createDate":"20090725 14:53:33"}]}>)
|
44
|
+
|
45
|
+
# CustomField
|
46
|
+
FakeWeb.register_uri(:get, "#{ic}/customfields?limit=500", :body => %q<{"customfields":[{"privateName":"test_field","publicName":"Test Field","displayToUser":"0","fieldType":"text"},{"privateName":"custom_field","publicName":"This is for the Rails integration specs","displayToUser":1,"fieldType":"text"}],"total":2}>)
|
47
|
+
FakeWeb.register_uri(:get, "#{ic}/customfields/test_field", :body => %q<{"customfield":{"privateName":"test_field","publicName":"Test Field","displayToUser":"0","fieldType":"text"}}>)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts_as_icontact
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen Eley
|
@@ -9,8 +9,8 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-07-
|
13
|
-
default_executable:
|
12
|
+
date: 2009-07-27 00:00:00 -04:00
|
13
|
+
default_executable: icontact
|
14
14
|
dependencies: []
|
15
15
|
|
16
16
|
description: |
|
@@ -20,8 +20,8 @@ description: |
|
|
20
20
|
* Automatic synchronizing between ActiveRecord models and iContact contact lists for Rails applications.
|
21
21
|
|
22
22
|
email: sfeley@gmail.com
|
23
|
-
executables:
|
24
|
-
|
23
|
+
executables:
|
24
|
+
- icontact
|
25
25
|
extensions: []
|
26
26
|
|
27
27
|
extra_rdoc_files:
|
@@ -34,18 +34,22 @@ files:
|
|
34
34
|
- Rakefile
|
35
35
|
- VERSION
|
36
36
|
- acts_as_icontact.gemspec
|
37
|
+
- bin/icontact
|
37
38
|
- init.rb
|
38
39
|
- lib/acts_as_icontact.rb
|
39
40
|
- lib/acts_as_icontact/config.rb
|
40
41
|
- lib/acts_as_icontact/connection.rb
|
41
42
|
- lib/acts_as_icontact/exceptions.rb
|
42
43
|
- lib/acts_as_icontact/rails.rb
|
44
|
+
- lib/acts_as_icontact/rails/callbacks.rb
|
43
45
|
- lib/acts_as_icontact/rails/macro.rb
|
46
|
+
- lib/acts_as_icontact/rails/mappings.rb
|
44
47
|
- lib/acts_as_icontact/resource.rb
|
45
48
|
- lib/acts_as_icontact/resource_collection.rb
|
46
49
|
- lib/acts_as_icontact/resources/account.rb
|
47
50
|
- lib/acts_as_icontact/resources/client.rb
|
48
51
|
- lib/acts_as_icontact/resources/contact.rb
|
52
|
+
- lib/acts_as_icontact/resources/custom_field.rb
|
49
53
|
- lib/acts_as_icontact/resources/list.rb
|
50
54
|
- lib/acts_as_icontact/resources/message.rb
|
51
55
|
- rails/init.rb
|
@@ -53,11 +57,14 @@ files:
|
|
53
57
|
- spec/connection_spec.rb
|
54
58
|
- spec/examples/schema.rb
|
55
59
|
- spec/rails_spec.rb
|
60
|
+
- spec/rails_spec/callbacks_spec.rb
|
61
|
+
- spec/rails_spec/mappings_spec.rb
|
56
62
|
- spec/resource_collection_spec.rb
|
57
63
|
- spec/resource_spec.rb
|
58
64
|
- spec/resources/account_spec.rb
|
59
65
|
- spec/resources/client_spec.rb
|
60
66
|
- spec/resources/contact_spec.rb
|
67
|
+
- spec/resources/custom_field_spec.rb
|
61
68
|
- spec/resources/list_spec.rb
|
62
69
|
- spec/resources/message_spec.rb
|
63
70
|
- spec/spec.opts
|
@@ -89,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
96
|
requirements: []
|
90
97
|
|
91
98
|
rubyforge_project: actsasicontact
|
92
|
-
rubygems_version: 1.3.
|
99
|
+
rubygems_version: 1.3.5
|
93
100
|
signing_key:
|
94
101
|
specification_version: 3
|
95
102
|
summary: Automatic bridge between iContact e-mail marketing service and Rails ActiveRecord
|
@@ -97,12 +104,15 @@ test_files:
|
|
97
104
|
- spec/config_spec.rb
|
98
105
|
- spec/connection_spec.rb
|
99
106
|
- spec/examples/schema.rb
|
107
|
+
- spec/rails_spec/callbacks_spec.rb
|
108
|
+
- spec/rails_spec/mappings_spec.rb
|
100
109
|
- spec/rails_spec.rb
|
101
110
|
- spec/resource_collection_spec.rb
|
102
111
|
- spec/resource_spec.rb
|
103
112
|
- spec/resources/account_spec.rb
|
104
113
|
- spec/resources/client_spec.rb
|
105
114
|
- spec/resources/contact_spec.rb
|
115
|
+
- spec/resources/custom_field_spec.rb
|
106
116
|
- spec/resources/list_spec.rb
|
107
117
|
- spec/resources/message_spec.rb
|
108
118
|
- spec/spec_helper.rb
|