acts_as_icontact 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.2
1
+ 0.4.0
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{acts_as_icontact}
8
- s.version = "0.3.2"
8
+ s.version = "0.4.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Stephen Eley"]
12
- s.date = %q{2009-08-08}
12
+ s.date = %q{2009-08-10}
13
13
  s.default_executable = %q{icontact}
14
14
  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:
15
15
 
@@ -32,6 +32,8 @@ Gem::Specification.new do |s|
32
32
  "bin/icontact",
33
33
  "init.rb",
34
34
  "lib/acts_as_icontact.rb",
35
+ "lib/acts_as_icontact/command_line/completion.rb",
36
+ "lib/acts_as_icontact/command_line/variables.rb",
35
37
  "lib/acts_as_icontact/config.rb",
36
38
  "lib/acts_as_icontact/connection.rb",
37
39
  "lib/acts_as_icontact/exceptions.rb",
@@ -46,6 +48,7 @@ Gem::Specification.new do |s|
46
48
  "lib/acts_as_icontact/resources/campaign.rb",
47
49
  "lib/acts_as_icontact/resources/client.rb",
48
50
  "lib/acts_as_icontact/resources/contact.rb",
51
+ "lib/acts_as_icontact/resources/contact_history.rb",
49
52
  "lib/acts_as_icontact/resources/custom_field.rb",
50
53
  "lib/acts_as_icontact/resources/list.rb",
51
54
  "lib/acts_as_icontact/resources/message.rb",
@@ -62,6 +65,7 @@ Gem::Specification.new do |s|
62
65
  "spec/resources/account_spec.rb",
63
66
  "spec/resources/campaign_spec.rb",
64
67
  "spec/resources/clientfolder_spec.rb",
68
+ "spec/resources/contact_history_spec.rb",
65
69
  "spec/resources/contact_spec.rb",
66
70
  "spec/resources/custom_field_spec.rb",
67
71
  "spec/resources/list_spec.rb",
@@ -91,6 +95,7 @@ Gem::Specification.new do |s|
91
95
  "spec/resources/account_spec.rb",
92
96
  "spec/resources/campaign_spec.rb",
93
97
  "spec/resources/clientfolder_spec.rb",
98
+ "spec/resources/contact_history_spec.rb",
94
99
  "spec/resources/contact_spec.rb",
95
100
  "spec/resources/custom_field_spec.rb",
96
101
  "spec/resources/list_spec.rb",
@@ -1,19 +1,30 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  $:.unshift File.dirname(__FILE__) + "/../lib"
4
- require 'acts_as_icontact'
5
4
  require 'rubygems'
5
+ require 'acts_as_icontact'
6
6
  require 'readline'
7
- require 'bond'
8
- require 'bond/completion'
7
+ require 'acts_as_icontact/command_line/completion'
8
+ require 'acts_as_icontact/command_line/variables'
9
9
 
10
10
  module ActsAsIcontact
11
+
11
12
  # Lifted from: http://tagaholic.me/2009/07/23/mini-irb-and-mini-script-console.html
12
13
  history_file = File.join(ENV["HOME"], '.icontact_history')
13
14
  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("\n>> ", true)) != 'exit'
16
- begin puts "=> #{eval(input).inspect}"; rescue Exception; puts "Error: #{$!}" end
15
+ print "# ActsAsIcontact command line (type 'quit' or 'exit' to quit)\n"
16
+ loop do
17
+ case input = Readline.readline("\n>> ", true)
18
+ when /^(exit|quit)$/
19
+ break
20
+ when /^[a-z][\w\d_]*\s*=/ # Keep local variable assignments from becoming black holes
21
+ input = "self." + input
22
+ end
23
+ begin
24
+ puts "=> #{eval(input).inspect}"
25
+ rescue Exception
26
+ puts "Error: #{$!}"
27
+ end
17
28
  end
18
29
  File.open(history_file, 'w') {|f| f.write Readline::HISTORY.to_a.join("\n") }
19
30
 
@@ -1,6 +1,5 @@
1
1
  require 'rubygems'
2
2
  require 'rest_client'
3
- require 'json'
4
3
 
5
4
  $LOAD_PATH.unshift(File.dirname(__FILE__))
6
5
  require 'acts_as_icontact/exceptions'
