SFEley-acts_as_icontact 0.3.2 → 0.4.0

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.
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",
data/bin/icontact CHANGED
@@ -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: SFEley-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 -07:00
12
+ date: 2009-08-10 00:00:00 -07:00
13
13
  default_executable: icontact
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -91,6 +91,8 @@ files:
91
91
  - bin/icontact
92
92
  - init.rb
93
93
  - lib/acts_as_icontact.rb
94
+ - lib/acts_as_icontact/command_line/completion.rb
95
+ - lib/acts_as_icontact/command_line/variables.rb
94
96
  - lib/acts_as_icontact/config.rb
95
97
  - lib/acts_as_icontact/connection.rb
96
98
  - lib/acts_as_icontact/exceptions.rb
@@ -105,6 +107,7 @@ files:
105
107
  - lib/acts_as_icontact/resources/campaign.rb
106
108
  - lib/acts_as_icontact/resources/client.rb
107
109
  - lib/acts_as_icontact/resources/contact.rb
110
+ - lib/acts_as_icontact/resources/contact_history.rb
108
111
  - lib/acts_as_icontact/resources/custom_field.rb
109
112
  - lib/acts_as_icontact/resources/list.rb
110
113
  - lib/acts_as_icontact/resources/message.rb
@@ -121,6 +124,7 @@ files:
121
124
  - spec/resources/account_spec.rb
122
125
  - spec/resources/campaign_spec.rb
123
126
  - spec/resources/clientfolder_spec.rb
127
+ - spec/resources/contact_history_spec.rb
124
128
  - spec/resources/contact_spec.rb
125
129
  - spec/resources/custom_field_spec.rb
126
130
  - spec/resources/list_spec.rb
@@ -171,6 +175,7 @@ test_files:
171
175
  - spec/resources/account_spec.rb
172
176
  - spec/resources/campaign_spec.rb
173
177
  - spec/resources/clientfolder_spec.rb
178
+ - spec/resources/contact_history_spec.rb
174
179
  - spec/resources/contact_spec.rb
175
180
  - spec/resources/custom_field_spec.rb
176
181
  - spec/resources/list_spec.rb