constant-contact-ruby 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,23 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ doc
23
+ .yardoc
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 BatchBlue Software, LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,75 @@
1
+ = constant-contact-ruby
2
+
3
+ Ruby wrapper for the Constant Contact REST API
4
+
5
+ == Requirements
6
+
7
+ HTTParty gem - http://rubygems.org/gems/httparty
8
+
9
+ == Usage
10
+
11
+ require 'constant-contact-ruby'
12
+ include ConstantContact
13
+
14
+ ### Contacts ###
15
+
16
+ all_contacts = Contact.all
17
+
18
+ contact_7 = Contact.get( 7 )
19
+ contact_7.update_attributes!( :first_name => 'John', :last_name => 'Doe' )
20
+ contact_7.add_to_list!( 3 )
21
+ contact_7.remove_from_list!( 3 )
22
+ contact_7.replace_contact_lists!( 1, 3, 9 )
23
+
24
+ new_contact = Contact.add( :email_address => 'john@example.com', :first_name => 'John', :last_name => 'Doe' )
25
+
26
+ john_doe = Contact.search_by_email( 'john@example.com' )
27
+
28
+
29
+ ### Contact Lists ###
30
+
31
+ all_lists = ContactList.all
32
+
33
+ list_1 = ContactList.get( 1 )
34
+ list_1_members = ContactList.members( list_1.uid )
35
+ list_1.clear_contacts! # remove all contacts from list
36
+ ContactList.delete( 1 ) # delete the list
37
+
38
+ new_list = ContactList.add( 'my new list' )
39
+
40
+
41
+ ### Bulk Activities (involving multiple contacts) ###
42
+
43
+ activities = Activity.all # list all activities in the system
44
+
45
+ activity_details = Activity.get( activities.first.uid )
46
+
47
+ Activity.remove_all_contacts_from_lists( 1, 3, 4 ) # remove all contacts from lists 1, 3, and 4
48
+
49
+ new_contacts = [
50
+ { :email_address = 'user1@example.com', :first_name => 'User1', :last_name => 'test' },
51
+ { :email_address = 'user2@example.com', :first_name => 'User2', :last_name => 'test 2' },
52
+ { :email_address = 'user3@example.com', :first_name => 'User1' },
53
+ { :email_address = 'user4@example.com' }
54
+ ]
55
+ Activity.add_contacts_to_lists( new_contacts, 3, 4 ) # all the users array to lists 3 and 4
56
+ Activity.remove_contacts_from_lists( new_contacts, 4 ) # remove the users from list 4
57
+
58
+
59
+ == Note on Patches/Pull Requests
60
+
61
+ * Fork the project.
62
+ * Make your feature addition or bug fix.
63
+ * Add tests for it. This is important so I don't break it in a
64
+ future version unintentionally.
65
+ * Commit, do not mess with rakefile, version, or history.
66
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
67
+ * Send me a pull request. Bonus points for topic branches.
68
+
69
+ == Authors
70
+
71
+ Craig P Jolicoeur - http://craigjolicoeur.com
72
+
73
+ == Copyright
74
+
75
+ Copyright (c) 2010 BatchBlue Software, LLC. See LICENSE for details.
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "constant-contact-ruby"
8
+ gem.summary = %Q{Constant Contact Ruby API Wrapper}
9
+ gem.description = %Q{Ruby wrapper around the Constant Contact REST API}
10
+ gem.email = "cpjolicoeur@gmail.com"
11
+ gem.homepage = "http://github.com/cpjolicoeur/constant-contact-ruby"
12
+ gem.authors = ["Craig P Jolicoeur"]
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/test_*.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+ task :test => :check_dependencies
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
47
+
48
+ rdoc.rdoc_dir = 'rdoc'
49
+ rdoc.title = "constant-contact-ruby #{version}"
50
+ rdoc.rdoc_files.include('README*')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
data/TODO ADDED
@@ -0,0 +1 @@
1
+ add export feature to Activities
@@ -0,0 +1,5 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 2
4
+ :build:
5
+ :patch: 1
@@ -0,0 +1,63 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{constant-contact-ruby}
8
+ s.version = "0.2.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Craig P Jolicoeur"]
12
+ s.date = %q{2010-03-03}
13
+ s.description = %q{Ruby wrapper around the Constant Contact REST API}
14
+ s.email = %q{cpjolicoeur@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc",
18
+ "TODO"
19
+ ]
20
+ s.files = [
21
+ ".document",
22
+ ".gitignore",
23
+ "LICENSE",
24
+ "README.rdoc",
25
+ "Rakefile",
26
+ "TODO",
27
+ "VERSION.yml",
28
+ "constant-contact-ruby.gemspec",
29
+ "constant-contact-ruby.rb",
30
+ "lib/constant_contact.rb",
31
+ "lib/constant_contact/activity.rb",
32
+ "lib/constant_contact/base_resource.rb",
33
+ "lib/constant_contact/contact.rb",
34
+ "lib/constant_contact/contact_list.rb",
35
+ "test/fixtures/get_contact.xml",
36
+ "test/fixtures/get_contacts.xml",
37
+ "test/fixtures/post_contacts.xml",
38
+ "test/helper.rb",
39
+ "test/test_constant-contact-ruby.rb",
40
+ "test/test_contacts.rb"
41
+ ]
42
+ s.homepage = %q{http://github.com/cpjolicoeur/constant-contact-ruby}
43
+ s.rdoc_options = ["--charset=UTF-8"]
44
+ s.require_paths = ["lib"]
45
+ s.rubygems_version = %q{1.3.5}
46
+ s.summary = %q{Constant Contact Ruby API Wrapper}
47
+ s.test_files = [
48
+ "test/helper.rb",
49
+ "test/test_constant-contact-ruby.rb",
50
+ "test/test_contacts.rb"
51
+ ]
52
+
53
+ if s.respond_to? :specification_version then
54
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
55
+ s.specification_version = 3
56
+
57
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
58
+ else
59
+ end
60
+ else
61
+ end
62
+ end
63
+
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'httparty'
3
+
4
+ require 'lib/constant_contact'
5
+ require 'lib/constant_contact/base_resource'
6
+ require 'lib/constant_contact/contact'
7
+ require 'lib/constant_contact/contact_list'
8
+ require 'lib/constant_contact/activity'
@@ -0,0 +1,18 @@
1
+ module ConstantContact
2
+
3
+ include HTTParty
4
+ format :xml
5
+ headers 'Accept' => 'application/atom+xml'
6
+ headers 'Content-Type' => 'application/atom+xml'
7
+
8
+ API_KEY = "59ca4bb4-51e9-4c08-a2b2-a34aac7bb78f"
9
+
10
+ class << self
11
+ # Create a connection to the Constant Contact API using your login credentials
12
+ def setup( user, pass )
13
+ basic_auth "#{API_KEY}%#{user}", pass
14
+ base_uri "https://api.constantcontact.com/ws/customers/#{user.downcase}"
15
+ end
16
+ end
17
+
18
+ end
@@ -0,0 +1,196 @@
1
+ module ConstantContact
2
+ class Activity < BaseResource
3
+
4
+ ADD_CONTACTS = 'ADD_CONTACTS'.freeze
5
+ REMOVE_CONTACTS = 'REMOVE_CONTACTS_FROM_LISTS'.freeze
6
+ CLEAR_CONTACTS = 'CLEAR_CONTACTS_FROM_LISTS'.freeze
7
+ EXPORT_CONTACTS = 'EXPORT_CONTACTS'.freeze
8
+
9
+ attr_reader :uid, :original_xml
10
+
11
+ def initialize( params={}, orig_xml='' ) #:nodoc:
12
+ return false if params.empty?
13
+
14
+ @uid = params['id'].split('/').last
15
+ @original_xml = orig_xml
16
+
17
+ fields = params['content']['Activity']
18
+
19
+ if errors = fields.delete( 'Errors' )
20
+ # FIXME: handle the <Errors> node properly
21
+ end
22
+
23
+ fields.each do |k,v|
24
+ underscore_key = underscore( k )
25
+
26
+ instance_eval %{
27
+ @#{underscore_key} = "#{v}"
28
+
29
+ def #{underscore_key}
30
+ @#{underscore_key}
31
+ end
32
+ }
33
+ end
34
+
35
+ end
36
+
37
+ # List all activities
38
+ def self.all( options={} )
39
+ activities = []
40
+
41
+ data = ConstantContact.get( '/activities', options )
42
+ return activities if ( data.nil? or data.empty? or data['feed']['entry'].nil? )
43
+
44
+ data['feed']['entry'].each do |entry|
45
+ activities << new( entry )
46
+ end
47
+
48
+ activities
49
+ end
50
+
51
+ # Get the details of a specific activity
52
+ def self.get( id, options={} )
53
+ activity = ConstantContact.get( "/activities/#{id.to_s}", options )
54
+ return nil if ( activity.nil? or activity.empty? )
55
+ new( activity['entry'], activity.body )
56
+ end
57
+
58
+ # Add multiple users to one or more contact lists
59
+ def self.add_contacts_to_lists( users=[], *lists )
60
+ data_param = build_data_param( users )
61
+ list_param = build_lists_param( *lists )
62
+
63
+ data = ConstantContact.post( '/activities',
64
+ :headers => { 'Content-Type' => 'application/x-www-form-urlencoded' },
65
+ :body => { 'activityType' => ADD_CONTACTS, :data => data_param, :lists => list_param } )
66
+
67
+ if data.code == 201
68
+ new( data['entry'] )
69
+ else
70
+ puts "HTTP Status Code: #{data.code}, message: #{data.message}"
71
+ return false
72
+ end
73
+ end
74
+
75
+ # Remove multiple users from a contact list
76
+ def self.remove_contacts_from_lists( users=[], *lists )
77
+ data_param = build_data_param( users )
78
+ list_param = build_lists_param( *lists )
79
+
80
+ data = ConstantContact.post( '/activities',
81
+ :headers => { 'Content-Type' => 'application/x-www-form-urlencoded' },
82
+ :body => { 'activityType' => REMOVE_CONTACTS, :data => data_param, :lists => list_param } )
83
+
84
+ if data.code == 201
85
+ new( data['entry'] )
86
+ else
87
+ puts "HTTP Status Code: #{data.code}, message: #{data.message}"
88
+ return false
89
+ end
90
+ end
91
+
92
+ # Remove all users from a specific contact list
93
+ def self.remove_all_contacts_from_lists( *lists )
94
+ list_param = build_lists_param( *lists )
95
+
96
+ data = ConstantContact.post( '/activities',
97
+ :headers => { 'Content-Type' => 'application/x-www-form-urlencoded' },
98
+ :body => { 'activityType' => CLEAR_CONTACTS, :lists => list_param } )
99
+
100
+ if data.code == 201
101
+ new( data['entry'] )
102
+ else
103
+ puts "HTTP Status Code: #{data.code}, message: #{data.message}"
104
+ return false
105
+ end
106
+ end
107
+
108
+ # Export subscribers list to a file
109
+ #
110
+ # @param [Integer/String] list_id is the uid of the list to export
111
+ # @param [Array] fields is an array of fields to export for list contacts
112
+ #
113
+ def self.export( list_id, *fields )
114
+ export_columns = build_export_columns( *fields )
115
+
116
+ data = ConstantContact.post( '/activities',
117
+ :headers => { 'Content-Type' => 'application/x-www-form-urlencoded' },
118
+ :body => {
119
+ 'activityType' => EXPORT_CONTACTS,
120
+ 'fileType' => 'CSV',
121
+ 'exportOptDate' => true,
122
+ 'exportOptSource' => true,
123
+ 'exportListName' => true,
124
+ 'sortBy' => 'EMAIL_ADDRESS',
125
+ 'listId' => ContactList.url_for( list_id ),
126
+ :columns => export_columns
127
+ } )
128
+
129
+ if data.code == 201
130
+ new( data['entry'] )
131
+ else
132
+ puts "HTTP Status Code: #{data.code}, message: #{data.message}"
133
+ return false
134
+ end
135
+ end
136
+
137
+ private
138
+
139
+ # Build the data= query param for a POST request
140
+ #
141
+ # @param [Array] Users - an array of user hash objects
142
+ #
143
+ def self.build_data_param( users )
144
+ return '' if users.empty?
145
+ data_start, data_end = '', ''
146
+ keys, fields = [], []
147
+
148
+ # get a list of all the key fields and then create values
149
+ users.each do |u|
150
+ u.each_key do |k|
151
+ readable_key = underscore(k).split('_').map{|x| x.capitalize}.join(' ')
152
+ packet = { :original => k, :readable => readable_key }
153
+ keys << packet unless keys.include?( packet )
154
+ end
155
+ end
156
+ data_start = keys.map { |k| k[:readable] }.join(',') + "\n"
157
+
158
+ # now build the data fields
159
+ users.each do |u|
160
+ tmp = ''
161
+ keys.each { |k| tmp << "#{u[k[:original]]}," }
162
+ fields << tmp.chomp(',') + "\n"
163
+ end
164
+ data_end = fields.join
165
+
166
+ return data_start + data_end
167
+ end
168
+
169
+ # Build the lists= param for a POST request
170
+ #
171
+ # @param [Array] lists - an array of list ids
172
+ # @return [String] a usable string for the list param in a POST
173
+ def self.build_lists_param( *lists )
174
+ # list_param = lists.map { |list| ContactList.url_for( list.to_s ) }
175
+ #
176
+ # FIXME: this is hack because passing in an array of lists wasnt working because of Hash.to_params
177
+ # Hash#to_params returns lists[]=foo&lists[]=bar instead of lists=foo&lists=bar
178
+ # I can't even find where Hash#to_params is defined!!!
179
+ list_param = ''
180
+ lists.each do |list|
181
+ list_param << "#{ContactList.url_for( list.to_s )}&lists="
182
+ end
183
+ list_param.chomp('&lists=')
184
+ end
185
+
186
+ def self.build_export_columns( *fields )
187
+ columns_param = ''
188
+ fields.each do |field|
189
+ readable_col = underscore( field ).split('_').map{ |x| x.upcase }.join(' ')
190
+ columns_param << "#{readable_col}&columns="
191
+ end
192
+ columns_param.chomp('&columns=')
193
+ end
194
+
195
+ end # class Activity
196
+ end # module ConstantContact
@@ -0,0 +1,23 @@
1
+ module ConstantContact
2
+ class BaseResource #:nodoc:
3
+
4
+ private
5
+
6
+ def self.camelize( string )
7
+ string.split( /[^a-z0-9]/i ).map{ |w| w.capitalize }.join
8
+ end
9
+
10
+ def camelize( string )
11
+ BaseResource.camelize( string )
12
+ end
13
+
14
+ def self.underscore( string )
15
+ string.to_s.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').tr("-", "_").downcase
16
+ end
17
+
18
+ def underscore( string )
19
+ BaseResource.underscore( string )
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,323 @@
1
+ module ConstantContact
2
+ class Contact < BaseResource
3
+
4
+ attr_reader :uid, :contact_lists, :original_xml
5
+
6
+ def initialize( params={}, orig_xml='', from_contact_list=false ) #:nodoc:
7
+ return false if params.empty?
8
+
9
+ @uid = params['id'].split('/').last
10
+ @original_xml = orig_xml
11
+ @contact_lists = []
12
+
13
+ if from_contact_list
14
+ fields = params['content']['ContactListMember']
15
+ else
16
+ fields = params['content']['Contact']
17
+ end
18
+
19
+ if lists = fields.delete( 'ContactLists' )
20
+ if lists['ContactList'].is_a?( Array )
21
+ @contact_lists = lists['ContactList'].collect { |list| list['id'].split('/').last }
22
+ else
23
+ @contact_lists << lists['ContactList']['id'].split('/').last
24
+ end
25
+ end
26
+
27
+ fields.each do |k,v|
28
+ underscore_key = underscore( k )
29
+
30
+ instance_eval %{
31
+ @#{underscore_key} = "#{v}"
32
+
33
+ def #{underscore_key}
34
+ @#{underscore_key}
35
+ end
36
+ }
37
+ end
38
+
39
+ end # def initialize
40
+
41
+ # Update a single contact record
42
+ #
43
+ # NOTE: you cannot update a Contact's ContactList subscriptions through
44
+ # this method. Use the apropriate ContactList methods instead
45
+ #
46
+ def update_attributes!( params={} )
47
+ return false unless full_record? # TODO: raise some kind of specific error here
48
+
49
+ params.each do |key,val|
50
+ self.instance_variable_set("@#{key.to_s}", val)
51
+ end
52
+
53
+ data = ConstantContact.put( "/contacts/#{self.uid}", :body => self.send(:to_xml) )
54
+ if data.code == 204 # success
55
+ return true
56
+ else
57
+ return false # probably should raise an error here instead
58
+ end
59
+ end
60
+
61
+ # Add user to a contact list
62
+ def add_to_list!( list_id, options={} )
63
+ list_id = list_id.to_s
64
+ xml = update_contact_lists( *(self.contact_lists + [list_id]) )
65
+
66
+ # FIXME: clean up the following code - it appears in 3 methods in this class!
67
+ options.merge!({ :body => xml })
68
+ data = ConstantContact.put( "/contacts/#{self.uid}", options )
69
+
70
+ if data.code == 204 # success
71
+ self.contact_lists << list_id unless self.contact_lists.include?( list_id )
72
+ return true
73
+ else
74
+ return false # probably should raise an error here instead
75
+ end
76
+ end
77
+
78
+ # Remove user from a contact list
79
+ def remove_from_list!( list_id, options={} )
80
+ list_id = list_id.to_s
81
+ xml = update_contact_lists( *(self.contact_lists - [list_id]) )
82
+
83
+ # FIXME: clean up the following code - it appears in 3 methods in this class!
84
+ options.merge!({ :body => xml })
85
+ data = ConstantContact.put( "/contacts/#{self.uid}", options )
86
+
87
+ if data.code == 204 # success
88
+ self.contact_lists.delete( list_id )
89
+ return true
90
+ else
91
+ return false # probably should raise an error here instead
92
+ end
93
+ end
94
+
95
+ # Set a users contact lists
96
+ def replace_contact_lists!( *lists )
97
+ xml = update_contact_lists( *lists )
98
+
99
+ # FIXME: clean up the following code - it appears in 3 methods in this class!
100
+ options = { :body => xml }
101
+ data = ConstantContact.put( "/contacts/#{self.uid}", options )
102
+
103
+ if data.code == 204 # success
104
+ @contact_lists = lists.map { |l| l.to_s }
105
+ return true
106
+ else
107
+ return false # probably should raise an error here instead
108
+ end
109
+ end
110
+
111
+ # Opt-out from all contact lists
112
+ #
113
+ # Contact will be removed from all lists and become a member of the
114
+ # Do-Not_Mail special list
115
+ def opt_out!( options={} )
116
+ data = ConstantContact.delete( "/contacts/#{self.uid}", options )
117
+
118
+ if data.code == 204
119
+ @contact_lists = []
120
+ return true
121
+ else
122
+ return false
123
+ end
124
+ end
125
+
126
+ # Opt-in a user who has previously opted out
127
+ #--
128
+ # FIXME: this isn't currently working. Currently I keep getting a 403-Forbidden response
129
+ # should this really even be in the API wrapper at all?
130
+ def opt_in!( *lists )
131
+ # # NOTE: same as replace_contact_lists but must set to ACTION_BY_CONTACT
132
+ # xml = update_contact_lists( *lists ).gsub( /<\/ContactLists>/, %Q(<OptInSource>ACTION_BY_CONTACT</OptInSource>\n\t</ContactLists>) )
133
+
134
+ # # FIXME: clean up the following code - it appears in 3 methods in this class!
135
+ # options = { :body => xml }
136
+ # data = ConstantContact.put( "/contacts/#{self.uid}", options )
137
+ #
138
+ # if data.code == 204 # success
139
+ # @contact_lists = lists.map { |l| l.to_s }
140
+ # return true
141
+ # else
142
+ # return false # probably should raise an error here instead
143
+ # end
144
+ end
145
+
146
+ # Get a summary list all contacts
147
+ def self.all( options={} )
148
+ contacts = []
149
+
150
+ data = ConstantContact.get( '/contacts', options )
151
+ return contacts if ( data.nil? or data.empty? )
152
+
153
+ data['feed']['entry'].each do |entry|
154
+ contacts << new( entry )
155
+ end
156
+
157
+ contacts
158
+ end
159
+
160
+ # Add a new contact
161
+ #
162
+ # Required data fields:
163
+ # * EmailAddress => String
164
+ # * ContactLists => Array of list IDs
165
+ #
166
+ # Options data fields:
167
+ # * EmailType
168
+ # * FirstName
169
+ # * MiddleName
170
+ # * LastName
171
+ # * JobTitle
172
+ # * CompanyName
173
+ # * HomePhone
174
+ # * WorkPhone
175
+ # * Addr1
176
+ # * Addr2
177
+ # * Addr3
178
+ # * City
179
+ # * StateCode => Must be valid US/Canada Code (http://ui.constantcontact.com/CCSubscriberAddFileFormat.jsp#states)
180
+ # * StateName
181
+ # * CountryCode = Must be valid code (http://constantcontact.custhelp.com/cgi-bin/constantcontact.cfg/php/enduser/std_adp.php?p_faqid=3614)
182
+ # * CountryName
183
+ # * PostalCode
184
+ # * SubPostalCode
185
+ # * Note
186
+ # * CustomField[1-15]
187
+ # * OptInSource
188
+ # * OptOutSource
189
+ #
190
+ def self.add( data={}, opt_in='ACTION_BY_CUSTOMER', options={} )
191
+ xml = build_contact_xml_packet( data, opt_in )
192
+
193
+ options.merge!({ :body => xml })
194
+ data = ConstantContact.post( "/contacts", options )
195
+
196
+ # check response.code
197
+ if data.code == 201 # Entity Created
198
+ return new( data['entry'] )
199
+ else
200
+ # data.code == 409 # Conflict ( probably a duplicate )
201
+ puts "HTTP Status Code: #{data.code}, message: #{data.message}"
202
+ return nil
203
+ end
204
+ end
205
+
206
+ # Get detailed record for a single contact by id
207
+ def self.get( id, options={} )
208
+ data = ConstantContact.get( "/contacts/#{id.to_s}", options )
209
+ return nil if ( data.nil? or data.empty? )
210
+ new( data['entry'], data.body )
211
+ end
212
+
213
+ # Search for a contact by last updated date
214
+ #
215
+ # Valid options:
216
+ # * :updated_since => Time object
217
+ # * :list_type => One of 'active'|'removed'|'do-not-mail'
218
+ #
219
+ # def self.search_by_date( options={} )
220
+ # end
221
+
222
+ # Search for a contact by email address
223
+ #
224
+ # @param [String] email => "user@example.com"
225
+ #
226
+ def self.search_by_email( email )
227
+ data = ConstantContact.get( '/contacts', :query => { :email => email.downcase } )
228
+ return [] if ( data.nil? )
229
+
230
+ if data.code == 500
231
+ puts "HTTP Status Code: #{data.code}, message: #{data.message}"
232
+ return false
233
+ else
234
+ new( data['feed']['entry'] )
235
+ end
236
+ end
237
+
238
+ # Returns the objects API URI
239
+ def self.url_for( id )
240
+ "#{ConstantContact.base_uri}/contacts/#{id}"
241
+ end
242
+
243
+ private
244
+
245
+ def update_contact_lists( *lists )
246
+ str = %Q(<ContactLists>\n)
247
+ lists.each do |list|
248
+ str << %Q( <ContactList id="#{ContactList.url_for( list )}" />\n)
249
+ end
250
+ str << %Q( </ContactLists>)
251
+
252
+ # self.original_xml.gsub(/<ContactLists>.*<\/ContactLists>/m, str)
253
+ if self.original_xml =~ /<ContactLists>.*<\/ContactLists>/m
254
+ self.original_xml.gsub( /#{$&}/, str)
255
+ else
256
+ self.original_xml.gsub( /<\/Contact>/m, "#{str}\n</Contact>" )
257
+ end
258
+ end
259
+
260
+ def self.build_contact_xml_packet( data={}, opt_in='ACTION_BY_CUSTOMER' )
261
+ xml = <<EOF
262
+ <entry xmlns="http://www.w3.org/2005/Atom">
263
+ <title type="text"> </title>
264
+ <updated>#{Time.now.strftime("%Y-%m-%dT%H:%M:%S.000Z")}</updated>
265
+ <author> </author>
266
+ <id>data:,none</id>
267
+ <summary type="text">Contact</summary>
268
+ <content type="application/vnd.ctct+xml">
269
+ <Contact xmlns="http://ws.constantcontact.com/ns/1.0/">
270
+ EOF
271
+
272
+ data.each do |key, val|
273
+ node = camelize(key.to_s)
274
+
275
+ if key == :contact_lists
276
+ xml << %Q( <ContactLists>\n)
277
+ val.each do |list_id|
278
+ xml<< %Q( <ContactList id="#{ContactList.url_for( list_id )}" />\n)
279
+ end
280
+ xml << %Q( </ContactLists>\n)
281
+ else
282
+ xml << %Q( <#{node}>#{val}</#{node}>\n)
283
+ end
284
+ end
285
+
286
+ xml += <<EOF
287
+ <OptInSource>#{opt_in}</OptInSource>
288
+ </Contact>
289
+ </content>
290
+ </entry>
291
+ EOF
292
+ xml
293
+ end # def build_contact_xml_packet
294
+
295
+ # Is this a full contact record?
296
+ def full_record?
297
+ !self.contact_lists.empty?
298
+ end
299
+
300
+ # convert a full Contact record into XML format
301
+ def to_xml
302
+ return nil unless full_record?
303
+
304
+ do_not_process = [ "@contact_lists", "@original_source", "@original_xml", "@uid", "@xmlns" ]
305
+
306
+ xml = self.original_xml
307
+
308
+ self.instance_variables.each do |ivar|
309
+ next if do_not_process.include?( ivar )
310
+
311
+ var = camelize( ivar.gsub(/@/,'') )
312
+
313
+ xml.gsub!( /<#{var}>(.*)<\/#{var}>/ , "<#{var}>#{self.instance_variable_get(ivar)}</#{var}>" )
314
+ end
315
+
316
+ # replace <updated> node with current time
317
+ xml.gsub( /<updated>.*<\/updated>/, Time.now.strftime("%Y-%m-%dT%H:%M:%S.000Z") )
318
+
319
+ xml
320
+ end
321
+
322
+ end # class Contact
323
+ end # module ConstantContact
@@ -0,0 +1,152 @@
1
+ module ConstantContact
2
+ class ContactList < BaseResource
3
+
4
+ attr_reader :uid, :original_xml
5
+
6
+ def initialize( params={}, orig_xml='' ) #:nodoc:
7
+ return false if params.empty?
8
+
9
+ @uid = params['id'].split('/').last
10
+ @original_xml = orig_xml
11
+
12
+ params['content']['ContactList'].each do |k,v|
13
+ underscore_key = underscore( k )
14
+
15
+ instance_eval %{
16
+ @#{underscore_key} = "#{v}"
17
+
18
+ def #{underscore_key}
19
+ @#{underscore_key}
20
+ end
21
+ }
22
+ end
23
+ end
24
+
25
+ # Update a contacts attributes
26
+ def update_attributes!( params={} )
27
+ return false unless full_record?
28
+
29
+ params.each do |key,val|
30
+ self.instance_variable_set("@#{key.to_s}", val)
31
+ end
32
+
33
+ data = ConstantContact.put( "/lists/#{self.uid}", :body => self.send(:to_xml) )
34
+
35
+ if data.code == 204
36
+ return true
37
+ else
38
+ return data # probably should raise an error here instead
39
+ end
40
+ end
41
+
42
+ # Remove all contacts from the contact list
43
+ def clear_contacts!
44
+ Activity.remove_all_contacts_from_lists( self.uid )
45
+ end
46
+
47
+ # Get all contact lists
48
+ def self.all( options={} )
49
+ lists = []
50
+
51
+ data = ConstantContact.get( '/lists', options )
52
+ return lists if ( data.nil? or data.empty? )
53
+
54
+ data['feed']['entry'].each do |entry|
55
+ lists << new( entry )
56
+ end
57
+
58
+ lists
59
+ end
60
+
61
+ # Add a new contact list
62
+ def self.add( name, opt_in=false, sort_order=99, options={} )
63
+ return nil if( name.nil? || name.empty? )
64
+
65
+ xml = build_contact_list_xml_packet( name, opt_in, sort_order )
66
+
67
+ options.merge!({ :body => xml })
68
+ data = ConstantContact.post( "/lists", options )
69
+
70
+ if data.code == 201
71
+ return new( data['entry'] )
72
+ else
73
+ puts "HTTP Status Code: #{data.code}, message: #{data.message}"
74
+ return nil
75
+ end
76
+ end
77
+
78
+ # Delete a contact list
79
+ def self.delete( id, options={} )
80
+ data = ConstantContact.delete( "/lists/#{id.to_s}", options )
81
+ return ( data.code == 204 ) ? true : false
82
+ end
83
+
84
+ # Get a single contact list
85
+ def self.get( id, options={} )
86
+ list = ConstantContact.get( "/lists/#{id.to_s}", options )
87
+ return nil if ( list.nil? or list.empty? )
88
+ new( list['entry'], list.body )
89
+ end
90
+
91
+ # Get a lists members
92
+ def self.members( id, options={} )
93
+ members = ConstantContact.get( "/lists/#{id.to_s}/members", options )
94
+ return nil if ( members.nil? or members.empty? )
95
+ members['feed']['entry'].collect { |entry| Contact.new( entry, '', true ) }
96
+ end
97
+
98
+ # Returns the objects API URI
99
+ def self.url_for( id )
100
+ "#{ConstantContact.base_uri}/lists/#{id}"
101
+ end
102
+
103
+ private
104
+
105
+ def self.build_contact_list_xml_packet( name, opt_in=false, sort=99 )
106
+ xml = <<EOF
107
+ <entry xmlns="http://www.w3.org/2005/Atom">
108
+ <updated>#{Time.now.strftime("%Y-%m-%dT%H:%M:%S.000Z")}</updated>
109
+ <title />
110
+ <author />
111
+ <id>data:,none</id>
112
+ <content type="application/vnd.ctct+xml">
113
+ <ContactList xmlns="http://ws.constantcontact.com/ns/1.0/">
114
+ <Name>#{name}</Name>
115
+ <SortOrder>#{sort}</SortOrder>
116
+ <OptInDefault>#{opt_in.to_s}</OptInDefault>
117
+ </ContactList>
118
+ </content>
119
+ </entry>
120
+ EOF
121
+ xml
122
+ end
123
+
124
+ # Is this a full record or a summary record?
125
+ def full_record?
126
+ !self.members.emtpy?
127
+ end
128
+
129
+ # Convert the object into the needed API XML format
130
+ def to_xml
131
+ return nil unless full_record?
132
+
133
+ do_not_process = [ "@original_xml", "@uid", "@members" ]
134
+
135
+ xml = self.original_xml
136
+
137
+ self.instance_variables.each do |ivar|
138
+ next if do_not_process.include?( ivar )
139
+
140
+ var = camelize( ivar.gsub(/@/,'') )
141
+
142
+ xml.gsub!( /<#{var}>(.*)<\/#{var}>/ , "<#{var}>#{self.instance_variable_get(ivar)}</#{var}>" )
143
+ end
144
+
145
+ # replace <updated> node with current time
146
+ xml.gsub( /<updated>.*<\/updated>/, Time.now.strftime("%Y-%m-%dT%H:%M:%S.000Z") )
147
+
148
+ xml
149
+ end
150
+
151
+ end # class ContactList
152
+ end # module ConstantContact
@@ -0,0 +1,72 @@
1
+ <?xml version='1.0' encoding='UTF-8'?>
2
+ <entry xmlns="http://www.w3.org/2005/Atom">
3
+ <link href="/ws/customers/joesflowers/contacts/22199" rel="edit" />
4
+ <id>http://api.constantcontact.com/ws/customers/joesflowers/contacts/22199</id>
5
+ <title type="text">Contact: joe@example.com</title>
6
+ <updated>2009-11-20T20:41:19.243Z</updated>
7
+ <author>
8
+ <name>Constant Contact</name>
9
+ </author>
10
+ <content type="application/vnd.ctct+xml">
11
+ <Contact xmlns="http://ws.constantcontact.com/ns/1.0/" id="http://api.constantcontact.
12
+ com/ws/customers/joesflowers/contacts/22199">
13
+ <Status>Active</Status>
14
+ <EmailAddress>joe@example.com</EmailAddress>
15
+ <EmailType>HTML</EmailType>
16
+ <Name>Customer Joe</Name>
17
+ <FirstName>Joe</FirstName>
18
+ <MiddleName></MiddleName>
19
+ <LastName>Smith</LastName>
20
+ <JobTitle></JobTitle>
21
+ <CompanyName></CompanyName>
22
+ <HomePhone></HomePhone>
23
+ <WorkPhone></WorkPhone>
24
+ <Addr1></Addr1>
25
+ <Addr2></Addr2>
26
+ <Addr3></Addr3>
27
+ <City></City>
28
+ <StateCode>MA</StateCode>
29
+ <StateName>Massachusetts</StateName>
30
+ <CountryCode>us</CountryCode>
31
+ <CountryName>United States</CountryName>
32
+ <PostalCode>02154</PostalCode>
33
+ <SubPostalCode>1781</SubPostalCode>
34
+ <Note></Note>
35
+ <CustomField1></CustomField1>
36
+ <CustomField2></CustomField2>
37
+ <CustomField3></CustomField3>
38
+ <CustomField4></CustomField4>
39
+ <CustomField5></CustomField5>
40
+ <CustomField6></CustomField6>
41
+ <CustomField7></CustomField7>
42
+ <CustomField8></CustomField8>
43
+ <CustomField9></CustomField9>
44
+ <CustomField10></CustomField10>
45
+ <CustomField11></CustomField11>
46
+ <CustomField12></CustomField12>
47
+ <CustomField13></CustomField13>
48
+ <CustomField14></CustomField14>
49
+ <CustomField15></CustomField15>
50
+ <ContactLists>
51
+ <ContactList id="http://api.constantcontact.com/ws/customers/joesflowers/lists/3">
52
+ <link xmlns="http://www.w3.org/2005/Atom" href="/ws/customers/joesflowers/lists/3" rel="self" />
53
+ <OptInSource>ACTION_BY_CONTACT</OptInSource>
54
+ <OptInTime>2009-11-20T20:41:06.595Z</OptInTime>
55
+ </ContactList>
56
+ </ContactLists>
57
+ <Confirmed>false</Confirmed>
58
+ <InsertTime>2009-11-20T20:41:06.593Z</InsertTime>
59
+ <LastUpdateTime>2009-11-20T20:41:19.243Z</LastUpdateTime>
60
+ </Contact>
61
+ </content>
62
+ <source>
63
+ <id>http://api.constantcontact.com/ws/customers/joesflowers/contacts</id>
64
+ <title type="text">Contacts for Customer: joesflowers</title>
65
+ <link href="contacts" />
66
+ <link href="contacts" rel="self" />
67
+ <author>
68
+ <name>joesflowers</name>
69
+ </author>
70
+ <updated>2009-11-20T20:45:44.199Z</updated>
71
+ </source>
72
+ </entry>
@@ -0,0 +1,54 @@
1
+ <?xml version='1.0' encoding='UTF-8'?>
2
+ <feed xmlns="http://www.w3.org/2005/Atom">
3
+ <id>http://api.constantcontact.com/ws/customers/joesflowers/contacts</id>
4
+ <title type="text">Contacts for Customer: joesflowers</title>
5
+ <link href="contacts" />
6
+ <link href="contacts" rel="self" />
7
+ <author>
8
+ <name>joesflowers</name>
9
+ </author>
10
+ <updated>2009-11-19T14:49:21.928Z</updated>
11
+ <link href="/ws/customers/joesflowers/contacts?next=g24umerp-fymhqg" rel="next" />
12
+ <link href="/ws/customers/joesflowers/contacts" rel="first" />
13
+ <link href="/ws/customers/joesflowers/contacts" rel="current" />
14
+ <entry>
15
+ <link href="/ws/customers/joesflowers/contacts/21930" rel="edit" />
16
+ <id>http://api.constantcontact.com/ws/customers/joesflowers/contacts/21930</id>
17
+ <title type="text">Contact: 1258642119407@example.com</title>
18
+ <updated>2009-11-19T14:49:23.203Z</updated>
19
+ <author>
20
+ <name>Constant Contact</name>
21
+ </author>
22
+ <content type="application/vnd.ctct+xml">
23
+ <Contact xmlns="http://ws.constantcontact.com/ns/1.0/" id="http://api.constantcontact.
24
+ com/ws/customers/joesflowers/contacts/21930">
25
+ <Status>Active</Status>
26
+ <EmailAddress>1258642119407@example.com</EmailAddress>
27
+ <EmailType>HTML</EmailType>
28
+ <Name>Customer 1</Name>
29
+ <OptInTime>2009-11-19T14:48:41.761Z</OptInTime>
30
+ <OptInSource>ACTION_BY_CONTACT</OptInSource>
31
+ </Contact>
32
+ </content>
33
+ </entry>
34
+ <entry>
35
+ <link href="/ws/customers/joesflowers/contacts/21929" rel="edit" />
36
+ <id>http://api.constantcontact.com/ws/customers/joesflowers/contacts/21929</id>
37
+ <title type="text">Contact: 1258641848349@example.com</title>
38
+ <updated>2009-11-19T14:49:23.204Z</updated>
39
+ <author>
40
+ <name>Constant Contact</name>
41
+ </author>
42
+ <content type="application/vnd.ctct+xml">
43
+ <Contact xmlns="http://ws.constantcontact.com/ns/1.0/" id="http://api.constantcontact.
44
+ com/ws/customers/joesflowers/contacts/21929">
45
+ <Status>Active</Status>
46
+ <EmailAddress>1258641848349@example.com</EmailAddress>
47
+ <EmailType>HTML</EmailType>
48
+ <Name>Customer 2</Name>
49
+ <OptInTime>2009-11-19T14:44:10.873Z</OptInTime>
50
+ <OptInSource>ACTION_BY_CONTACT</OptInSource>
51
+ </Contact>
52
+ </content>
53
+ </entry>
54
+ </feed>
@@ -0,0 +1,18 @@
1
+ <entry xmlns="http://www.w3.org/2005/Atom">
2
+ <title type="text"> </title>
3
+ <updated>2008-07-23T14:21:06.407Z</updated>
4
+ <author></author>
5
+ <id>data:,none</id>
6
+ <summary type="text">Contact</summary>
7
+ <content type="application/vnd.ctct+xml">
8
+ <Contact xmlns="http://ws.constantcontact.com/ns/1.0/">
9
+ <EmailAddress>test_100@example.com</EmailAddress>
10
+ <FirstName>First</FirstName>
11
+ <LastName>Last</LastName>
12
+ <OptInSource>ACTION_BY_CONTACT</OptInSource>
13
+ <ContactLists>
14
+ <ContactList id="http://api.constantcontact.com/ws/customers/geeksquad/joesflowers/lists/1" />
15
+ </ContactLists>
16
+ </Contact>
17
+ </content>
18
+ </entry>
@@ -0,0 +1,21 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'fakeweb'
4
+
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib', 'constant_contact'))
8
+ require 'constant-contact-ruby'
9
+
10
+ class Test::Unit::TestCase
11
+
12
+ fixtures_path = File.join( File.dirname( __FILE__ ), 'fixtures' )
13
+
14
+ # setup FakeWeb
15
+ FakeWeb.allow_net_connect = false
16
+ # GET /contacts
17
+ FakeWeb.register_uri( :get, %r{https://.+:.+@api\.constantcontact\.com/ws/customers/.+/contacts$}, :body => File.read( File.join( fixtures_path, 'get_contacts.xml' ) ) )
18
+ # GET /contact/:id
19
+ FakeWeb.register_uri( :get, %r{https://.+:.+@api\.constantcontact\.com/ws/customers/.+/contact/\d+$}, :body => File.read( File.join( fixtures_path, 'get_contact.xml' ) ) )
20
+
21
+ end
@@ -0,0 +1,8 @@
1
+ require 'helper'
2
+
3
+ class TestConstantContactRuby < Test::Unit::TestCase
4
+ def test_setup
5
+ ConstantContact.setup( 'u', 'p' )
6
+ assert_equal 'https://api.constantcontact.com/ws/customers/u', ConstantContact.base_uri
7
+ end
8
+ end
@@ -0,0 +1,49 @@
1
+ require 'helper'
2
+ require 'contact'
3
+
4
+ class TestContact < Test::Unit::TestCase
5
+
6
+ def setup
7
+ ConstantContact.setup( 'user', 'password' )
8
+ end
9
+
10
+ def test_all_contacts
11
+ contacts = ConstantContact::Contact.all
12
+ assert !contacts.nil?
13
+ assert_equal 2, contacts.size
14
+ assert_equal ConstantContact::Contact, contacts.first.class
15
+
16
+ assert_equal 'Customer 1', contacts.first.name
17
+ assert_equal '21930', contacts.first.uid
18
+ end
19
+
20
+ def test_all_contacts_with_bad_credentials
21
+ # FakeWeb.register_uri( :get, %r{https://.+:.+@api\.constantcontact\.com/ws/customers/.+/contacts}, :body => '', :status => ['403', 'Not Authorized'] )
22
+ # contacts = ConstantContact::Contact.all
23
+ # assert contacts.empty?
24
+ end
25
+
26
+ def test_get_contact
27
+ contact = ConstantContact::Contact.get( 22199 )
28
+ assert_equal 'Customer Joe', contact.name
29
+ assert_equal '22199', contact.uid
30
+ assert_equal 'joe@example.com', contact.emailaddress
31
+
32
+ # TODO: contactLists
33
+ end
34
+
35
+ def test_add_contact
36
+ # c = ConstantContact::Contact.add(
37
+ # :email_address => 'test@example.com',
38
+ # :first_name => 'First',
39
+ # :last_name => 'Name',
40
+ # :opt_in_source => 'ACTION_BY_CONTACT', # or ACTION_BY_CUSTOMER
41
+ # :contact_lists => [1]
42
+ # )
43
+
44
+ # assert_equal 'test@example.com', c.email_address
45
+ #
46
+ # contact = ConstantContact::Contact.get( c.uid )
47
+ # assert_equal 'test@example.com', contact.email_address
48
+ end
49
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: constant-contact-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Craig P Jolicoeur
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-03-03 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Ruby wrapper around the Constant Contact REST API
17
+ email: cpjolicoeur@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - LICENSE
24
+ - README.rdoc
25
+ - TODO
26
+ files:
27
+ - .document
28
+ - .gitignore
29
+ - LICENSE
30
+ - README.rdoc
31
+ - Rakefile
32
+ - TODO
33
+ - VERSION.yml
34
+ - constant-contact-ruby.gemspec
35
+ - constant-contact-ruby.rb
36
+ - lib/constant_contact.rb
37
+ - lib/constant_contact/activity.rb
38
+ - lib/constant_contact/base_resource.rb
39
+ - lib/constant_contact/contact.rb
40
+ - lib/constant_contact/contact_list.rb
41
+ - test/fixtures/get_contact.xml
42
+ - test/fixtures/get_contacts.xml
43
+ - test/fixtures/post_contacts.xml
44
+ - test/helper.rb
45
+ - test/test_constant-contact-ruby.rb
46
+ - test/test_contacts.rb
47
+ has_rdoc: true
48
+ homepage: http://github.com/cpjolicoeur/constant-contact-ruby
49
+ licenses: []
50
+
51
+ post_install_message:
52
+ rdoc_options:
53
+ - --charset=UTF-8
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: "0"
67
+ version:
68
+ requirements: []
69
+
70
+ rubyforge_project:
71
+ rubygems_version: 1.3.5
72
+ signing_key:
73
+ specification_version: 3
74
+ summary: Constant Contact Ruby API Wrapper
75
+ test_files:
76
+ - test/helper.rb
77
+ - test/test_constant-contact-ruby.rb
78
+ - test/test_contacts.rb