@@ -0,0 +1,50 @@
1
+ require 'bond'
2
+
3
+ module Bond
4
+
5
+ module Actions
6
+ # ActsAsIcontact resource classes
7
+ def icontact_classes(input)
8
+ ActsAsIcontact::Resource.subclasses.map{|c| c.sub(/ActsAsIcontact\:\:/,'')}
9
+ end
10
+
11
+
12
+ # ActsAsIcontact resource properties
13
+ def icontact_properties(input)
14
+ receiver = ActsAsIcontact.instance_eval(input.matched[1])
15
+ if receiver.respond_to?(:property_names)
16
+ (receiver.property_names + receiver.methods - Object.methods).sort
17
+ else
18
+ (receiver.methods - Object.methods).sort
19
+ end
20
+ end
21
+
22
+ end
23
+ debrief(:eval_binding => binding)
24
+ debrief(:default_search => :ignore_case)
25
+ debrief(:default_mission => :icontact_classes)
26
+
27
+ end
28
+
29
+ # Complete ActsAsIcontact resources
30
+ Bond.complete(:on=>/([A-Z][^.\s]*)+$/, :action=>:icontact_classes)
31
+
32
+ # ActsAsIcontact resource class methods
33
+ Bond.complete(:on=>/([A-Z][^.\s]*)\.([^.\s]*)$/, :search => false) do |input|
34
+ receiver = ActsAsIcontact.const_get(input.matched[1].to_sym)
35
+ methods = (receiver.methods - Class.methods).sort
36
+ methods.grep(/^#{input.matched[2]}/i).collect{|m| "#{input.matched[1]}.#{m}"}
37
+ end
38
+
39
+
40
+ # ActsAsIcontact resource properties
41
+ Bond.complete(:on=>/([^.\s]+)\.([^.\s]*)$/, :search => false) do |input|
42
+ receiver = ActsAsIcontact.instance_eval(input.matched[1])
43
+ if receiver.respond_to?(:property_names)
44
+ methods = (receiver.property_names + receiver.methods - Object.methods).sort
45
+ else
46
+ methods = (receiver.methods - Object.methods).sort
47
+ end
48
+ methods.grep(/^#{input.matched[2]}/i).collect{|m| "#{input.matched[1]}.#{m}"}
49
+ end
50
+
@@ -0,0 +1,16 @@
1
+ module ActsAsIcontact
2
+
3
+ # Allows local variables to be set in the command line client. :nodoc:
4
+ def self.method_missing(method, *params)
5
+ @variables ||= {}
6
+ variable = method.to_s
7
+ if variable =~ /(.*)=$/ # It's a variable assignment
8
+ @variables[variable.sub(/=$/,'')] = params[0]
9
+ @variables
10
+ else
11
+ @variables[variable]
12
+ end
13
+ end
14
+
15
+ end
16
+
@@ -8,9 +8,12 @@ module ActsAsIcontact
8
8
  # Thrown before saving if iContact validation rules are not met.
9
9
  class ValidationError < StandardError; end
10
10
 
11
+ # Thrown by attempts to save a read-only resource.
12
+ class ReadOnlyError < StandardError; end
13
+
11
14
  # Thrown when a resource calls save! and fails. Contains the +.errors+ array from
12
15
  # the resource.
13
- class RecordNotSaved < StandardError
16
+ class SaveError < StandardError
14
17
  attr_reader :errors
15
18
 
16
19
  def initialize(errors = [])
@@ -1,6 +1,5 @@
1
1
  require 'activesupport'
2
2
  require 'uri'
3
- require 'YAML'
4
3
 
5
4
  module ActsAsIcontact
6
5
  # Base class for shared functionality between iContact resources. Supports getting, finding, saving,
@@ -87,10 +86,10 @@ module ActsAsIcontact
87
86
  false
88
87
  end
89
88
 
90
- # Like +save+, but raises an ActsAsIcontact::RecordNotSaved exception if the save
89
+ # Like +save+, but raises an ActsAsIcontact::SaveError exception if the save
91
90
  # failed. The exception message contains the first error from iContact.
92
91
  def save!
93
- save or raise ActsAsIcontact::RecordNotSaved.new(errors)
92
+ save or raise ActsAsIcontact::SaveError.new(errors)
94
93
  end
95
94
 
96
95
  # The first message from the +errors+ array.
@@ -156,6 +155,11 @@ module ActsAsIcontact
156
155
  properties.to_yaml
157
156
  end
158
157
 
158
+ # An array of all defined property keys for this resource.
159
+ def property_names
160
+ properties.symbolize_keys.keys
161
+ end
162
+
159
163
  protected
160
164
  # The minimum set of fields that must be sent back to iContact on an update.
161
165
  # Includes any fields that changed or were added, the primary key, and anything
@@ -2,9 +2,10 @@ module ActsAsIcontact
2
2
  class ResourceCollection < Enumerator
3
3
  attr_reader :total, :retrieved, :offset, :collection_name
4
4
 
5
- def initialize(klass, collection, forwardTo=nil)
5
+ def initialize(klass, collection, options={})
6
6
  @klass = klass
7
- @forwardTo = forwardTo
7
+ @forwardTo = options.delete(:forwardTo)
8
+ @parent = options.delete(:parent)
8
9
 
9
10
  @collection_name = klass.collection_name
10
11
  @collection = collection[collection_name]
@@ -48,6 +49,10 @@ module ActsAsIcontact
48
49
 
49
50
  private
50
51
  def resource(properties)
52
+ # If this is a subresource, include the parent object as a property
53
+ properties.merge!(:parent => @parent) if @parent
54
+
55
+ # "Forward to" is used to link Contacts and Lists via Subscriptions in a has_many :through
51
56
  if @forwardTo
52
57
  id = @forwardTo.primary_key
53
58
  @forwardTo.find(properties[id])
@@ -23,5 +23,9 @@ module ActsAsIcontact
23
23
  s.save
24
24
  end
25
25
 
26
+ # Returns a collection of ContactHistory resources for this contact. The usual iContact search options (limit, offset, search terms, etc.) can be passed.
27
+ def history(options={})
28
+ ActsAsIcontact::ContactHistory.scoped_find(self, options)
29
+ end
26
30
  end
27
31
  end
@@ -0,0 +1,57 @@
1
+ module ActsAsIcontact
2
+ # The read-only list of actions attached to every Contact. Because of this intrinsic association, the usual #find methods don't
3
+ # work; contact history _must_ be obtained using the individual contact's #history method.
4
+ # Property updates and saving are also prohibited (returning a ReadOnlyError exception.)
5
+ class ContactHistory < Resource
6
+ attr_reader :parent
7
+ alias_method :contact, :parent
8
+
9
+ # Should only be called by ResourceCollection. Raises an exception if a parent object is not passed.
10
+ def initialize(properties={})
11
+ @parent = properties.delete(:parent) or raise ActsAsIcontact::ValidationError, "Contact History requires a Contact"
12
+ super
13
+ end
14
+
15
+ # Properties of this class are read-only.
16
+ def method_missing(method, *params)
17
+ raise ActsAsIcontact::ReadOnlyError, "Contact History is read-only!" if method.to_s =~ /(.*)=$/
18
+ super
19
+ end
20
+
21
+
22
+ # Returns the ContactHistory collection for the passed contact. Takes the usual iContact search parameters.
23
+ def self.scoped_find(parent, options = {})
24
+ query_options = default_options.merge(options)
25
+ validate_options(query_options)
26
+ uri_extension = uri_component + build_query(query_options)
27
+ response = parent.connection[uri_extension].get
28
+ parsed = JSON.parse(response)
29
+ ResourceCollection.new(self, parsed, :parent => parent)
30
+ end
31
+
32
+
33
+ class <<self
34
+ # Replace all search methods with an exception
35
+ def cannot_query(*arguments)
36
+ raise ActsAsIcontact::QueryError, "Contact History must be obtained using the contact.history method."
37
+ end
38
+ alias_method :all, :cannot_query
39
+ alias_method :first, :cannot_query
40
+ alias_method :find, :cannot_query
41
+ end
42
+
43
+ # Replace save methods with an exception
44
+ def cannot_save(*arguments)
45
+ raise ActsAsIcontact::ReadOnlyError, "Contact History is read-only!"
46
+ end
47
+ alias_method :save, :cannot_save
48
+ alias_method :save!, :cannot_save
49
+
50
+ protected
51
+ # An oddball resource class; iContact's URL for it is 'actions', not 'contact_histories'.
52
+ def self.resource_name
53
+ 'action'
54
+ end
55
+
56
+ end
57
+ end
@@ -35,7 +35,7 @@ module ActsAsIcontact
35
35
  query_options = default_options.merge(options)
36
36
  validate_options(query_options)
37
37
  result = query_collection(query_options)
38
- ResourceCollection.new(self, result, forwardTo)
38
+ ResourceCollection.new(self, result, :forwardTo => forwardTo)
39
39
  end
40
40
 
41
41
  # Returns a collection of all contacts matching the query. Unfortunately, this has to be performed by looking up
@@ -220,7 +220,7 @@ describe ActsAsIcontact::Resource do
220
220
  end
221
221
 
222
222
  it "throws an exception with a bang" do
223
- lambda{@bad.save!}.should raise_error(ActsAsIcontact::RecordNotSaved,"You did not provide a foo. foo is a required field. Please provide a foo")
223
+ lambda{@bad.save!}.should raise_error(ActsAsIcontact::SaveError,"You did not provide a foo. foo is a required field. Please provide a foo")
224
224
  end
225
225
  end
226
226
 
@@ -244,7 +244,7 @@ describe ActsAsIcontact::Resource do
244
244
  end
245
245
 
246
246
  it "throws an exception with a bang" do
247
- lambda{@bad.save!}.should raise_error(ActsAsIcontact::RecordNotSaved,"You did not provide a clue. Clue is a required field. Please provide a clue")
247
+ lambda{@bad.save!}.should raise_error(ActsAsIcontact::SaveError,"You did not provide a clue. Clue is a required field. Please provide a clue")
248
248
  end
249
249
  end
250
250
 
@@ -324,7 +324,7 @@ describe ActsAsIcontact::Resource do
324
324
  end
325
325
 
326
326
  it "throws an exception with a bang" do
327
- lambda{@res.save!}.should raise_error(ActsAsIcontact::RecordNotSaved,"You did not provide a foo. foo is a required field. Please provide a foo")
327
+ lambda{@res.save!}.should raise_error(ActsAsIcontact::SaveError,"You did not provide a foo. foo is a required field. Please provide a foo")
328
328
  end
329
329
  end
330
330
 
@@ -349,7 +349,7 @@ describe ActsAsIcontact::Resource do
349
349
  end
350
350
 
351
351
  it "throws an exception with a bang" do
352
- lambda{@res.save!}.should raise_error(ActsAsIcontact::RecordNotSaved,"You did not provide a clue. Clue is a required field. Please provide a clue")
352
+ lambda{@res.save!}.should raise_error(ActsAsIcontact::SaveError,"You did not provide a clue. Clue is a required field. Please provide a clue")
353
353
  end
354
354
  end
355
355
 
@@ -0,0 +1,36 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe ActsAsIcontact::ContactHistory do
4
+ before(:each) do
5
+ @contact = ActsAsIcontact::Contact.find(333333)
6
+ @this = @contact.history.first
7
+ end
8
+
9
+ it "knows its contact" do
10
+ @this.contact.email.should == "john@example.org"
11
+ end
12
+
13
+ it "cannot be altered" do
14
+ lambda{@this.actor = 333333}.should raise_error(ActsAsIcontact::ReadOnlyError, "Contact History is read-only!")
15
+ end
16
+
17
+ it "cannot be saved" do
18
+ lambda{@this.save}.should raise_error(ActsAsIcontact::ReadOnlyError, "Contact History is read-only!")
19
+ end
20
+
21
+ it "does not allow .find" do
22
+ lambda{ActsAsIcontact::ContactHistory.find(:all)}.should raise_error(ActsAsIcontact::QueryError, "Contact History must be obtained using the contact.history method.")
23
+ end
24
+
25
+ it "does not allow .first" do
26
+ lambda{ActsAsIcontact::ContactHistory.first}.should raise_error(ActsAsIcontact::QueryError, "Contact History must be obtained using the contact.history method.")
27
+ end
28
+
29
+ it "does not allow .all" do
30
+ lambda{ActsAsIcontact::ContactHistory.all}.should raise_error(ActsAsIcontact::QueryError, "Contact History must be obtained using the contact.history method.")
31
+ end
32
+
33
+ it "requires a contactId" do
34
+ lambda{ActsAsIcontact::ContactHistory.new(contact: nil)}.should raise_error(ActsAsIcontact::ValidationError, "Contact History requires a Contact")
35
+ end
36
+ end
@@ -29,7 +29,9 @@ describe ActsAsIcontact::Contact do
29
29
  @john.subscribe(444444)
30
30
  end
31
31
 
32
- it "knows its history"
32
+ it "knows its history" do
33
+ @john.history.count.should == 4
34
+ end
33
35
  end
34
36
 
35
37
  end
@@ -34,6 +34,10 @@ FakeWeb.register_uri(:post, "#{ic}/contacts", :body => %q<{"contacts":[{"email":
34
34
  FakeWeb.register_uri(:get, "#{ic}/contacts/333444", :body => %q<{"contact":{"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":""}}>)
35
35
  FakeWeb.register_uri(:get, "#{ic}/contacts/333333", :body => %q<{"contact":{"email":"john@example.org","firstName":"John","lastName":"Test","status":"normal","contactId":"333333","createDate":"2009-07-24 01:00:00"}}>)
36
36
 
37
+ # Contact History
38
+ FakeWeb.register_uri(:get, "#{ic}/contacts/333333/actions?limit=500", :body => %q<{"actions":[{"actionType":"EditFields","actionTime":"2009-08-01T16:02:44-0400","actor":"407118","details":{"email":"susan_smith@example.org"}},{"actionType":"EditFields","actionTime":"2009-08-01T16:02:13-0400","actor":"407118","details":{"firstName":"Susan","lastName":"Smith","custom_test":"hello 4"}},{"actionType":"EditSubscription","actionTime":"2009-08-01T16:00:31-0400","actor":"407118","details":{"listId":"174137","newStatus":"normal"}},{"actionType":"AddContact","actionTime":"2009-08-01T16:00:30-0400","actor":"407118","details":[]}],"limit":50,"offset":0,"total":4}>)
39
+ FakeWeb.register_uri(:get, "#{ic}/contacts/333333/actions?limit=1", :body => %q<{"actions":[{"actionType":"EditFields","actionTime":"2009-08-01T16:02:44-0400","actor":"407118","details":{"email":"susan_smith@example.org"}},{"actionType":"EditFields","actionTime":"2009-08-01T16:02:13-0400","actor":"407118","details":{"firstName":"Susan","lastName":"Smith","custom_test":"hello 4"}},{"actionType":"EditSubscription","actionTime":"2009-08-01T16:00:31-0400","actor":"407118","details":{"listId":"174137","newStatus":"normal"}},{"actionType":"AddContact","actionTime":"2009-08-01T16:00:30-0400","actor":"407118","details":[]}],"limit":50,"offset":0,"total":4}>)
40
+
37
41
  # Lists
38
42
  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."}]}>)
39
43
  FakeWeb.register_uri(:get, "#{ic}/lists/444444", :body => %q<{"list":{"listId":"444444","name":"First Test","emailOwnerOnChange":"0","welcomeOnManualAdd":"0","welcomeOnSignupAdd":"0","welcomeMessageId":"555555","description":"Just a test list."}}>)
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.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Eley
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-08-08 00:00:00 -04:00
12
+ date: 2009-08-10 00:00:00 -04:00
13
13
  default_executable: icontact
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -96,6 +96,8 @@ files:
96
96
  - bin/icontact
97
97
  - init.rb
98
98
  - lib/acts_as_icontact.rb
99
+ - lib/acts_as_icontact/command_line/completion.rb
100
+ - lib/acts_as_icontact/command_line/variables.rb
99
101
  - lib/acts_as_icontact/config.rb
100
102
  - lib/acts_as_icontact/connection.rb
101
103
  - lib/acts_as_icontact/exceptions.rb
@@ -110,6 +112,7 @@ files:
110
112
  - lib/acts_as_icontact/resources/campaign.rb
111
113
  - lib/acts_as_icontact/resources/client.rb
112
114
  - lib/acts_as_icontact/resources/contact.rb
115
+ - lib/acts_as_icontact/resources/contact_history.rb
113
116
  - lib/acts_as_icontact/resources/custom_field.rb
114
117
  - lib/acts_as_icontact/resources/list.rb
115
118
  - lib/acts_as_icontact/resources/message.rb
@@ -126,6 +129,7 @@ files:
126
129
  - spec/resources/account_spec.rb
127
130
  - spec/resources/campaign_spec.rb
128
131
  - spec/resources/clientfolder_spec.rb
132
+ - spec/resources/contact_history_spec.rb
129
133
  - spec/resources/contact_spec.rb
130
134
  - spec/resources/custom_field_spec.rb
131
135
  - spec/resources/list_spec.rb
@@ -177,6 +181,7 @@ test_files:
177
181
  - spec/resources/account_spec.rb
178
182
  - spec/resources/campaign_spec.rb
179
183
  - spec/resources/clientfolder_spec.rb
184
+ - spec/resources/contact_history_spec.rb
180
185
  - spec/resources/contact_spec.rb
181
186
  - spec/resources/custom_field_spec.rb
182
187
  - spec/resources/list_spec.rb