From 1bf4f722e4fa4d075140496c707aa2a2c3d23fea Mon Sep 17 00:00:00 2001 From: Ed Hickey Date: Wed, 24 Mar 2010 12:03:05 -0500 Subject: [PATCH] Added some convenience code to deal with ConstantContact's camelcased XML nodes (and ruby's tradition underscore'd) Added some ActiveRecord-ish functionality: custom validation (via validate()), update_attributes(), and before_save and after_save hooks Beginnings of defining Email addresses (email_address.rb) Beginnings of Campaign creation. --- lib/constant_contact.rb | 3 +- lib/constant_contact/base.rb | 79 +++++++++++++++++++-- lib/constant_contact/campaign.rb | 114 +++++++++++++++++++++++++++++++ lib/constant_contact/contact.rb | 15 +++- lib/constant_contact/list.rb | 2 + 5 files changed, 203 insertions(+), 10 deletions(-) diff --git a/lib/constant_contact.rb b/lib/constant_contact.rb index e268f79..c8898aa 100644 --- a/lib/constant_contact.rb +++ b/lib/constant_contact.rb @@ -9,4 +9,5 @@ require File.join(directory, 'constant_contact', 'contact') require File.join(directory, 'constant_contact', 'campaign') require File.join(directory, 'constant_contact', 'contact_event') -require File.join(directory, 'constant_contact', 'activity') \ No newline at end of file +require File.join(directory, 'constant_contact', 'activity') +require File.join(directory, 'constant_contact', 'email_address') \ No newline at end of file diff --git a/lib/constant_contact/base.rb b/lib/constant_contact/base.rb index 676835a..4111ef9 100644 --- a/lib/constant_contact/base.rb +++ b/lib/constant_contact/base.rb @@ -1,11 +1,19 @@ - module ConstantContact class Base < ActiveResource::Base + self.site = "https://api.constantcontact.com" self.format = :atom + + DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" + class << self - - + # Returns an integer which can be used in #find calls. + # Assumes url structure with the id at the end, e.g.: + # http://api.constantcontact.com/ws/customers/yourname/contacts/29 + def parse_id(url) + url.to_s.split(/\//).last.to_i + end + def api_key if defined?(@api_key) @api_key @@ -38,16 +46,69 @@ def collection_path(prefix_options = {}, query_options = nil) def element_path(id, prefix_options = {}, query_options = nil) prefix_options, query_options = split_options(prefix_options) if query_options.nil? - "#{collection_path}/#{id}" + integer_id = parse_id(id) + id_val = integer_id.zero? ? nil : "/#{integer_id}" + "#{collection_path}#{id_val}#{query_string(query_options)}" end end + # Slightly tweaked ARes::Base's implementation so all the + # attribute names are looked up using camelcase since + # that's how the CC API returns them. + def method_missing(method_symbol, *arguments) #:nodoc: + method_name = method_symbol.to_s + + case method_name.last + when "=" + attributes[method_name.first(-1).camelize] = arguments.first + when "?" + attributes[method_name.first(-1).camelize] + else + attributes.has_key?(method_name.camelize) ? attributes[method_name.camelize] : super + end + end + + # Caching accessor for the the id integer + def int_id + @id ||= self.class.parse_id(self.attributes['id']) + end + + # Mimics ActiveRecord's version + def update_attributes(atts={}) + camelcased_hash = {} + atts.each{|key, val| camelcased_hash[key.to_s.camelize] = val} + self.attributes.update(camelcased_hash) + save + end + + # Mimic ActiveRecord (snagged from HyperactiveResource). + def save + return false unless valid? + before_save + successful = super + after_save if successful + successful + end + + def before_save + end + + def after_save + end + + # So client-side validations run + def valid? + errors.clear + validate + super + end + def encode " - 2008-07-23T14:21:06.407Z + #{Time.now.strftime(DATE_FORMAT)} Bluesteel - data:,none + #{id.blank? ? 'data:,none' : id} Bluesteel #{self.to_xml} @@ -55,6 +116,10 @@ def encode " end - + # TODO: Move this out to a lib + def html_encode(txt) + mapping = { '&' => '&', '>' => '>', '<' => '<', '"' => '"' } + txt.to_s.gsub(/[&"><]/) { |special| mapping[special] } + end end end \ No newline at end of file diff --git a/lib/constant_contact/campaign.rb b/lib/constant_contact/campaign.rb index 2be9975..d229694 100644 --- a/lib/constant_contact/campaign.rb +++ b/lib/constant_contact/campaign.rb @@ -1,4 +1,118 @@ +# http://developer.constantcontact.com/doc/manageCampaigns module ConstantContact class Campaign < Base + STATUS_CODES = ['SENT', 'SCHEDULED', 'DRAFT', 'RUNNING'] + # SENT All campaigns that have been sent and not currently scheduled for resend + # SCHEDULED All campaigns that are currently scheduled to be sent some time in the future + # DRAFT All campaigns that have not yet been scheduled for delivery + # RUNNING All campaigns that are currently being processed and delivered + @@column_names = [:archive_status, :archive_url, :bounces, :campaign_type, :clicks, :contact_lists, :date, + :email_content, :email_content_format, :email_text_content, :forward_email_link_text, :forwards, + :from_email, :from_name, :greeting_name, :greeting_salutation, :greeting_string, + :include_forward_email, :include_subscribe_link, :last_edit_date, :name, :opens, :opt_outs, + :organization_address1, :organization_address2, :organization_address3, :organization_city, + :organization_country, :organization_international_state, :organization_name, :organization_postal_code, + :organization_state, :permission_reminder, :reply_to_email, :sent, :spam_reports, :status, + :style_sheet, :subject, :subscribe_link_text, :view_as_webpage, :view_as_webpage_link_text, :view_as_webpage_text] + + + # Setup defaults when creating a new object since + # CC requires so many extraneous fields to be present + # when creating a new Campaign. + def initialize + obj = super + obj.set_defaults + obj + end + + def to_xml + xml = Builder::XmlMarkup.new + xml.tag!("Campaign", :xmlns => "http://ws.constantcontact.com/ns/1.0/") do + self.attributes.each{ |k, v| xml.tag!(k.to_s.camelize, v) } + # Overrides the default formatting above to CC's required format. + xml.tag!("ReplyToEmail") do + xml.tag!('Email', :id => self.reply_to_email_url) + end + xml.tag!("FromEmail") do + xml.tag!('Email', :id => self.from_email_url) + end + xml.tag!("ContactLists") do + xml.tag!("ContactList", :id => self.list_url) + end + end + end + + def list_url + id = defined?(self.list_id) ? self.list_id : 1 + List.find(id).id + end + + def from_email_url + id = defined?(self.from_email_id) ? self.from_email_id : 1 + EmailAddress.find(id).id + end + + def reply_to_email_url + from_email_url + end + + + protected + def set_defaults + self.view_as_webpage = 'NO' unless attributes.has_key?('ViewAsWebpage') + self.from_name = self.class.user unless attributes.has_key?('FromName') + self.permission_reminder = 'YES' unless attributes.has_key?('PermissionReminder') + 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') + self.greeting_salutation = 'Dear' unless attributes.has_key?('GreetingSalutation') + self.greeting_name = "FirstName" unless attributes.has_key?('GreetingName') + self.greeting_string = 'Greetings!' unless attributes.has_key?('GreetingString') + self.status = 'DRAFT' unless attributes.has_key?('Status') + self.style_sheet = '' unless attributes.has_key?('StyleSheet') + self.include_forward_email = 'NO' unless attributes.has_key?('IncludeForwardEmail') + self.forward_email_link_text = '' unless attributes.has_key?('ForwardEmailLinkText') + self.subscribe_link_text = '' unless attributes.has_key?('SubscribeLinkText') + self.include_subscribe_link = 'NO' unless attributes.has_key?('IncludeSubscribeLink') + self.organization_name = self.class.user unless attributes.has_key?('OrganizationName') + self.organization_address1 = '123 Main' unless attributes.has_key?('OrganizationAddress1') + self.organization_address2 = '' unless attributes.has_key?('OrganizationAddress2') + self.organization_address3 = '' unless attributes.has_key?('OrganizationAddress3') + self.organization_city = 'Kansas City' unless attributes.has_key?('OrganizationCity') + self.organization_state = 'KS' unless attributes.has_key?('OrganizationState') + self.organization_international_state = '' unless attributes.has_key?('OrganizationInternationalState') + self.organization_country = 'US' unless attributes.has_key?('OrganizationCountry') + self.organization_postal_code = '64108' unless attributes.has_key?('OrganizationPostalCode') + end + + # Encodes and formats data if present. + def before_save + unless @encoded + self.style_sheet = html_encode(style_sheet) if attributes.has_key?('StyleSheet') + self.permission_reminder_text = html_encode(permission_reminder_text) if attributes.has_key?('PermissionReminderText') + self.email_content = html_encode(email_content) + self.email_text_content = "#{email_text_content}" + self.date = self.date.strftime(DATE_FORMAT) if attributes.has_key?('Date') + @encoded = true + end + + end + + + def validate + unless attributes.has_key?('EmailContentFormat') && ['html', 'xhtml'].include?(email_content_format.downcase) + errors.add(:email_content_format, 'must be either HTML or XHTML (the latter for advanced email features)') + end + + if attributes.has_key?('ViewAsWebpage') && view_as_webpage.downcase == 'yes' + unless attributes['ViewAsWebpageLinkText'].present? && attributes['ViewAsWebpageText'].present? + 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") + end + end + + errors.add(:email_content, 'cannot be blank') unless attributes.has_key?('EmailContent') + errors.add(:email_text_content, 'cannot be blank') unless attributes.has_key?('EmailTextContent') + errors.add(:name, 'cannot be blank') unless attributes.has_key?('Name') + errors.add(:subject, 'cannot be blank') unless attributes.has_key?('Subject') + end + end end diff --git a/lib/constant_contact/contact.rb b/lib/constant_contact/contact.rb index 3fd53d7..62ba644 100644 --- a/lib/constant_contact/contact.rb +++ b/lib/constant_contact/contact.rb @@ -1,11 +1,19 @@ +# Value limits: http://constantcontact.custhelp.com/cgi-bin/constantcontact.cfg/php/enduser/std_adp.php?p_faqid=2217 module ConstantContact class Contact < Base attr_accessor :opt_in_source + # @@column_names = [ :addr1, :addr2, :addr3, :city, :company_name, :country_code, :country_name, + # :custom_field1, :custom_field10, :custom_field11, :custom_field12, :custom_field13, + # :custom_field14, :custom_field15, :custom_field2, :custom_field3, :custom_field4, :custom_field5, + # :custom_field6, :custom_field7, :custom_field8, :custom_field9, :email_address, :email_type, + # :first_name, :home_phone, :insert_time, :job_title, :last_name, :last_update_time, :name, :note, + # :postal_code, :state_code, :state_name, :status, :sub_postal_code, :work_phone ] + def to_xml xml = Builder::XmlMarkup.new xml.tag!("Contact", :xmlns => "http://ws.constantcontact.com/ns/1.0/") do - self.attributes.each{|k, v| xml.tag!(k.camelize, v)} + self.attributes.each{|k, v| xml.tag!( k.to_s.camelize, v )} xml.tag!("OptInSource", self.opt_in_source) xml.tag!("ContactLists") do xml.tag!("ContactList", :id=> self.list_url) @@ -21,7 +29,10 @@ def list_url id = defined?(self.list_id) ? self.list_id : 1 "http://api.constantcontact.com/ws/customers/#{self.class.user}/lists/#{id}" end - + # Can we email them? + def active? + status.downcase == 'active' + end end end \ No newline at end of file diff --git a/lib/constant_contact/list.rb b/lib/constant_contact/list.rb index 9fb28dc..09d9565 100644 --- a/lib/constant_contact/list.rb +++ b/lib/constant_contact/list.rb @@ -1,5 +1,7 @@ module ConstantContact class List < Base + + # @@column_names = [:contact_count, :display_on_signup, :members, :name, :opt_in_default, :short_name, :sort_order] def self.find_by_name(name) lists = self.find :all