constant_contact 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +56 -0
  3. data/lib/constant_contact.rb +13 -0
  4. data/lib/constant_contact/activity.rb +41 -0
  5. data/lib/constant_contact/base.rb +150 -0
  6. data/lib/constant_contact/campaign.rb +112 -0
  7. data/lib/constant_contact/contact.rb +82 -0
  8. data/lib/constant_contact/contact_event.rb +7 -0
  9. data/lib/constant_contact/email_address.rb +6 -0
  10. data/lib/constant_contact/formats.rb +3 -0
  11. data/lib/constant_contact/formats/atom_format.rb +78 -0
  12. data/lib/constant_contact/formats/html_encoded_format.rb +17 -0
  13. data/lib/constant_contact/list.rb +11 -0
  14. data/lib/constant_contact/member.rb +5 -0
  15. data/lib/constant_contact/version.rb +3 -0
  16. data/test/constant_contact/activity_test.rb +41 -0
  17. data/test/constant_contact/atom_format_test.rb +28 -0
  18. data/test/constant_contact/base_test.rb +85 -0
  19. data/test/constant_contact/contact_test.rb +135 -0
  20. data/test/constant_contact/list_test.rb +17 -0
  21. data/test/constant_contact/member_test.rb +25 -0
  22. data/test/constant_contact_test.rb +8 -0
  23. data/test/fixtures/all_contacts.xml +51 -0
  24. data/test/fixtures/contactlistscollection.xml +96 -0
  25. data/test/fixtures/contactlistsindividuallist.xml +30 -0
  26. data/test/fixtures/memberscollection.xml +763 -0
  27. data/test/fixtures/new_list.xml +16 -0
  28. data/test/fixtures/nocontent.xml +12 -0
  29. data/test/fixtures/service_document.xml +15 -0
  30. data/test/fixtures/single_contact_by_email.xml +34 -0
  31. data/test/fixtures/single_contact_by_id.xml +71 -0
  32. data/test/fixtures/single_contact_by_id_with_no_contactlists.xml +65 -0
  33. data/test/test_helper.rb +46 -0
  34. metadata +186 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 [name of plugin creator]
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.
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ ConstantContact
2
+ ===============
3
+ This is a very ActiveResource-like ruby wrapper to the Constant Contact API. It's not quite done yet and if you are interested in using this code, check it out, and message me with questions.
4
+
5
+ See the [ActiveResource::Base docs](http://api.rubyonrails.org/classes/ActiveResource/Base.html) for more information on how to use this ActiveResource-based wrapper.
6
+
7
+ Examples
8
+ --------
9
+
10
+ All examples require setting up either the specific class you'll be use or the Base object before use:
11
+
12
+ ConstantContact::Base.user = 'user'
13
+ ConstantContact::Base.api_key = 'api-key'
14
+ ConstantContact::Base.password = 'password'
15
+
16
+
17
+ ### Find Lists
18
+
19
+ ConstantContact::List.find(1)
20
+ ConstantContact::List.find :all
21
+
22
+ ### Find A Contact
23
+
24
+
25
+ ConstantContact::Contact.find(1)
26
+ ConstantContact::Contact.find(:first, :params => {:email => 'jon@example.com'})
27
+ ConstantContact::Contact.find_by_email('jon@example.com') # => same as previous line
28
+
29
+ ### Create a Contact (with rescue if it already exists)
30
+
31
+ ConstantContact::Base.user = 'user'
32
+ ConstantContact::Base.api_key = 'api-key'
33
+ ConstantContact::Base.password = 'password'
34
+
35
+ # Contact not found. Create it.
36
+ begin
37
+ @contact = ConstantContact::Contact.new(
38
+ :email_address => "jon@example.com",
39
+ :first_name => "jon",
40
+ :last_name => "smith"
41
+ )
42
+ @contact.save
43
+ rescue ActiveResource::ResourceConflict => e
44
+ # contact already exists
45
+ puts 'Contact already exists. Saving contact failed.'
46
+ puts e
47
+ end
48
+
49
+ ### Find a Contact By Email Address, Check if They're a Member of the Default List
50
+
51
+ c = ConstantContact::Contact.find_by_email('jon@example.com')
52
+ @contact = ConstantContact::Contact.find(@contact.int_id)
53
+ puts 'In default contact list.' if @contact.contact_lists.include?(1) # contact_lists is an array of list ids
54
+
55
+
56
+ Copyright (c) 2009 Timothy Case, released under the MIT license
@@ -0,0 +1,13 @@
1
+ directory = File.expand_path(File.dirname(__FILE__))
2
+
3
+ require 'active_resource'
4
+ require 'action_pack'
5
+ require File.join(directory, 'constant_contact', 'formats')
6
+ require File.join(directory, 'constant_contact', 'base')
7
+ require File.join(directory, 'constant_contact', 'list');
8
+ require File.join(directory, 'constant_contact', 'member')
9
+ require File.join(directory, 'constant_contact', 'contact')
10
+ require File.join(directory, 'constant_contact', 'campaign')
11
+ require File.join(directory, 'constant_contact', 'contact_event')
12
+ require File.join(directory, 'constant_contact', 'activity')
13
+ require File.join(directory, 'constant_contact', 'email_address')
@@ -0,0 +1,41 @@
1
+
2
+ module ConstantContact
3
+ class Activity < Base
4
+ self.format = ActiveResource::Formats::HtmlEncodedFormat
5
+ attr_accessor :contacts, :lists, :activity_type
6
+
7
+ def encode
8
+ post_data = "activityType=#{self.activity_type}"
9
+ post_data += self.encoded_data
10
+ post_data += self.encoded_lists
11
+ return post_data
12
+ end
13
+
14
+ def activity_type
15
+ @activity_type ||= "SV_ADD"
16
+ end
17
+
18
+ protected
19
+ def encoded_data
20
+ result = "&data="
21
+ result += CGI.escape("Email Address,First Name,Last Name\n")
22
+ contact_strings = []
23
+ self.contacts.each do |contact|
24
+ contact_strings << "#{contact.email_address}, #{contact.first_name}, #{contact.last_name}"
25
+ end
26
+ result += CGI.escape(contact_strings.join("\n"))
27
+ return result
28
+ end
29
+
30
+
31
+ def encoded_lists
32
+ result = ""
33
+ self.lists.each do |list|
34
+ result += "&lists="
35
+ result += CGI.escape(list.id)
36
+ end
37
+ return result
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,150 @@
1
+ module ConstantContact
2
+ class Base < ActiveResource::Base
3
+
4
+ self.site = "https://api.constantcontact.com"
5
+ self.format = :atom
6
+
7
+ DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
8
+
9
+ class << self
10
+ # Returns an integer which can be used in #find calls.
11
+ # Assumes url structure with the id at the end, e.g.:
12
+ # http://api.constantcontact.com/ws/customers/yourname/contacts/29
13
+ def parse_id(url)
14
+ url.to_s.split(/\//).last.to_i
15
+ end
16
+
17
+ def api_key
18
+ if defined?(@api_key)
19
+ @api_key
20
+ elsif superclass != Object && superclass.api_key
21
+ superclass.api_key.dup.freeze
22
+ end
23
+ end
24
+
25
+ def api_key=(api_key)
26
+ @connection = nil
27
+ @api_key = api_key
28
+ end
29
+
30
+ def connection(refresh = false)
31
+ if defined?(@connection) || superclass == Object
32
+ @connection = ActiveResource::Connection.new(site, format) if refresh || @connection.nil?
33
+ @connection.user = "#{api_key}%#{user}" if user
34
+ @connection.password = password if password
35
+ @connection.timeout = timeout if timeout
36
+ @connection
37
+ else
38
+ superclass.connection
39
+ end
40
+ end
41
+
42
+ def collection_path(prefix_options = {}, query_options = nil)
43
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
44
+ "/ws/customers/#{self.user}#{prefix(prefix_options)}#{collection_name}#{query_string(query_options)}"
45
+ end
46
+
47
+ def element_path(id, prefix_options = {}, query_options = nil)
48
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
49
+ integer_id = parse_id(id)
50
+ id_val = integer_id.zero? ? nil : "/#{integer_id}"
51
+ "#{collection_path}#{id_val}#{query_string(query_options)}"
52
+ end
53
+
54
+ # Slight modification to AR::Base.find_every to handle instances
55
+ # where a single element is returned. This enables calling
56
+ # <tt>find(:first, {:params => {:email => 'sample@example.com'}})
57
+ def find_every(options)
58
+ case from = options[:from]
59
+ when Symbol
60
+ instantiate_collection(get(from, options[:params]))
61
+ when String
62
+ path = "#{from}#{query_string(options[:params])}"
63
+ instantiate_collection(connection.get(path, headers) || [])
64
+ else
65
+ prefix_options, query_options = split_options(options[:params])
66
+ path = collection_path(prefix_options, query_options)
67
+ result = connection.get(path, headers)
68
+ case result.class.name
69
+ when 'Hash': instantiate_collection( [ result ], prefix_options )
70
+ else
71
+ instantiate_collection( (result || []), prefix_options )
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ # Slightly tweaked ARes::Base's implementation so all the
78
+ # attribute names are looked up using camelcase since
79
+ # that's how the CC API returns them.
80
+ def method_missing(method_symbol, *arguments) #:nodoc:
81
+ method_name = method_symbol.to_s
82
+
83
+ case method_name.last
84
+ when "="
85
+ attributes[method_name.first(-1).camelize] = arguments.first
86
+ when "?"
87
+ attributes[method_name.first(-1).camelize]
88
+ else
89
+ attributes.has_key?(method_name.camelize) ? attributes[method_name.camelize] : super
90
+ end
91
+ end
92
+
93
+ # Caching accessor for the the id integer
94
+ def int_id
95
+ @id ||= self.class.parse_id(self.attributes['id'])
96
+ end
97
+
98
+ # Mimics ActiveRecord's version
99
+ def update_attributes(atts={})
100
+ camelcased_hash = {}
101
+ atts.each{|key, val| camelcased_hash[key.to_s.camelize] = val}
102
+ self.attributes.update(camelcased_hash)
103
+ save
104
+ end
105
+
106
+ # Mimic ActiveRecord (snagged from HyperactiveResource).
107
+ def save
108
+ return false unless valid?
109
+ before_save
110
+ successful = super
111
+ after_save if successful
112
+ successful
113
+ end
114
+
115
+ def before_save
116
+ end
117
+
118
+ def after_save
119
+ end
120
+
121
+ def validate
122
+ end
123
+
124
+ # So client-side validations run
125
+ def valid?
126
+ errors.clear
127
+ validate
128
+ super
129
+ end
130
+
131
+ def encode
132
+ "<entry xmlns=\"http://www.w3.org/2005/Atom\">
133
+ <title type=\"text\"> </title>
134
+ <updated>#{Time.now.strftime(DATE_FORMAT)}</updated>
135
+ <author>Bluesteel</author>
136
+ <id>#{id.blank? ? 'data:,none' : id}</id>
137
+ <summary type=\"text\">Bluesteel</summary>
138
+ <content type=\"application/vnd.ctct+xml\">
139
+ #{self.to_xml}
140
+ </content>
141
+ </entry>"
142
+ end
143
+
144
+ # TODO: Move this out to a lib
145
+ def html_encode(txt)
146
+ mapping = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;' }
147
+ txt.to_s.gsub(/[&"><]/) { |special| mapping[special] }
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,112 @@
1
+ # http://developer.constantcontact.com/doc/manageCampaigns
2
+ module ConstantContact
3
+ class Campaign < Base
4
+ STATUS_CODES = ['SENT', 'SCHEDULED', 'DRAFT', 'RUNNING']
5
+ # SENT All campaigns that have been sent and not currently scheduled for resend
6
+ # SCHEDULED All campaigns that are currently scheduled to be sent some time in the future
7
+ # DRAFT All campaigns that have not yet been scheduled for delivery
8
+ # RUNNING All campaigns that are currently being processed and delivered
9
+ @@column_names = [:archive_status, :archive_url, :bounces, :campaign_type, :clicks, :contact_lists, :date,
10
+ :email_content, :email_content_format, :email_text_content, :forward_email_link_text, :forwards,
11
+ :from_email, :from_name, :greeting_name, :greeting_salutation, :greeting_string,
12
+ :include_forward_email, :include_subscribe_link, :last_edit_date, :name, :opens, :opt_outs,
13
+ :organization_address1, :organization_address2, :organization_address3, :organization_city,
14
+ :organization_country, :organization_international_state, :organization_name, :organization_postal_code,
15
+ :organization_state, :permission_reminder, :reply_to_email, :sent, :spam_reports, :status,
16
+ :style_sheet, :subject, :subscribe_link_text, :view_as_webpage, :view_as_webpage_link_text, :view_as_webpage_text]
17
+
18
+
19
+ # Setup defaults when creating a new object since
20
+ # CC requires so many extraneous fields to be present
21
+ # when creating a new Campaign.
22
+ def initialize
23
+ obj = super
24
+ obj.set_defaults
25
+ obj
26
+ end
27
+
28
+ def to_xml
29
+ xml = Builder::XmlMarkup.new
30
+ xml.tag!("Campaign", :xmlns => "http://ws.constantcontact.com/ns/1.0/") do
31
+ self.attributes.each{ |k, v| xml.tag!(k.to_s.camelize, v) }
32
+ # Overrides the default formatting above to CC's required format.
33
+ xml.tag!("ReplyToEmail") do
34
+ xml.tag!('Email', :id => self.reply_to_email_url)
35
+ end
36
+ xml.tag!("FromEmail") do
37
+ xml.tag!('Email', :id => self.from_email_url)
38
+ end
39
+ xml.tag!("ContactLists") do
40
+ xml.tag!("ContactList", :id => self.list_url)
41
+ end
42
+ end
43
+ end
44
+
45
+ def list_url
46
+ id = defined?(self.list_id) ? self.list_id : 1
47
+ List.find(id).id
48
+ end
49
+
50
+ def from_email_url
51
+ id = defined?(self.from_email_id) ? self.from_email_id : 1
52
+ EmailAddress.find(id).id
53
+ end
54
+
55
+ def reply_to_email_url
56
+ from_email_url
57
+ end
58
+
59
+
60
+ protected
61
+ def set_defaults
62
+ self.view_as_webpage = 'NO' unless attributes.has_key?('ViewAsWebpage')
63
+ self.from_name = self.class.user unless attributes.has_key?('FromName')
64
+ self.permission_reminder = 'YES' unless attributes.has_key?('PermissionReminder')
65
+ self.permission_reminder_text = %Q{You're receiving this email because of your relationship with us. Please <ConfirmOptin><a style="color:#0000ff;">confirm</a></ConfirmOptin> your continued interest in receiving email from us.} unless attributes.has_key?('PermissionReminderText')
66
+ self.greeting_salutation = 'Dear' unless attributes.has_key?('GreetingSalutation')
67
+ self.greeting_name = "FirstName" unless attributes.has_key?('GreetingName')
68
+ self.greeting_string = 'Greetings!' unless attributes.has_key?('GreetingString')
69
+ self.status = 'DRAFT' unless attributes.has_key?('Status')
70
+ self.style_sheet = '' unless attributes.has_key?('StyleSheet')
71
+ self.include_forward_email = 'NO' unless attributes.has_key?('IncludeForwardEmail')
72
+ self.forward_email_link_text = '' unless attributes.has_key?('ForwardEmailLinkText')
73
+ self.subscribe_link_text = '' unless attributes.has_key?('SubscribeLinkText')
74
+ self.include_subscribe_link = 'NO' unless attributes.has_key?('IncludeSubscribeLink')
75
+ self.organization_name = self.class.user unless attributes.has_key?('OrganizationName')
76
+ self.organization_address1 = '123 Main' unless attributes.has_key?('OrganizationAddress1')
77
+ self.organization_address2 = '' unless attributes.has_key?('OrganizationAddress2')
78
+ self.organization_address3 = '' unless attributes.has_key?('OrganizationAddress3')
79
+ self.organization_city = 'Kansas City' unless attributes.has_key?('OrganizationCity')
80
+ self.organization_state = 'KS' unless attributes.has_key?('OrganizationState')
81
+ self.organization_international_state = '' unless attributes.has_key?('OrganizationInternationalState')
82
+ self.organization_country = 'US' unless attributes.has_key?('OrganizationCountry')
83
+ self.organization_postal_code = '64108' unless attributes.has_key?('OrganizationPostalCode')
84
+ end
85
+
86
+ # Formats data if present.
87
+ def before_save
88
+ self.email_text_content = "<Text>#{email_text_content}</Text>" unless email_text_content.match /^\<Text/
89
+ self.date = self.date.strftime(DATE_FORMAT) if attributes.has_key?('Date')
90
+ end
91
+
92
+
93
+ def validate
94
+ # NOTE: Needs to be uppercase!
95
+ unless attributes.has_key?('EmailContentFormat') && ['HTML', 'XHTML'].include?(email_content_format)
96
+ errors.add(:email_content_format, 'must be either HTML or XHTML (the latter for advanced email features)')
97
+ end
98
+
99
+ if attributes.has_key?('ViewAsWebpage') && view_as_webpage.downcase == 'yes'
100
+ unless attributes['ViewAsWebpageLinkText'].present? && attributes['ViewAsWebpageText'].present?
101
+ errors.add(:view_as_webpage, "You need to set view_as_webpage_link_text and view_as_webpage_link if view_as_webpage is YES")
102
+ end
103
+ end
104
+
105
+ errors.add(:email_content, 'cannot be blank') unless attributes.has_key?('EmailContent')
106
+ errors.add(:email_text_content, 'cannot be blank') unless attributes.has_key?('EmailTextContent')
107
+ errors.add(:name, 'cannot be blank') unless attributes.has_key?('Name')
108
+ errors.add(:subject, 'cannot be blank') unless attributes.has_key?('Subject')
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,82 @@
1
+ # Value limits: http://constantcontact.custhelp.com/cgi-bin/constantcontact.cfg/php/enduser/std_adp.php?p_faqid=2217
2
+ module ConstantContact
3
+ class Contact < Base
4
+ attr_accessor :opt_in_source
5
+
6
+ # NOTE: All column names are available only when finding a contact by id.
7
+ # @@column_names = [ :addr1, :addr2, :addr3, :city, :company_name, :country_code, :country_name,
8
+ # :custom_field1, :custom_field10, :custom_field11, :custom_field12, :custom_field13,
9
+ # :custom_field14, :custom_field15, :custom_field2, :custom_field3, :custom_field4, :custom_field5,
10
+ # :custom_field6, :custom_field7, :custom_field8, :custom_field9, :email_address, :email_type,
11
+ # :first_name, :home_phone, :insert_time, :job_title, :last_name, :last_update_time, :name, :note,
12
+ # :postal_code, :state_code, :state_name, :status, :sub_postal_code, :work_phone ]
13
+
14
+ def to_xml
15
+ xml = Builder::XmlMarkup.new
16
+ xml.tag!("Contact", :xmlns => "http://ws.constantcontact.com/ns/1.0/") do
17
+ self.attributes.reject {|k,v| k == 'ContactLists'}.each{|k, v| xml.tag!( k.to_s.camelize, v )}
18
+ xml.tag!("OptInSource", self.opt_in_source)
19
+ xml.tag!("ContactLists") do
20
+ @contact_lists = [1] if @contact_lists.nil? && self.new?
21
+ self.contact_lists.sort.each do |list_id|
22
+ xml.tag!("ContactList", :id=> self.list_url(list_id))
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ def opt_in_source
29
+ @opt_in_source ||= "ACTION_BY_CUSTOMER"
30
+ end
31
+
32
+ # see http://developer.constantcontact.com/doc/manageContacts#create_contact for more info about the two values.
33
+ def opt_in_source=(val)
34
+ @opt_in_source = val if ['ACTION_BY_CONTACT','ACTION_BY_CUSTOMER'].include?(val)
35
+ end
36
+
37
+ def list_url(id=nil)
38
+ id ||= defined?(self.list_id) ? self.list_id : 1
39
+ "http://api.constantcontact.com/ws/customers/#{self.class.user}/lists/#{id}"
40
+ end
41
+
42
+ # Can we email them?
43
+ def active?
44
+ status.downcase == 'active'
45
+ end
46
+
47
+ def contact_lists
48
+ return @contact_lists if defined?(@contact_lists)
49
+ # otherwise, attempt to assign it
50
+ @contact_lists = if self.attributes.keys.include?('ContactLists')
51
+ if self.ContactLists
52
+ if self.ContactLists.ContactList.is_a?(Array)
53
+ self.ContactLists.ContactList.collect { |list|
54
+ ConstantContact::Base.parse_id(list.id)
55
+ }
56
+ else
57
+ [ ConstantContact::Base.parse_id(self.ContactLists.ContactList.id) ]
58
+ end
59
+ else
60
+ [] # => Contact is not a member of any lists (legitimatly empty!)
61
+ end
62
+ else
63
+ nil
64
+ end
65
+ end
66
+
67
+ def contact_lists=(val)
68
+ @contact_lists = val.kind_of?(Array) ? val : [val]
69
+ end
70
+
71
+ def self.find_by_email(email_address)
72
+ find :first, {:params => {:email => email_address.downcase}}
73
+ end
74
+
75
+ protected
76
+ def validate
77
+ # errors.add(:opt_in_source, 'must be either ACTION_BY_CONTACT or ACTION_BY_CUSTOMER') unless ['ACTION_BY_CONTACT','ACTION_BY_CUSTOMER'].include?(attributes['OptInSource'])
78
+ # errors.add(:email_address, 'cannot be blank') unless attributes.has_key?('EmailAddress')
79
+ end
80
+
81
+ end
82
+ end