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 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. Right now there's only one option:
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
- `default_lists` -- _The name or ID number of a list to subscribe new contacts to automatically, or an array of said names or numbers_
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 :default_lists => ['New Customers', 'All Users'] # Puts new contact on two lists
166
- :given_name => :firstName, # Key is Rails field, value is iContact field
167
- :family_name => :lastName,
168
- :address1 => :street,
169
- :address2 => :street2,
170
- :id => :rails_id, # Custom field created in iContact
171
- :preferred? => :preferred_customer # Custom field
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.0
1
+ 0.2.1
@@ -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.0"
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-26}
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.4}
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
- module Rails
9
- module ClassMethods
10
- include Macro
11
- end
12
- end
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
- true
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 an array of resources starting at the base.
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 ActsAsiContact::QueryError, "iContact's response did not contain a #{resource_name}!" unless parsed[resource_name]
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"
@@ -3,7 +3,7 @@ module ActsAsIcontact
3
3
 
4
4
  # Email is required
5
5
  def self.required_on_create
6
- super << ['email']
6
+ super << 'email'
7
7
  end
8
8
 
9
9
  # Derived from clientFolder
@@ -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"
@@ -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.string "icontact_id"
9
- t.datetime "icontact_created"
10
- t.string "bounces"
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
- # Dummy model
7
- class Person < ActiveRecord::Base
8
- # create_table "people", :force => true do |t|
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
- it "relies on a model with an email address" do
32
- @person = Person.new(:email => "john@example.org")
33
- @person.email.should == "john@example.org"
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
@@ -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
@@ -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.0
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-26 00:00:00 -04:00
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.4
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